diff --git a/.github/release-allowed-signers b/.github/release-allowed-signers new file mode 100644 index 00000000..500e102b --- /dev/null +++ b/.github/release-allowed-signers @@ -0,0 +1 @@ +steipete@gmail.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9XsaCcr8TInPnHcuTVfvXXcsoUFrOE7menfbEIHFW9 steipete@gmail.com diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml deleted file mode 100644 index fcefa77d..00000000 --- a/.github/workflows/release-assets.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: Release Assets - -on: - release: - types: [published] - workflow_dispatch: - inputs: - tag: - description: "Draft or published release tag to verify" - required: true - type: string - -permissions: - # GitHub hides draft releases from the workflow token without write visibility. - # This workflow only issues authenticated GET requests and never publishes. - contents: write - -jobs: - verify-macos-assets: - if: >- - (github.event_name == 'workflow_dispatch' && - github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && - endsWith(github.workflow_ref, format('@refs/heads/{0}', github.event.repository.default_branch))) || - (github.event_name == 'release' && github.event.action == 'published') - strategy: - fail-fast: false - matrix: - include: - - runs-on: macos-15 - arch: arm64 - - runs-on: macos-15-intel - arch: amd64 - runs-on: ${{ matrix.runs-on }} - env: - RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name }} - EXPECTED_DRAFT: ${{ github.event_name == 'workflow_dispatch' && 'true' || 'false' }} - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.repository.default_branch }} - persist-credentials: false - - - name: Download release assets - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+[.][0-9]+[.][0-9]+([.-][A-Za-z0-9._-]+)?$ ]]; then - echo "::error::Invalid release tag: $RELEASE_TAG" - exit 2 - fi - mkdir -p artifacts - version="${RELEASE_TAG#v}" - release_id=$(gh api --method GET "repos/$GITHUB_REPOSITORY/releases?per_page=100" | \ - jq -r --arg tag "$RELEASE_TAG" --arg draft "$EXPECTED_DRAFT" \ - '[.[] | select(.tag_name == $tag and (.draft == ($draft == "true")))] | if length == 1 then .[0].id else empty end') - [[ -n "$release_id" ]] || { - echo "::error::Expected exactly one release for $RELEASE_TAG with draft=$EXPECTED_DRAFT" - exit 1 - } - assets=$(gh api --method GET "repos/$GITHUB_REPOSITORY/releases/$release_id/assets?per_page=100") - expected_assets=$(printf '%s\n' \ - checksums.txt \ - "gitcrawl_${version}_darwin_amd64.tar.gz" \ - "gitcrawl_${version}_darwin_arm64.tar.gz" \ - "gitcrawl_${version}_linux_amd64.tar.gz" \ - "gitcrawl_${version}_linux_arm64.tar.gz" \ - "gitcrawl_${version}_windows_amd64.zip" \ - "gitcrawl_${version}_windows_arm64.zip" | LC_ALL=C sort) - actual_assets=$(jq -r '.[].name' <<<"$assets" | LC_ALL=C sort) - [[ "$actual_assets" == "$expected_assets" ]] || { - echo "::error::Unexpected release asset inventory" - diff -u <(printf '%s\n' "$expected_assets") <(printf '%s\n' "$actual_assets") || true - exit 1 - } - for name in "gitcrawl_${version}_darwin_${{ matrix.arch }}.tar.gz" checksums.txt; do - asset_id=$(jq -r --arg name "$name" \ - '[.[] | select(.name == $name)] | if length == 1 then .[0].id else empty end' <<<"$assets") - [[ -n "$asset_id" ]] || { - echo "::error::Expected exactly one release asset named $name" - exit 1 - } - asset_url=$(jq -r --arg name "$name" '.[] | select(.name == $name) | .url' <<<"$assets") - expected_url="https://api.github.com/repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" - [[ "$asset_url" == "$expected_url" ]] || { - echo "::error::Unexpected API URL for $name" - exit 1 - } - gh api --method GET -H "Accept: application/octet-stream" "$asset_url" >"artifacts/$name" - done - - - name: Verify Developer ID signature - shell: bash - run: | - unset GH_TOKEN GITHUB_TOKEN - ./scripts/verify-release.sh "$RELEASE_TAG" artifacts "${{ matrix.arch }}" diff --git a/.github/workflows/release-unified.yml b/.github/workflows/release-unified.yml new file mode 100644 index 00000000..d6f4989b --- /dev/null +++ b/.github/workflows/release-unified.yml @@ -0,0 +1,40 @@ +name: Release (unified) + +on: + workflow_dispatch: + inputs: + version: + description: SemVer to release + required: true + type: string + +permissions: {} + +jobs: + release: + permissions: + actions: read + checks: read + contents: write + pull-requests: write + statuses: read + uses: openclaw/release-workflows/.github/workflows/release-go-cli.yml@v1 + with: + version: ${{ inputs.version }} + repository-type: openclaw + homebrew-tap: openclaw/homebrew-tap + homebrew-formula: gitcrawl + archive-files: '["CHANGELOG.md","LICENSE","README.md"]' + checksum-filename: checksums.txt + nfpm: auto + stable-identifier: org.openclaw.gitcrawl + require-signed-tag: true + darwin-universal: disabled + ci-check-events: '["push","pull_request"]' + secrets: + MACOS_SIGNING_P12: ${{ secrets.MACOS_SIGNING_P12 }} + MACOS_SIGNING_P12_PASSWORD: ${{ secrets.MACOS_SIGNING_P12_PASSWORD }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + ASC_PRIVATE_KEY_P8: ${{ secrets.ASC_PRIVATE_KEY_P8 }} + TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index b8ecc3b3..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: release validation - -on: - push: - tags: - - "v*" - workflow_dispatch: - inputs: - tag: - description: "Tag to validate (e.g. v0.7.1)" - required: true - type: string - -permissions: - contents: read - -jobs: - goreleaser-snapshot: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v7 - with: - go-version-file: go.mod - cache-dependency-path: go.sum - - - name: Stash release tooling - shell: bash - run: | - set -euo pipefail - cp scripts/codesign-macos.sh /tmp/gitcrawl-codesign-macos.sh - sed 's#./scripts/codesign-macos.sh#/tmp/gitcrawl-codesign-macos.sh#' \ - .goreleaser.yaml > /tmp/.goreleaser.yaml - chmod +x /tmp/gitcrawl-codesign-macos.sh - - - name: Checkout release tag - if: ${{ github.event_name == 'workflow_dispatch' }} - shell: bash - env: - RELEASE_TAG: ${{ inputs.tag }} - run: | - set -euo pipefail - if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+[.][0-9]+[.][0-9]+([.-][A-Za-z0-9._-]+)?$ ]]; then - echo "::error::Invalid release tag: $RELEASE_TAG" - exit 2 - fi - git checkout --detach "$RELEASE_TAG" - - - name: Snapshot release - uses: goreleaser/goreleaser-action@v7.2.3 - with: - distribution: goreleaser - version: "~> v2" - args: release --snapshot --clean --skip=publish --config /tmp/.goreleaser.yaml diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9edf3174..676f374e 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -28,10 +28,6 @@ builds: targets: - darwin_amd64 - darwin_arm64 - hooks: - post: - - ./scripts/codesign-macos.sh "{{ .Path }}" - archives: - ids: - gitcrawl diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e675d8..898bd4fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.8.8 - Unreleased +### Maintenance + +- Move official releases to the shared signed, notarized, independently verified GitHub Actions pipeline while preserving archive names, contents, checksums, and Homebrew delivery. + ## 0.8.7 - 2026-07-26 ### Portable stores diff --git a/Makefile b/Makefile index ccd19e13..7061c2da 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ VERSION ?= dev .DEFAULT_GOAL := help -.PHONY: help build generate-sqlc tidy-check fmt lint test test-coverage run clean smoke test-release check snapshot release verify-release release-artifacts +.PHONY: help build generate-sqlc tidy-check fmt lint test test-coverage run clean smoke test-release check snapshot release verify-release release-artifacts release-snapshot help: @printf '%s\n' \ @@ -15,7 +15,7 @@ help: ' lint Run vet, vulnerability, and dead-code checks.' \ ' check Run every local gate enforced by CI.' \ ' snapshot Build credential-free release artifacts.' \ - ' release Build and verify official release artifacts (VERSION=vX.Y.Z).' \ + ' release Refuse local publishing and print the official CI command.' \ ' verify-release Verify existing release artifacts (VERSION=vX.Y.Z).' \ ' generate-sqlc Regenerate sqlc output.' \ ' tidy-check Verify go.mod and go.sum are tidy.' \ @@ -24,7 +24,8 @@ help: ' test-release Test the release scripts.' \ ' run Run the CLI (ARGS=...).' \ ' clean Remove local build output.' \ - ' release-artifacts Alias for release.' + ' release-artifacts Alias for release.' \ + ' release-snapshot Alias for snapshot.' build: mkdir -p bin @@ -70,7 +71,6 @@ clean: smoke: build @set -e; version="$$(./bin/$(BINARY) --version)"; test -n "$$version" @set -e; output="$$(./bin/$(BINARY) metadata --json)"; printf '%s' "$$output" | grep -q '"schema_version"' - @set -e; output="$$(./bin/$(BINARY) status --json)"; printf '%s' "$$output" | grep -q '"databases"' @set -e; output="$$(./bin/$(BINARY) help tui)"; \ printf '%s\n' "$$output"; \ printf '%s' "$$output" | grep -q "gitcrawl tui" @@ -84,12 +84,12 @@ snapshot: GOWORK=off goreleaser release --snapshot --clean --skip=publish release: - @test -n "$(VERSION)" && [ "$(VERSION)" != dev ] || (echo "usage: make release VERSION=vX.Y.Z" >&2; exit 2) - @helper="$${MAC_RELEASE_HELPER:-$$HOME/Projects/agent-scripts/skills/release-mac-app/scripts/mac-release}"; \ - "$$helper" codesign-run -- ./scripts/package-release.sh "$(VERSION)" + @./scripts/package-release.sh verify-release: @test -n "$(VERSION)" && [ "$(VERSION)" != dev ] || (echo "usage: make verify-release VERSION=vX.Y.Z" >&2; exit 2) ./scripts/verify-release.sh "$(VERSION)" release-artifacts: release + +release-snapshot: snapshot diff --git a/docs/releasing.md b/docs/releasing.md index 8e7b5783..70d0731e 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -5,21 +5,31 @@ permalink: /releasing/ # Releasing Gitcrawl -Official releases are assembled locally on an authorized maintainer Mac. GitHub Actions only validates credential-free snapshots and never publishes release artifacts. +`.github/workflows/release-unified.yml` is the only official release path. It calls `openclaw/release-workflows@v1` from protected `main`, requires an existing SSH-signed version tag, preserves Gitcrawl's six thin platform archives and `checksums.txt`, signs and notarizes both macOS binaries as `org.openclaw.gitcrawl`, verifies the complete asset inventory independently on arm64 and Intel macOS, and waits for `openclaw/homebrew-tap` to update successfully. -1. Prepare and sign the release commit and tag on `main`, then ensure the checkout is clean and `HEAD` exactly matches that tag. -2. Configure the shared `release-mac-app` helper at runtime for the passwordless managed keychain. The identity must be `Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)`. Supply `NOTARYTOOL_KEYCHAIN_PROFILE` through the approved private runtime environment; never commit its value or add it to GitHub Actions. Keep keychain and 1Password routing in the ignored `.mac-release.env` or another approved private environment, never in Git. -3. Use the official local release target to build all release archives. Each thin Darwin binary is signed as `org.openclaw.gitcrawl` with the hardened runtime and a trusted timestamp, submitted to Apple in an ephemeral ZIP, and required to pass the notarized code requirement before packaging. Linux and Windows builds remain ordinary cross-compiles: +The public compatibility contract remains: - ```bash - make release VERSION=vX.Y.Z - ``` +- `gitcrawl_VERSION_{darwin,linux}_{amd64,arm64}.tar.gz` +- `gitcrawl_VERSION_windows_{amd64,arm64}.zip` +- `checksums.txt` +- `CHANGELOG.md`, `LICENSE`, `README.md`, and the Gitcrawl executable inside every platform archive +- OpenClaw Foundation Team ID `FWJYW4S8P8` and code identifier `org.openclaw.gitcrawl` - `make release` fails unless `scripts/package-release.sh` completes `scripts/verify-release.sh` against every final Darwin archive. To recheck artifacts that were already built, run `make verify-release VERSION=vX.Y.Z`. The former `make release-artifacts` name remains an alias for compatibility. +The shared pipeline also publishes verifier control assets (`ASSET-INVENTORY.json`, `SIGNING-MANIFEST.json`, and `RELEASE-NOTES.md`). -4. Create a draft GitHub release from the signed tag. Attach the archives and `checksums.txt` from `dist/`, then manually run the `Release Assets` workflow for that tag. Its ephemeral token has `contents: write` only because GitHub otherwise hides drafts; the token is scoped to read-only asset downloads and is removed before verification. Publish only after both macOS verification jobs pass. -5. After publication, verify the release notes and assets, then dispatch the `openclaw/homebrew-tap` formula update for `gitcrawl` and verify the installed binary. +## Release -Local `go build`, `make build`, tests, and `make snapshot` never require release credentials. Official releases set `GITCRAWL_REQUIRE_CODESIGN=1`; the signing hook then requires `NOTARYTOOL_KEYCHAIN_PROFILE`, waits for an accepted notarization response, and replaces the GoReleaser output only after online notarization verification succeeds. `scripts/verify-release.sh` independently checks every extracted Darwin binary for the Foundation designated requirement and the notarized requirement. Raw executables cannot carry a stapled ticket, so verification requires network access to Apple. +Prepare a dated changelog section and land it on protected `main`. The `user.signingkey` SSH key must be listed for your principal in `.github/release-allowed-signers`. Create the annotated signed tag, verify it explicitly against the repository allowlist, push it, and dispatch the workflow: -`scripts/package-release.sh` fails closed unless it runs from the exact trusted signed tag with the Foundation identity supplied by `release-mac-app codesign-run` and a runtime notary profile. +```sh +git -c gpg.format=ssh tag -s vX.Y.Z -m "Release X.Y.Z" +git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=.github/release-allowed-signers tag -v vX.Y.Z +git push origin vX.Y.Z +gh workflow run release-unified.yml --repo openclaw/gitcrawl -f version=X.Y.Z +``` + +The release is complete only when the GitHub Release contains the full asset set, both native macOS verification jobs pass, and the Homebrew handoff is green. + +## Local diagnostics + +Local publishing is disabled. `make release`, `make release-artifacts`, and `scripts/package-release.sh` refuse and print the official workflow command. `make snapshot` remains credential-free, and `make verify-release VERSION=vX.Y.Z` rechecks already downloaded Darwin artifacts in `dist/` against `checksums.txt`, the stable Foundation designated requirement, architecture, embedded version, and Apple's online notarization ticket. diff --git a/scripts/package-release.sh b/scripts/package-release.sh index fe00c50a..24db0fdb 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -1,71 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -VERSION=${1:-} -EXPECTED_AUTHORITY='Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)' - -if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then - echo "usage: $0 vX.Y.Z" >&2 - exit 2 -fi -[[ "$(uname -s)" == Darwin ]] || { - echo "official release packaging must run on macOS" >&2 - exit 1 -} -[[ "$(uname -m)" == arm64 ]] || { - echo "official release packaging requires Apple Silicon with Rosetta for both architecture smoke tests" >&2 - exit 1 -} -[[ "${CODESIGN_IDENTITY:-}" == "$EXPECTED_AUTHORITY" ]] || { - echo "official releases require $EXPECTED_AUTHORITY" >&2 - exit 1 -} -[[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || { - echo "official releases require NOTARYTOOL_KEYCHAIN_PROFILE at runtime" >&2 - exit 1 -} - -for tool in codesign ditto git go goreleaser lipo plutil shasum tar xcrun; do - command -v "$tool" >/dev/null || { - echo "missing required tool: $tool" >&2 - exit 1 - } -done - -head_commit=$(git -C "$ROOT" rev-parse HEAD) -tag_commit=$(git -C "$ROOT" rev-parse "refs/tags/$VERSION^{commit}" 2>/dev/null) || { - echo "release tag does not exist locally: $VERSION" >&2 - exit 1 -} -[[ "$head_commit" == "$tag_commit" ]] || { - echo "HEAD does not match release tag $VERSION" >&2 - exit 1 -} -[[ -z "$(git -C "$ROOT" status --porcelain --untracked-files=normal)" ]] || { - echo "release checkout is not clean" >&2 - exit 1 -} -git -C "$ROOT" tag -v "$VERSION" >/dev/null 2>&1 || { - echo "release tag is not signed by a trusted git signing key: $VERSION" >&2 - exit 1 -} - -release_version=${VERSION#v} -for arch in amd64 arm64; do - archive="$ROOT/dist/gitcrawl_${release_version}_darwin_${arch}.tar.gz" - [[ ! -e "$archive" ]] || { - echo "refusing to overwrite existing artifact: $archive" >&2 - exit 1 - } -done - -( - cd "$ROOT" - CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \ - GITCRAWL_REQUIRE_CODESIGN=1 \ - GOWORK=off \ - goreleaser release --clean --skip=publish -) - -"$ROOT/scripts/verify-release.sh" "$VERSION" +echo "local releases are disabled because this path cannot enforce the shared verifier and publication chain" >&2 +echo "official releases must use: gh workflow run release-unified.yml --repo openclaw/gitcrawl -f version=X.Y.Z" >&2 +exit 1 diff --git a/scripts/test-release.sh b/scripts/test-release.sh index c81874ba..801131b5 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -87,16 +87,11 @@ EOF chmod 0755 "$test_binary" GITCRAWL_REQUIRE_CODESIGN=0 "$ROOT/scripts/codesign-macos.sh" "$test_binary" -if CODESIGN_IDENTITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \ - "$ROOT/scripts/package-release.sh" v0.7.1 >/dev/null 2>&1; then - echo "package script accepted personal signing identity" >&2 - exit 1 -fi -if CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \ - "$ROOT/scripts/package-release.sh" v0.7.1 >/dev/null 2>&1; then - echo "package script accepted a missing notary profile" >&2 +if package_output=$("$ROOT/scripts/package-release.sh" 2>&1); then + echo "local package script unexpectedly succeeded" >&2 exit 1 fi +grep -F 'gh workflow run release-unified.yml --repo openclaw/gitcrawl -f version=X.Y.Z' <<<"$package_output" >/dev/null if GITCRAWL_REQUIRE_CODESIGN=1 \ CODESIGN_IDENTITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \ "$ROOT/scripts/codesign-macos.sh" "$test_binary" >/dev/null 2>&1; then @@ -174,27 +169,16 @@ if "$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS" >/dev/null 2>&1; then exit 1 fi -release_workflow="$ROOT/.github/workflows/release-assets.yml" -grep -F 'contents: write' "$release_workflow" >/dev/null -grep -F "github.ref == format('refs/heads/{0}', github.event.repository.default_branch)" "$release_workflow" >/dev/null -grep -F "endsWith(github.workflow_ref, format('@refs/heads/{0}', github.event.repository.default_branch))" "$release_workflow" >/dev/null -grep -F "github.event_name == 'release' && github.event.action == 'published'" "$release_workflow" >/dev/null -# shellcheck disable=SC2016 # GitHub expression must remain literal. -grep -F 'ref: ${{ github.event.repository.default_branch }}' "$release_workflow" >/dev/null -grep -F 'persist-credentials: false' "$release_workflow" >/dev/null -# shellcheck disable=SC2016 # GitHub expression must remain literal. -[[ "$(grep -F -c 'GH_TOKEN: ${{ github.token }}' "$release_workflow")" == 1 ]] -# shellcheck disable=SC2016 # jq expression must remain literal. -grep -F 'tag_name == $tag and (.draft == ($draft == "true"))' "$release_workflow" >/dev/null -grep -F 'Accept: application/octet-stream' "$release_workflow" >/dev/null -grep -F 'unset GH_TOKEN GITHUB_TOKEN' "$release_workflow" >/dev/null +release_workflow="$ROOT/.github/workflows/release-unified.yml" +grep -F 'uses: openclaw/release-workflows/.github/workflows/release-go-cli.yml@v1' "$release_workflow" >/dev/null +grep -F 'checksum-filename: checksums.txt' "$release_workflow" >/dev/null +grep -F 'archive-files: '\''["CHANGELOG.md","LICENSE","README.md"]'\''' "$release_workflow" >/dev/null +grep -F 'stable-identifier: org.openclaw.gitcrawl' "$release_workflow" >/dev/null +grep -F 'require-signed-tag: true' "$release_workflow" >/dev/null +grep -F 'darwin-universal: disabled' "$release_workflow" >/dev/null if grep -R -F 'NOTARYTOOL_KEYCHAIN_PROFILE' "$ROOT/.github/workflows" >/dev/null; then echo "notary profile must not be configured in GitHub Actions" >&2 exit 1 fi -if grep -F 'gh release download' "$release_workflow" >/dev/null; then - echo "release workflow cannot resolve draft assets through gh release download" >&2 - exit 1 -fi echo "release script tests passed"