diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml deleted file mode 100644 index 89d4dfe..0000000 --- a/.github/workflows/release-assets.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Release Assets - -on: - release: - types: [published] - workflow_dispatch: - inputs: - tag: - description: "Release tag to verify" - required: true - type: string - draft: - description: "Verify an unpublished draft release" - required: true - default: true - type: boolean - -permissions: - # GitHub hides draft releases from read-only workflow tokens. - contents: write - -jobs: - verify-macos-crawlctl: - if: >- - github.event_name == 'release' || - (endsWith(github.workflow_ref, format('@refs/heads/{0}', github.event.repository.default_branch)) && - github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - strategy: - fail-fast: false - matrix: - include: - - runs-on: macos-15 - arch: arm64 - - runs-on: macos-15-intel - arch: x86_64 - runs-on: ${{ matrix.runs-on }} - env: - RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.event_name == 'release' && github.event.repository.default_branch || github.workflow_sha }} - path: trusted - persist-credentials: false - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: trusted/go.mod - cache: false - - - name: Verify release provenance - shell: bash - run: | - set -euo pipefail - release_commit=$(trusted/scripts/verify-crawlctl-release-provenance.sh "$RELEASE_TAG") - echo "RELEASE_COMMIT=$release_commit" >> "$GITHUB_ENV" - - - name: Download crawlctl assets - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - expected_draft=false - if [[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then - expected_draft="${{ inputs.draft }}" - fi - trusted/scripts/download-crawlctl-release-assets.sh \ - "$RELEASE_TAG" "${{ matrix.arch }}" "$expected_draft" artifacts - - - name: Verify Developer ID signatures - shell: bash - run: | - set -euo pipefail - archive="artifacts/crawlctl-${RELEASE_TAG}-macos-${{ matrix.arch }}.tar.gz" - expected=("$archive" "$archive.sha256") - for path in "${expected[@]}"; do - test -f "$path" - done - trusted/scripts/verify-crawlctl-release.sh \ - "$RELEASE_TAG" "$RELEASE_COMMIT" "$archive" diff --git a/.github/workflows/release-unified.yml b/.github/workflows/release-unified.yml new file mode 100644 index 0000000..0914b91 --- /dev/null +++ b/.github/workflows/release-unified.yml @@ -0,0 +1,36 @@ +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 + checksum-filename: checksums.txt + nfpm: disabled + stable-identifier: org.openclaw.crawlctl + 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 }} diff --git a/.gitignore b/.gitignore index 2fb06c7..11d05b0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,4 @@ coverage.out *.db-wal .DS_Store .crabbox/ -.mac-release.env /dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..951ec60 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,32 @@ +version: 2 + +project_name: crawlkit + +changelog: + disable: true + +builds: + - id: crawlctl + main: ./cmd/crawlctl + binary: crawlctl + flags: + - -trimpath + env: + - CGO_ENABLED=0 + ldflags: + - -s -w -X main.version={{ .Version }} + targets: + - darwin_amd64 + - darwin_arm64 + - linux_amd64 + - linux_arm64 + +archives: + - ids: + - crawlctl + formats: + - tar.gz + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + +checksum: + name_template: checksums.txt diff --git a/.mac-release.env.example b/.mac-release.env.example deleted file mode 100644 index d2f8a52..0000000 --- a/.mac-release.env.example +++ /dev/null @@ -1,3 +0,0 @@ -MAC_RELEASE_CODESIGN_IDENTITY='Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)' -MAC_RELEASE_CODESIGN_KEYCHAIN_MANAGED=1 -MAC_RELEASE_CODESIGN_PASSWORDLESS=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index e779032..f742a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,16 @@ ## v0.14.4 - Unreleased +### Breaking release asset rename + +- Replace `crawlctl-v0.14.3-macos-arm64.tar.gz` with `crawlkit_0.14.4_darwin_arm64.tar.gz` and `crawlctl-v0.14.3-macos-x86_64.tar.gz` with `crawlkit_0.14.4_darwin_amd64.tar.gz`. The new fleet-standard names use the repository prefix, omit the `v` inside filenames, use `darwin` and `amd64`, and replace per-asset `.sha256` sidecars with one `checksums.txt`; scripts that download the old names must be updated. +- Add fleet-standard `crawlkit_0.14.4_linux_arm64.tar.gz` and `crawlkit_0.14.4_linux_amd64.tar.gz` archives containing the `crawlctl` executable. + +### Maintenance + - Update Go dependencies, including `go-runewidth` v0.0.27 and `x/crypto` v0.54.0, and refresh Actions Checkout to v7.0.1 and CodeQL Action to v4.37.3. - Standardize local build, check, snapshot, and fail-closed release commands under the shared crawler Makefile contract. - -## Unreleased +- Move official publication to the shared signed, notarized, independently verified GitHub Actions pipeline while preserving the `org.openclaw.crawlctl` code-signing identifier. ## v0.14.3 - 2026-07-17 diff --git a/Makefile b/Makefile index 91688c5..f9073bb 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ help: ' lint Run vet, dead-code, and vulnerability checks.' \ ' check Run every local gate enforced by CI.' \ ' snapshot Build a credential-free local snapshot.' \ - ' release Build and verify official release artifacts (VERSION=vX.Y.Z).' \ - ' verify-release Verify existing release artifacts (VERSION=vX.Y.Z).' \ + ' release Refuse local publishing and print the unified workflow command.' \ + ' verify-release Verify downloaded fleet release artifacts (VERSION=X.Y.Z).' \ ' test-race Run the Go test suite with the race detector.' \ ' test-release Test the release scripts.' \ ' vet Run go vet (compatibility target).' \ @@ -64,16 +64,19 @@ check: tidy-check fmt lint test test-race test-release snapshot: build release: - @test -n "$(VERSION)" || (echo "usage: make release VERSION=vX.Y.Z" >&2; exit 2) @./scripts/package-crawlctl-release.sh "$(VERSION)" verify-release: - @test -n "$(VERSION)" || (echo "usage: make verify-release VERSION=vX.Y.Z" >&2; exit 2) + @test -n "$(VERSION)" || (echo "usage: make verify-release VERSION=X.Y.Z" >&2; exit 2) @set -e; \ - release_commit="$$(./scripts/verify-crawlctl-release-provenance.sh "$(VERSION)")"; \ - ./scripts/verify-crawlctl-release.sh "$(VERSION)" "$$release_commit" \ - "dist/crawlctl-$(VERSION)-macos-arm64.tar.gz" \ - "dist/crawlctl-$(VERSION)-macos-x86_64.tar.gz" + version="$(VERSION)"; \ + version="$${version#v}"; \ + release_commit="$$(./scripts/verify-crawlctl-release-provenance.sh "v$$version")"; \ + ./scripts/verify-crawlctl-release.sh "$$version" "$$release_commit" \ + "dist/crawlkit_$${version}_darwin_arm64.tar.gz" \ + "dist/crawlkit_$${version}_darwin_amd64.tar.gz" \ + "dist/crawlkit_$${version}_linux_arm64.tar.gz" \ + "dist/crawlkit_$${version}_linux_amd64.tar.gz" release-artifacts: release diff --git a/README.md b/README.md index e9d1a0b..c3c820b 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ safe desktop-cache snapshot utilities. go get github.com/openclaw/crawlkit@latest ``` -Beginning with the next release after v0.13.3, macOS users should install or -update `crawlctl` from the Developer ID-signed release artifact: +macOS users should install or update `crawlctl` from the Developer ID-signed +release artifact: ```bash curl -fsSLO https://raw.githubusercontent.com/openclaw/crawlkit/main/scripts/install-crawlctl.sh @@ -33,7 +33,21 @@ requirement across in-place updates; the separate clean-VM release gate must prove that a later same-path build does not trigger a second protected-data alert. This contract does not suppress the first-install consent. -Starting with v0.14.0, release binaries also require an Apple online +Starting with v0.14.4, release assets use the fleet naming convention: +`crawlkit__darwin_arm64.tar.gz` and +`crawlkit__darwin_amd64.tar.gz`, with one `checksums.txt` file. This is +a deliberate breaking rename from the v0.14.3 +`crawlctl-v0.14.3-macos-.tar.gz` archives and their individual `.sha256` +sidecars. The version in the new filename has no `v` prefix, and Intel uses +`amd64` instead of `x86_64`. The installer understands the new names. + +The same release also provides `crawlkit__linux_arm64.tar.gz` and +`crawlkit__linux_amd64.tar.gz`. Each archive contains the `crawlctl` +executable; verify it against `checksums.txt` before installation. The bundled +installer remains macOS-specific because it also enforces the Developer ID +designated requirement and online notarization ticket. + +Starting with v0.14.0, macOS release binaries also require an Apple online notarization ticket before packaging and installation. The raw executable and ephemeral ZIP submitted to Apple do not support ticket stapling, so verification requires network access. Release verification also binds each thin binary's Go @@ -50,9 +64,9 @@ development, but produces a locally compiled, ad-hoc-signed macOS executable. Replacing that executable changes its designated requirement and invalidates Full Disk Access grants. -Go packages are published by tagging this repository. GitHub releases also -carry signed macOS `crawlctl` artifacts. See `docs/publishing.md` for the -release commands. +Go packages are published by tagging this repository. GitHub releases carry +signed macOS and static Linux `crawlctl` artifacts. See `docs/publishing.md` +for the release commands and exact asset contract. See `docs/boundary.md` for the crawlkit-versus-app ownership boundary and `docs/remote-contract.md` for the Worker/client split. diff --git a/docs/publishing.md b/docs/publishing.md index 67d0a0d..a0ade3f 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,156 +1,87 @@ # Publishing Crawlkit -Go modules are published from git tags. GitHub releases additionally carry -Developer ID-signed `crawlctl` binaries for macOS. Users must install those -artifacts instead of rebuilding the production executable so macOS can preserve -its TCC/Full Disk Access identity across updates. +Go modules are published from signed git tags. GitHub releases are built by the +fleet `release-go-cli` workflow and contain Developer ID-signed, notarized +`crawlctl` binaries for macOS plus static Linux binaries. + +## Release assets + +Starting with v0.14.4, crawlkit uses the fleet archive convention. The archive +prefix is the repository name because that is the shared pipeline default; the +executable inside every archive remains `crawlctl`. + +| Platform | Asset | +| --- | --- | +| macOS Apple Silicon | `crawlkit__darwin_arm64.tar.gz` | +| macOS Intel | `crawlkit__darwin_amd64.tar.gz` | +| Linux arm64 | `crawlkit__linux_arm64.tar.gz` | +| Linux amd64 | `crawlkit__linux_amd64.tar.gz` | +| All platforms | `checksums.txt` | + +This deliberately replaces the v0.14.3 names. In particular, +`crawlctl-v0.14.3-macos-arm64.tar.gz` became +`crawlkit_0.14.4_darwin_arm64.tar.gz`, and +`crawlctl-v0.14.3-macos-x86_64.tar.gz` became +`crawlkit_0.14.4_darwin_amd64.tar.gz`. The version inside an asset name no +longer has a `v` prefix, `macos` is now `darwin`, `x86_64` is now `amd64`, and +the four per-asset `.sha256` sidecars are replaced by one `checksums.txt` file. +Linux arm64 and amd64 archives are new in v0.14.4. + +The fixed macOS code identifier remains `org.openclaw.crawlctl`, and the +required signing identity remains `Developer ID Application: OpenClaw +Foundation (FWJYW4S8P8)`. The workflow signs each native Mach-O with the +hardened runtime, submits it for notarization, and independently verifies both +architectures before publication. There is no universal macOS archive, nFPM +package, or Homebrew handoff. ## Release checklist -1. Rebase `crawlkit` and every downstream app branch on each repo's `origin/main`. -2. Run the crawlkit gate: - -```bash -make check -``` - -3. Update docs and changelogs in `crawlkit` plus every downstream app branch - that consumes the release. -4. Test downstream apps against the local checkout through a temporary Go workspace. -5. Merge `crawlkit` to `main`. -6. Pull the final protected `main` commit into a clean checkout. - -```bash -git switch main -git pull --ff-only -git status --short --branch -``` - -7. Run a complete notarization preflight from that exact release candidate with - the approved private signing environment. This does not - inspect or create a tag. It produces disposable candidates, restores the - managed keychain, then runs the credential-free online-ticket and build - provenance verifier: - -```bash -preflight_dir=$(mktemp -d /private/tmp/crawlkit-v0.14.3-preflight.XXXXXX) -scripts/preflight-crawlctl-release.sh v0.14.3 "$preflight_dir" -``` - -8. Tag the next semver release from `main`: - -```bash -git tag -s v0.14.3 -git push origin main -git push origin v0.14.3 -``` - -9. From the clean checkout whose `HEAD` exactly matches the signed release tag, -build the signed macOS artifacts. The package entrypoint fetches the remote tag -and protected `main` head into temporary refs, verifies the annotated tag against -the repository-pinned SSH signer principal and key, requires the signed tag -object's embedded name to equal the requested version, and requires the tag, -remote branch, and local checkout to resolve to one commit. A signed side-commit -or renamed tag is not releasable. It then uses the shared secret-safe release -keychain helper and fails closed if any provenance or signing check differs: - -```bash -make release VERSION=v0.14.3 -``` - -`make release` does not return successfully until the package entrypoint has -verified both final archives, including their Developer ID signatures and -online notarization tickets. Use `make verify-release VERSION=v0.14.3` to -recheck artifacts that were already built. The former `make release-artifacts` -name remains an alias for compatibility. For an unsigned, credential-free -development build, use `make snapshot`. - -The fixed code identifier is `org.openclaw.crawlctl`. The required signing -identity is `Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)`; the -release scripts reject every other certificate or Team ID. Never rebuild, -strip, patch, or re-sign an artifact after this step. The tracked -`.mac-release.env.example` contains only the public signing identity and -managed, passwordless-keychain policy. Keep `.mac-release.env` ignored and -inject the runtime signing-keychain routing and -`NOTARYTOOL_KEYCHAIN_PROFILE` through the approved private environment; never -commit their values or locators. Ordinary `go build` and `go install` remain -unsigned/local workflows and do not require signing or notarization -credentials. - -For each architecture, the managed helper builds one thin executable with Go -VCS metadata, signs it with the hardened runtime, creates an ephemeral ZIP, and -runs `xcrun notarytool submit` with `--no-s3-acceleration --wait`. It requires -an accepted response, the exact embedded designated requirement, and an online -notarization-ticket lookup before creating the existing tarball and checksum. -The helper finishes and restores the signing keychain state before the package -entrypoint invokes either candidate. The credential-free verifier uses -`go version -m` to require `vcs.revision` equal to the verified remote release -commit, `vcs.modified=false`, and the exact -`github.com/openclaw/crawlkit/cmd/crawlctl` main-package path; its version probe -runs with a minimal environment so caller tokens are absent. Final artifacts -move through a destination-side staging directory, and any promotion failure -rolls back the entire four-file set so the command is safely rerunnable. - -The ZIP is only a submission carrier and is never a release asset. Raw Mach-O -executables and ZIP archives cannot carry a stapled ticket, so both release -verification and installation need network access to retrieve the ticket from -Apple. Do not use -`spctl --assess --type execute` as a raw-CLI gate: even valid Apple-notarized -standalone executables can exit with “the code is valid but does not seem to be -an app.” - -10. Create the GitHub release as a draft, attach both archives and their -`.sha256` files, then run the `Release Assets` workflow manually from the -default branch with the tag to verify the uploaded draft assets. Publish only -after that check succeeds. A crawlkit release without both verified signed -macOS assets is incomplete. The workflow runs again on publication and uses its -own trusted revision to reject missing, wrongly-signed, checksum-invalid, -ticketless, universal, dirty, wrong-revision, or side-commit artifacts. At both -draft and publication gates it refetches the remote tag and protected branch, -rechecks the pinned signer and exact commit equality, then binds every binary's -Go build info to that commit. This protected-main verifier uses no notarization -credentials; it performs only public provenance, online-ticket, and -signature-policy checks. - -Before publishing, run two independent clean-VM checks and record their results -separately: - -- Gatekeeper: acquire the draft artifact through a path that naturally applies - and preserves quarantine, then execute it on the clean VM and prove there is - no security alert. A `curl`/`scp`/`tar` transfer and raw-CLI `spctl` assessment - are supporting evidence only and must not be called a fresh-download - Gatekeeper pass. -- Full Disk Access: grant the expected first protected-data consent once to the - official stable-path binary, update that same path to a distinct later build - with the identical designated requirement, and prove the same protected-data - action succeeds without a second alert. Do not claim first-install prompt - suppression. - -11. Prime and verify module proxy visibility: - -```bash -GOPROXY=https://proxy.golang.org GONOSUMDB= go list -m github.com/openclaw/crawlkit@v0.14.3 -GOPROXY=https://proxy.golang.org go list -m github.com/openclaw/crawlkit@v0.14.3 -``` - -12. Bump downstream apps to the new tag and commit their `go.mod`/`go.sum` updates: - -```bash -go get github.com/openclaw/crawlkit@v0.14.3 -GOWORK=off go mod tidy -``` - -`pkg.go.dev` indexes public modules automatically after the tag is reachable. - -Use a patch tag only for narrow bug fixes on the existing API. Use a minor tag -for broad crawler infrastructure changes. The module-path move needs a new tag -on `openclaw/crawlkit` before downstream apps can drop local `replace` lines. - -## Versioning - -Keep `v0.x.y` while the downstream crawler rewires are still settling. If the -module ever reaches `v2`, Go requires the module path to become: - -```text -github.com/openclaw/crawlkit/v2 -``` +1. Pull the protected default branch and confirm the checkout is clean. +2. Add the release notes to the versioned Unreleased section and run: + + ```bash + make check + actionlint + ``` + +3. Merge the release-preparation PR, then date its changelog heading. +4. Create an SSH-signed annotated tag from the exact protected `main` commit + and push it. The signer must match `.github/release-allowed-signers`. +5. Dispatch the unified workflow from the current protected `main` head: + + ```bash + gh workflow run release-unified.yml --repo openclaw/crawlkit -f version=X.Y.Z + ``` + + Local release publishing is intentionally disabled. `make release`, the + compatibility alias `make release-artifacts`, and + `scripts/package-crawlctl-release.sh` only print the workflow command and + fail. + +6. Watch the workflow through publication. It freezes the signed tag, builds + all four native archives, signs and notarizes the two macOS binaries, + verifies the immutable draft independently on Apple Silicon and Intel, and + publishes only the byte-for-byte verified assets. +7. Download every asset and `checksums.txt`, validate every checksum, inspect + both Linux binaries, and verify a downloaded macOS binary with: + + ```bash + codesign --verify --strict --check-notarization -R=notarized ./crawlctl + ``` + + `make verify-release VERSION=X.Y.Z` performs the repository's local + diagnostics when the five release assets have already been downloaded into + `dist/`. +8. Prime and verify module proxy visibility: + + ```bash + GOPROXY=https://proxy.golang.org GONOSUMDB= go list -m github.com/openclaw/crawlkit@vX.Y.Z + GOPROXY=https://proxy.golang.org go list -m github.com/openclaw/crawlkit@vX.Y.Z + ``` + +9. Merge the workflow's closeout PR, or otherwise add the next Unreleased + changelog section. + +Use a patch tag only for narrow fixes on the existing API. Use a minor tag for +broad crawler infrastructure changes. If the module reaches v2, Go requires +the module path to become `github.com/openclaw/crawlkit/v2`. diff --git a/scripts/download-crawlctl-release-assets.sh b/scripts/download-crawlctl-release-assets.sh deleted file mode 100755 index e342b60..0000000 --- a/scripts/download-crawlctl-release-assets.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TAG=${1:-} -ARCH=${2:-} -EXPECTED_DRAFT=${3:-} -OUT_DIR=${4:-} -REPOSITORY=${GITHUB_REPOSITORY:-} - -usage() { - echo "usage: $0 vX.Y.Z arm64|x86_64 true|false output-directory" >&2 - exit 2 -} - -[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || usage -[[ "$ARCH" == arm64 || "$ARCH" == x86_64 ]] || usage -[[ "$EXPECTED_DRAFT" == true || "$EXPECTED_DRAFT" == false ]] || usage -[[ -n "$OUT_DIR" && "$REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || usage -[[ -n "${GH_TOKEN:-}" ]] || { - echo "GH_TOKEN is required" >&2 - exit 1 -} - -for tool in gh jq; do - command -v "$tool" >/dev/null || { - echo "missing required tool: $tool" >&2 - exit 1 - } -done - -WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/crawlctl-release-download.XXXXXX") -trap 'rm -rf "$WORK_DIR"' EXIT - -gh api --paginate "repos/$REPOSITORY/releases?per_page=100" > "$WORK_DIR/release-pages.json" -release=$( - jq -cs --arg tag "$TAG" --argjson draft "$EXPECTED_DRAFT" \ - '[.[][] | select(.tag_name == $tag and .draft == $draft)]' \ - "$WORK_DIR/release-pages.json" -) -[[ "$(jq 'length' <<<"$release")" == 1 ]] || { - echo "expected exactly one release for $TAG with draft=$EXPECTED_DRAFT" >&2 - exit 1 -} -release_id=$(jq -r '.[0].id' <<<"$release") -[[ "$release_id" =~ ^[0-9]+$ ]] || { - echo "release has an invalid API id" >&2 - exit 1 -} - -gh api --paginate "repos/$REPOSITORY/releases/$release_id/assets?per_page=100" > "$WORK_DIR/asset-pages.json" -assets=$(jq -cs '[.[][]]' "$WORK_DIR/asset-pages.json") -expected_names=( - "crawlctl-${TAG}-macos-arm64.tar.gz" - "crawlctl-${TAG}-macos-arm64.tar.gz.sha256" - "crawlctl-${TAG}-macos-x86_64.tar.gz" - "crawlctl-${TAG}-macos-x86_64.tar.gz.sha256" -) -[[ "$(jq 'length' <<<"$assets")" == "${#expected_names[@]}" ]] || { - echo "release must contain exactly four crawlctl assets" >&2 - exit 1 -} - -mkdir -p "$OUT_DIR" -api_prefix="https://api.github.com/repos/$REPOSITORY/releases/assets/" -for name in "${expected_names[@]}"; do - matches=$(jq -c --arg name "$name" '[.[] | select(.name == $name)]' <<<"$assets") - [[ "$(jq 'length' <<<"$matches")" == 1 ]] || { - echo "release asset missing or duplicated: $name" >&2 - exit 1 - } - [[ "$name" == *"-macos-${ARCH}.tar.gz"* ]] || continue - api_url=$(jq -r '.[0].url' <<<"$matches") - asset_id=${api_url#"$api_prefix"} - [[ "$api_url" == "$api_prefix"* && "$asset_id" =~ ^[0-9]+$ ]] || { - echo "release asset has an invalid API URL: $name" >&2 - exit 1 - } - gh api "$api_url" -H "Accept: application/octet-stream" > "$WORK_DIR/$name" - [[ -s "$WORK_DIR/$name" ]] || { - echo "downloaded release asset is empty: $name" >&2 - exit 1 - } - mv "$WORK_DIR/$name" "$OUT_DIR/$name" -done diff --git a/scripts/install-crawlctl.sh b/scripts/install-crawlctl.sh index feca232..7b021ba 100755 --- a/scripts/install-crawlctl.sh +++ b/scripts/install-crawlctl.sh @@ -37,16 +37,14 @@ verify_thin_architecture() { } verify_checksum() { - local archive_path=$1 checksum_path=$2 expected_hash expected_name extra actual_hash - [[ "$(wc -l < "$checksum_path" | tr -d ' ')" == 1 ]] || { - echo "invalid checksum file: $checksum_path" >&2 - return 1 - } - read -r expected_hash expected_name extra < "$checksum_path" - [[ "$expected_hash" =~ ^[[:xdigit:]]{64}$ && "$expected_name" == "$(basename "$archive_path")" && -z "${extra:-}" ]] || { - echo "invalid checksum record: $checksum_path" >&2 + local archive_path=$1 checksum_path=$2 expected_name matches expected_hash actual_hash + expected_name=$(basename "$archive_path") + matches=$(awk -v name="$expected_name" '$2 == name || $2 == "*" name { print $1 }' "$checksum_path") + [[ -n "$matches" && "$matches" != *$'\n'* && "$matches" =~ ^[[:xdigit:]]{64}$ ]] || { + echo "checksums file must contain exactly one valid record for $expected_name" >&2 return 1 } + expected_hash=$matches actual_hash=$(shasum -a 256 "$archive_path" | awk '{print $1}') [[ "$actual_hash" == "$expected_hash" ]] || { echo "checksum mismatch: $archive_path" >&2 @@ -64,14 +62,14 @@ esac case "$(uname -m)" in arm64) arch=arm64 ;; - x86_64) arch=x86_64 ;; + x86_64) arch=amd64 ;; *) echo "unsupported macOS architecture: $(uname -m)" >&2 exit 1 ;; esac -for tool in codesign csreq curl env lipo shasum tar; do +for tool in awk codesign csreq curl env lipo shasum tar; do command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2 exit 1 @@ -104,7 +102,8 @@ fi exit 2 } -asset="crawlctl-${VERSION}-macos-${arch}.tar.gz" +release_version=${VERSION#v} +asset="crawlkit_${release_version}_darwin_${arch}.tar.gz" base_url=${CRAWLCTL_DOWNLOAD_BASE_URL:-"https://github.com/$REPOSITORY/releases/download/$VERSION"} WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/crawlctl-install.XXXXXX") temp_binary= @@ -115,15 +114,16 @@ cleanup() { trap cleanup EXIT curl -fsSL "$base_url/$asset" -o "$WORK_DIR/$asset" -curl -fsSL "$base_url/$asset.sha256" -o "$WORK_DIR/$asset.sha256" -verify_checksum "$WORK_DIR/$asset" "$WORK_DIR/$asset.sha256" -[[ "$(tar -tzf "$WORK_DIR/$asset")" == crawlctl ]] || { +curl -fsSL "$base_url/checksums.txt" -o "$WORK_DIR/checksums.txt" +verify_checksum "$WORK_DIR/$asset" "$WORK_DIR/checksums.txt" +members=$(tar -tzf "$WORK_DIR/$asset" | sed 's#^\./##; /^$/d') +[[ "$members" == crawlctl ]] || { echo "release archive must contain only crawlctl" >&2 exit 1 } binary="$WORK_DIR/crawlctl" -tar -xOf "$WORK_DIR/$asset" crawlctl > "$binary" +tar -xzf "$WORK_DIR/$asset" -C "$WORK_DIR" chmod 0755 "$binary" codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$binary" codesign --verify --strict --check-notarization -R=notarized --verbose=2 "$binary" @@ -133,7 +133,9 @@ grep -Fx "TeamIdentifier=$EXPECTED_TEAM_ID" <<<"$signature" >/dev/null grep -Fx "Authority=$EXPECTED_AUTHORITY" <<<"$signature" >/dev/null grep -F '(runtime)' <<<"$signature" >/dev/null verify_designated_requirement "$binary" -verify_thin_architecture "$binary" "$arch" +expected_macho_arch=$arch +[[ "$expected_macho_arch" == amd64 ]] && expected_macho_arch=x86_64 +verify_thin_architecture "$binary" "$expected_macho_arch" [[ "$(env -i PATH=/usr/bin:/bin "$binary" --version)" == "${VERSION#v}" ]] mkdir -p "$INSTALL_DIR" diff --git a/scripts/package-crawlctl-release.sh b/scripts/package-crawlctl-release.sh index 505374e..f07c9ba 100755 --- a/scripts/package-crawlctl-release.sh +++ b/scripts/package-crawlctl-release.sh @@ -1,262 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -SCRIPT_PATH="$ROOT/scripts/package-crawlctl-release.sh" -MODE=package -if [[ "${1:-}" == --produce ]]; then - MODE=produce - shift -fi -VERSION=${1:-} -OUT_DIR=${2:-"$ROOT/dist"} -IDENTIFIER=org.openclaw.crawlctl -EXPECTED_AUTHORITY='Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)' -EXPECTED_TEAM_ID=FWJYW4S8P8 -REQUIREMENT="identifier \"$IDENTIFIER\" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = \"$EXPECTED_TEAM_ID\"" +version=${1:-X.Y.Z} +version=${version#v} -verify_designated_requirement() { - local binary=$1 requirement_output actual_requirement actual_canonical - requirement_output=$(codesign -d -r- "$binary" 2>&1) - actual_requirement=$(sed -n 's/^designated => //p' <<<"$requirement_output") - [[ -n "$actual_requirement" && "$actual_requirement" != *$'\n'* ]] || { - echo "crawlctl must contain exactly one designated requirement: $binary" >&2 - return 1 - } - actual_canonical=$(csreq -r "=$actual_requirement" -t) - [[ "$actual_canonical" == "$EXPECTED_REQUIREMENT_CANONICAL" ]] || { - echo "crawlctl embedded designated requirement does not match release policy: $binary" >&2 - return 1 - } -} - -verify_thin_architecture() { - local binary=$1 expected_arch=$2 architecture_output - local -a slices - architecture_output=$(lipo -archs "$binary") - architecture_output=${architecture_output//$'\n'/ } - read -r -a slices <<<"$architecture_output" - if [[ "${#slices[@]}" -ne 1 || "${slices[0]}" != "$expected_arch" ]]; then - echo "crawlctl must contain exactly one $expected_arch architecture slice: $binary" >&2 - return 1 - fi -} - -usage() { - echo "usage: $0 vX.Y.Z [output-directory]" >&2 - exit 2 -} - -[[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || usage -[[ "$(uname -s)" == Darwin ]] || { - echo "crawlctl macOS release packaging must run on macOS" >&2 - exit 1 -} -[[ "$(uname -m)" == arm64 ]] || { - echo "crawlctl release packaging requires Apple Silicon with Rosetta" >&2 - exit 1 -} - -if [[ "$MODE" == produce ]]; then - EXPECTED_COMMIT=${CRAWLCTL_EXPECTED_COMMIT:-} - [[ "$EXPECTED_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { - echo "CRAWLCTL_EXPECTED_COMMIT must be the verified remote release commit" >&2 - exit 1 - } - [[ -n "${CODESIGN_IDENTITY:-}" ]] || { - echo "CODESIGN_IDENTITY is required; run the package entrypoint normally" >&2 - exit 1 - } - [[ "$CODESIGN_IDENTITY" == "$EXPECTED_AUTHORITY" ]] || { - echo "crawlctl releases require $EXPECTED_AUTHORITY" >&2 - exit 1 - } - [[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || { - echo "NOTARYTOOL_KEYCHAIN_PROFILE is required for crawlctl release notarization" >&2 - exit 1 - } - for tool in codesign csreq ditto git go jq lipo shasum tar xcrun; do - command -v "$tool" >/dev/null || { - echo "missing required tool: $tool" >&2 - exit 1 - } - done - EXPECTED_REQUIREMENT_CANONICAL=$(csreq -r "=$REQUIREMENT" -t) - [[ "$(git -C "$ROOT" rev-parse HEAD)" == "$EXPECTED_COMMIT" ]] || { - echo "local HEAD changed after remote release provenance verification" >&2 - exit 1 - } - [[ -z "$(git -C "$ROOT" status --porcelain --untracked-files=normal)" ]] || { - echo "release checkout changed after remote release provenance verification" >&2 - exit 1 - } - - WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/crawlctl-release-produce.XXXXXX") - trap 'rm -rf "$WORK_DIR"' EXIT - STAGED_OUTPUT="$WORK_DIR/output" - mkdir -p "$STAGED_OUTPUT" - staged_artifacts=() - - for arch in arm64 amd64; do - asset_arch=$arch - [[ "$arch" == amd64 ]] && asset_arch=x86_64 - asset="crawlctl-${VERSION}-macos-${asset_arch}.tar.gz" - archive="$STAGED_OUTPUT/$asset" - stage="$WORK_DIR/$asset_arch" - binary="$stage/crawlctl" - notary_archive="$stage/crawlctl-${asset_arch}-notary.zip" - notary_result="$stage/notary-result.json" - - mkdir -p "$stage" - ( - cd "$ROOT" - CGO_ENABLED=0 GOOS=darwin GOARCH="$arch" GOWORK=off \ - go build -buildvcs=true -trimpath \ - -ldflags "-s -w -X main.version=${VERSION#v}" \ - -o "$binary" ./cmd/crawlctl - ) - - codesign --force --options runtime --timestamp \ - --identifier "$IDENTIFIER" --sign "$CODESIGN_IDENTITY" "$binary" - codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$binary" - signature=$(codesign -dvvv "$binary" 2>&1) - grep -Fx "Identifier=$IDENTIFIER" <<<"$signature" >/dev/null - grep -Fx "TeamIdentifier=$EXPECTED_TEAM_ID" <<<"$signature" >/dev/null - grep -Fx "Authority=$EXPECTED_AUTHORITY" <<<"$signature" >/dev/null - grep -F '(runtime)' <<<"$signature" >/dev/null - verify_designated_requirement "$binary" - verify_thin_architecture "$binary" "$asset_arch" - - ditto -c -k --keepParent "$binary" "$notary_archive" - if ! xcrun notarytool submit "$notary_archive" \ - --keychain-profile "$NOTARYTOOL_KEYCHAIN_PROFILE" \ - --no-s3-acceleration --wait --output-format json >"$notary_result"; then - echo "crawlctl notarization submission failed for $asset_arch" >&2 - exit 1 - fi - jq -e '.status == "Accepted"' "$notary_result" >/dev/null || { - echo "crawlctl notarization was not accepted for $asset_arch" >&2 - exit 1 - } - codesign --verify --strict --check-notarization -R=notarized --verbose=2 "$binary" - - tar -czf "$archive" -C "$stage" crawlctl - ( - cd "$STAGED_OUTPUT" - shasum -a 256 "$asset" >"$asset.sha256" - ) - staged_artifacts+=("$asset" "$asset.sha256") - done - - for artifact in "${staged_artifacts[@]}"; do - [[ ! -e "$OUT_DIR/$artifact" ]] || { - echo "refusing to overwrite staged artifact: $OUT_DIR/$artifact" >&2 - exit 1 - } - done - mkdir -p "$OUT_DIR" - for artifact in "${staged_artifacts[@]}"; do - mv "$STAGED_OUTPUT/$artifact" "$OUT_DIR/$artifact" - done - exit 0 -fi - -for tool in git mv; do - command -v "$tool" >/dev/null || { - echo "missing required tool: $tool" >&2 - exit 1 - } -done -[[ -z "$(git -C "$ROOT" status --porcelain --untracked-files=normal)" ]] || { - echo "release checkout is not clean" >&2 - exit 1 -} -EXPECTED_COMMIT=$("$ROOT/scripts/verify-crawlctl-release-provenance.sh" "$VERSION") - -OUT_PARENT=$(dirname "$OUT_DIR") -[[ -d "$OUT_PARENT" ]] || { - echo "output parent directory does not exist: $OUT_PARENT" >&2 - exit 1 -} -OUT_PARENT=$(cd "$OUT_PARENT" && pwd) -OUT_DIR="$OUT_PARENT/$(basename "$OUT_DIR")" -artifacts=( - "crawlctl-${VERSION}-macos-arm64.tar.gz" - "crawlctl-${VERSION}-macos-arm64.tar.gz.sha256" - "crawlctl-${VERSION}-macos-x86_64.tar.gz" - "crawlctl-${VERSION}-macos-x86_64.tar.gz.sha256" -) -for artifact in "${artifacts[@]}"; do - [[ ! -e "$OUT_DIR/$artifact" ]] || { - echo "refusing to overwrite existing artifact: $OUT_DIR/$artifact" >&2 - exit 1 - } -done - -WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/crawlctl-release.XXXXXX") -SIGNED_OUTPUT="$WORK_DIR/signed" -DESTINATION_STAGE= -PROMOTION_ACTIVE=0 -OUT_DIR_CREATED=0 -promoted_artifacts=() -cleanup() { - local rc=$? - set +e - if [[ "$PROMOTION_ACTIVE" == 1 ]]; then - for artifact in "${promoted_artifacts[@]}"; do - rm -f "$OUT_DIR/$artifact" - done - fi - [[ -z "$DESTINATION_STAGE" ]] || rm -rf "$DESTINATION_STAGE" - if [[ "$rc" -ne 0 && "$OUT_DIR_CREATED" == 1 ]]; then - rmdir "$OUT_DIR" >/dev/null 2>&1 || true - fi - rm -rf "$WORK_DIR" - trap - EXIT - exit "$rc" -} -trap cleanup EXIT - -helper=${MAC_RELEASE_HELPER:-"$HOME/Projects/agent-scripts/skills/release-mac-app/scripts/mac-release"} -[[ -x "$helper" ]] || { - echo "mac-release helper is not executable: $helper" >&2 - exit 1 -} -CRAWLCTL_EXPECTED_COMMIT="$EXPECTED_COMMIT" \ - "$helper" codesign-run --with-package-secrets -- \ - "$SCRIPT_PATH" --produce "$VERSION" "$SIGNED_OUTPUT" - -REVALIDATED_COMMIT=$("$ROOT/scripts/verify-crawlctl-release-provenance.sh" "$VERSION") -[[ "$REVALIDATED_COMMIT" == "$EXPECTED_COMMIT" ]] || { - echo "remote release provenance changed during artifact production" >&2 - exit 1 -} - -"$ROOT/scripts/verify-crawlctl-release.sh" "$VERSION" "$EXPECTED_COMMIT" \ - "$SIGNED_OUTPUT/crawlctl-${VERSION}-macos-arm64.tar.gz" \ - "$SIGNED_OUTPUT/crawlctl-${VERSION}-macos-x86_64.tar.gz" - -[[ ! -L "$OUT_DIR" ]] || { - echo "release output directory must not be a symbolic link: $OUT_DIR" >&2 - exit 1 -} -if [[ ! -d "$OUT_DIR" ]]; then - mkdir "$OUT_DIR" - OUT_DIR_CREATED=1 -fi -DESTINATION_STAGE=$(mktemp -d "$OUT_DIR/.crawlctl-release-promote.XXXXXX") -for artifact in "${artifacts[@]}"; do - mv "$SIGNED_OUTPUT/$artifact" "$DESTINATION_STAGE/$artifact" -done -for artifact in "${artifacts[@]}"; do - [[ ! -e "$OUT_DIR/$artifact" ]] || { - echo "refusing to overwrite existing artifact: $OUT_DIR/$artifact" >&2 - exit 1 - } -done -PROMOTION_ACTIVE=1 -for artifact in "${artifacts[@]}"; do - mv "$DESTINATION_STAGE/$artifact" "$OUT_DIR/$artifact" - promoted_artifacts+=("$artifact") -done -PROMOTION_ACTIVE=0 +echo "local release publishing is disabled; run:" >&2 +echo "gh workflow run release-unified.yml --repo openclaw/crawlkit -f version=$version" >&2 +exit 1 diff --git a/scripts/preflight-crawlctl-release.sh b/scripts/preflight-crawlctl-release.sh deleted file mode 100755 index 3392540..0000000 --- a/scripts/preflight-crawlctl-release.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -VERSION=${1:-} -OUT_DIR=${2:-} -REMOTE=origin -DEFAULT_BRANCH=main - -if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ || -z "$OUT_DIR" ]]; then - echo "usage: $0 vX.Y.Z output-directory" >&2 - exit 2 -fi -for tool in awk git; do - command -v "$tool" >/dev/null || { - echo "missing required tool: $tool" >&2 - exit 1 - } -done -export GIT_NO_REPLACE_OBJECTS=1 -replacement_refs=$(git -C "$ROOT" for-each-ref --format='%(refname)' refs/replace/) -[[ -z "$replacement_refs" ]] || { - echo "release preflight rejects Git replacement refs" >&2 - exit 1 -} -if ! worktree_state=$(git -C "$ROOT" status --porcelain --untracked-files=normal); then - echo "could not inspect release preflight checkout" >&2 - exit 1 -fi -[[ -z "$worktree_state" ]] || { - echo "release preflight checkout is not clean" >&2 - exit 1 -} - -remote_url=$(git -C "$ROOT" remote get-url "$REMOTE") -case "$remote_url" in - https://github.com/openclaw/crawlkit | https://github.com/openclaw/crawlkit.git | \ - git@github.com:openclaw/crawlkit | git@github.com:openclaw/crawlkit.git | \ - ssh://git@github.com/openclaw/crawlkit | ssh://git@github.com/openclaw/crawlkit.git) ;; - *) - echo "release preflight requires the official openclaw/crawlkit origin" >&2 - exit 1 - ;; -esac -remote_default_ref=$(git -C "$ROOT" ls-remote --symref "$REMOTE" HEAD | - awk '$1 == "ref:" && $3 == "HEAD" { print $2 }') -[[ "$remote_default_ref" == "refs/heads/$DEFAULT_BRANCH" ]] || { - echo "official release origin default branch is not $DEFAULT_BRANCH" >&2 - exit 1 -} - -ref_namespace="refs/crawlctl-release-preflight/$$-$RANDOM" -branch_ref="$ref_namespace/branch" -cleanup() { - git -C "$ROOT" update-ref -d "$branch_ref" >/dev/null 2>&1 || true -} -trap cleanup EXIT -git -C "$ROOT" \ - -c fetch.prune=false -c fetch.pruneTags=false \ - -c remote.origin.prune=false -c remote.origin.pruneTags=false \ - fetch --force --no-tags --no-write-fetch-head "$REMOTE" \ - "+refs/heads/$DEFAULT_BRANCH:$branch_ref" >/dev/null -EXPECTED_COMMIT=$(git -C "$ROOT" rev-parse "$branch_ref^{commit}") -[[ "$(git -C "$ROOT" rev-parse HEAD)" == "$EXPECTED_COMMIT" ]] || { - echo "release preflight HEAD does not match protected $DEFAULT_BRANCH" >&2 - exit 1 -} - -helper=${MAC_RELEASE_HELPER:-"$HOME/Projects/agent-scripts/skills/release-mac-app/scripts/mac-release"} -[[ -x "$helper" ]] || { - echo "mac-release helper is not executable: $helper" >&2 - exit 1 -} - -CRAWLCTL_EXPECTED_COMMIT="$EXPECTED_COMMIT" \ - "$helper" codesign-run --with-package-secrets -- \ - "$ROOT/scripts/package-crawlctl-release.sh" --produce "$VERSION" "$OUT_DIR" - -"$ROOT/scripts/verify-crawlctl-release.sh" "$VERSION" "$EXPECTED_COMMIT" \ - "$OUT_DIR/crawlctl-${VERSION}-macos-arm64.tar.gz" \ - "$OUT_DIR/crawlctl-${VERSION}-macos-x86_64.tar.gz" - -printf '%s\n' "$EXPECTED_COMMIT" diff --git a/scripts/test-crawlctl-release.sh b/scripts/test-crawlctl-release.sh index 18cca85..8cdd610 100755 --- a/scripts/test-crawlctl-release.sh +++ b/scripts/test-crawlctl-release.sh @@ -2,693 +2,77 @@ set -euo pipefail ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -EXPECTED_AUTHORITY='Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)' -EXPECTED_TEAM_ID=FWJYW4S8P8 -WRONG_REQUIREMENT='identifier org.openclaw.other and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = FWJYW4S8P8' fail() { - echo "release script test failed: $*" >&2 + echo "release contract test failed: $*" >&2 exit 1 } -for script in download-crawlctl-release-assets.sh install-crawlctl.sh package-crawlctl-release.sh \ - preflight-crawlctl-release.sh verify-crawlctl-release-provenance.sh \ - verify-crawlctl-release.sh; do +for script in install-crawlctl.sh package-crawlctl-release.sh \ + verify-crawlctl-release-provenance.sh verify-crawlctl-release.sh; do bash -n "$ROOT/scripts/$script" done + +[[ ! -e "$ROOT/.github/workflows/release-assets.yml" ]] || + fail "legacy Release Assets workflow still exists" +[[ ! -e "$ROOT/scripts/preflight-crawlctl-release.sh" ]] || + fail "legacy local release preflight still exists" +[[ ! -e "$ROOT/scripts/download-crawlctl-release-assets.sh" ]] || + fail "legacy draft-asset downloader still exists" + +workflow="$ROOT/.github/workflows/release-unified.yml" +grep -F 'uses: openclaw/release-workflows/.github/workflows/release-go-cli.yml@v1' "$workflow" >/dev/null +grep -F 'checksum-filename: checksums.txt' "$workflow" >/dev/null +grep -F 'nfpm: disabled' "$workflow" >/dev/null +grep -F 'stable-identifier: org.openclaw.crawlctl' "$workflow" >/dev/null +grep -F 'require-signed-tag: true' "$workflow" >/dev/null +grep -F 'darwin-universal: disabled' "$workflow" >/dev/null +if grep -Eq 'homebrew-|TAP_TOKEN|archive-(name|template)|sidecar' "$workflow"; then + fail "unified workflow contains an unsupported release customization" +fi + +config="$ROOT/.goreleaser.yaml" +grep -F 'project_name: crawlkit' "$config" >/dev/null +grep -F 'binary: crawlctl' "$config" >/dev/null +grep -F 'name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"' "$config" >/dev/null +for target in darwin_amd64 darwin_arm64 linux_amd64 linux_arm64; do + grep -F -- "- $target" "$config" >/dev/null || fail "missing GoReleaser target $target" +done +if grep -Eq 'windows_|nfpms:|universal' "$config"; then + fail "GoReleaser config contains an unexpected release target or package" +fi + grep -Fx \ 'steipete@gmail.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rFpd7CodTF6fy60LZTriTeiGAJ7haIBWD4hrdxmDB' \ "$ROOT/.github/release-allowed-signers" >/dev/null [[ "$(wc -l < "$ROOT/.github/release-allowed-signers" | tr -d ' ')" == 1 ]] || fail "release signer policy must contain exactly one reviewed signer" -grep -F "github.event_name == 'release' ||" \ - "$ROOT/.github/workflows/release-assets.yml" >/dev/null -grep -F "ref: \${{ github.event_name == 'release' && github.event.repository.default_branch || github.workflow_sha }}" \ - "$ROOT/.github/workflows/release-assets.yml" >/dev/null -grep -F "expected_draft=\"\${{ inputs.draft }}\"" \ - "$ROOT/.github/workflows/release-assets.yml" >/dev/null -if grep -F 'NOTARYTOOL_KEYCHAIN_PROFILE' "$ROOT/.github/workflows/release-assets.yml" >/dev/null; then - fail "protected draft verifier must remain credential-free" -fi -# shellcheck disable=SC2016 -grep -F './scripts/package-crawlctl-release.sh "$(VERSION)"' "$ROOT/Makefile" >/dev/null -grep -F 'codesign-run --with-package-secrets --' "$ROOT/scripts/package-crawlctl-release.sh" >/dev/null -# shellcheck disable=SC2016 -grep -F 'verify-crawlctl-release-provenance.sh "$RELEASE_TAG"' \ - "$ROOT/.github/workflows/release-assets.yml" >/dev/null -# shellcheck disable=SC2016 -grep -F '"$RELEASE_TAG" "$RELEASE_COMMIT" "$archive"' \ - "$ROOT/.github/workflows/release-assets.yml" >/dev/null -grep -F 'gpg.ssh.allowedSignersFile=' \ - "$ROOT/scripts/verify-crawlctl-release-provenance.sh" >/dev/null -grep -F 'export GIT_NO_REPLACE_OBJECTS=1' \ - "$ROOT/scripts/preflight-crawlctl-release.sh" >/dev/null -grep -F -- '-c fetch.prune=false -c fetch.pruneTags=false' \ - "$ROOT/scripts/preflight-crawlctl-release.sh" >/dev/null -grep -F -- '-c remote.origin.prune=false -c remote.origin.pruneTags=false' \ - "$ROOT/scripts/preflight-crawlctl-release.sh" >/dev/null -# shellcheck disable=SC2016 -grep -F '+refs/heads/$DEFAULT_BRANCH:$branch_ref' \ - "$ROOT/scripts/verify-crawlctl-release-provenance.sh" >/dev/null -grep -F 'go build -buildvcs=true' "$ROOT/scripts/package-crawlctl-release.sh" >/dev/null -# shellcheck disable=SC2016 -grep -F 'go version -m "$binary"' "$ROOT/scripts/verify-crawlctl-release.sh" >/dev/null -# shellcheck disable=SC2016 -grep -F 'env -i PATH=/usr/bin:/bin "$binary" --version' \ - "$ROOT/scripts/verify-crawlctl-release.sh" "$ROOT/scripts/install-crawlctl.sh" >/dev/null -grep -Fx '.mac-release.env' "$ROOT/.gitignore" >/dev/null -if git -C "$ROOT" ls-files | grep -Fx '.mac-release.env' >/dev/null; then - fail "runtime release manifest must remain untracked" -fi -grep -Fx "MAC_RELEASE_CODESIGN_IDENTITY='$EXPECTED_AUTHORITY'" \ - "$ROOT/.mac-release.env.example" >/dev/null -grep -Fx 'MAC_RELEASE_CODESIGN_KEYCHAIN_MANAGED=1' "$ROOT/.mac-release.env.example" >/dev/null -grep -Fx 'MAC_RELEASE_CODESIGN_PASSWORDLESS=1' "$ROOT/.mac-release.env.example" >/dev/null -if grep -Eq '^(MAC_RELEASE_CODESIGN_KEYCHAIN=|MAC_RELEASE_CODESIGN_OP_|MAC_RELEASE_OP_)' \ - "$ROOT/.mac-release.env.example"; then - fail "release manifest example contains runtime credential routing" -fi -if grep -E 'stapler|spctl' "$ROOT/scripts/package-crawlctl-release.sh" \ - "$ROOT/scripts/verify-crawlctl-release.sh" "$ROOT/scripts/install-crawlctl.sh" >/dev/null; then - fail "raw crawlctl release flow must not claim stapling or spctl assessment support" -fi - -WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/crawlctl-release-test.XXXXXX") -trap 'rm -rf "$WORK_DIR"' EXIT -FAKE_BIN="$WORK_DIR/bin" -mkdir -p "$FAKE_BIN" -MOCK_COMMIT=0123456789abcdef0123456789abcdef01234567 -MOCK_SIDE_COMMIT=89abcdef0123456789abcdef0123456789abcdef -MOCK_CLEANUP_MARKER="$WORK_DIR/keychain-cleaned" -MOCK_RELEASE_EVENT_LOG="$WORK_DIR/release-events.log" -MOCK_HELPER_LOG="$WORK_DIR/helper.log" -MOCK_GIT_LOG="$WORK_DIR/git.log" -MOCK_DITTO_LOG="$WORK_DIR/ditto.log" -MOCK_FETCHED_TAG_NAME_FILE="$WORK_DIR/fetched-tag-name" -export MOCK_COMMIT MOCK_CLEANUP_MARKER MOCK_RELEASE_EVENT_LOG MOCK_HELPER_LOG MOCK_GIT_LOG \ - MOCK_DITTO_LOG MOCK_FETCHED_TAG_NAME_FILE - -cat > "$FAKE_BIN/uname" <<'EOF' -#!/usr/bin/env bash -case "${1:-}" in - -s) echo Darwin ;; - -m) echo arm64 ;; - *) echo Darwin ;; -esac -EOF - -cat > "$FAKE_BIN/git" <<'EOF' -#!/usr/bin/env bash -if [[ "${1:-}" == -C ]]; then - shift 2 -fi -while [[ "${1:-}" == -c ]]; do - shift 2 -done -case "${1:-}" in - remote) - [[ "${2:-}" == get-url && "${3:-}" == origin ]] - echo "${MOCK_REMOTE_URL:-https://github.com/openclaw/crawlkit.git}" - ;; - ls-remote) - [[ "${2:-}" == --symref && "${3:-}" == origin && "${4:-}" == HEAD ]] - printf 'ref: refs/heads/%s\tHEAD\n' "${MOCK_DEFAULT_BRANCH:-main}" - ;; - fetch) - printf '%s\n' "$*" >> "${MOCK_GIT_LOG:?}" - for refspec in "$@"; do - case "$refspec" in - +refs/tags/*:*) - fetched_tag=${refspec#+refs/tags/} - printf '%s\n' "${fetched_tag%%:*}" > "${MOCK_FETCHED_TAG_NAME_FILE:?}" - ;; - esac - done - [[ "${MOCK_FETCH_RESULT:-ok}" == ok ]] - ;; - cat-file) - case "${2:-}" in - -t) echo "${MOCK_TAG_TYPE:-tag}" ;; - tag) - tag_name=${MOCK_TAG_NAME:-} - if [[ -z "$tag_name" && -s "${MOCK_FETCHED_TAG_NAME_FILE:?}" ]]; then - tag_name=$(/bin/cat "$MOCK_FETCHED_TAG_NAME_FILE") - fi - tag_name=${tag_name:-v0.13.4} - signature_format=${MOCK_TAG_SIGNATURE_FORMAT:-SSH} - cat < 0 +0000 - -Release $tag_name ------BEGIN $signature_format SIGNATURE----- -mock-signature ------END $signature_format SIGNATURE----- -TAG - ;; - *) exit 2 ;; - esac - ;; - verify-tag) - [[ "${MOCK_VERIFY_TAG_RESULT:-accepted}" == accepted ]] - ;; - rev-parse) - case "${2:-}" in - HEAD) echo "${MOCK_LOCAL_HEAD:-${MOCK_COMMIT:?}}" ;; - */tag\^\{commit\}) echo "${MOCK_TAG_COMMIT:-${MOCK_COMMIT:?}}" ;; - */branch\^\{commit\}) echo "${MOCK_BRANCH_COMMIT:-${MOCK_COMMIT:?}}" ;; - *) exit 2 ;; - esac - ;; - status) [[ "${MOCK_STATUS_RESULT:-ok}" == ok ]] ;; - for-each-ref) - [[ -z "${MOCK_REPLACE_REF:-}" ]] || echo "$MOCK_REPLACE_REF" - ;; - update-ref) exit 0 ;; - *) exit 2 ;; -esac -EOF - -cat > "$FAKE_BIN/go" <<'EOF' -#!/usr/bin/env bash -if [[ "${1:-}" == version && "${2:-}" == -m ]]; then - { - printf '\tpath\t%s\n' "${MOCK_BUILDINFO_PATH:-github.com/openclaw/crawlkit/cmd/crawlctl}" - printf '\tbuild\tvcs=git\n' - printf '\tbuild\tvcs.revision=%s\n' "${MOCK_BUILDINFO_REVISION:-${MOCK_COMMIT:?}}" - printf '\tbuild\tvcs.modified=%s\n' "${MOCK_BUILDINFO_MODIFIED:-false}" - } - exit 0 -fi -output= -version= -while [[ "$#" -gt 0 ]]; do - case "$1" in - -o) - output=$2 - shift 2 - ;; - -ldflags) - version=${2##*=} - shift 2 - ;; - *) shift ;; - esac -done -[[ -n "$output" && -n "$version" ]] -{ - echo '#!/usr/bin/env bash' - echo '[[ "${1:-}" == --version ]] || exit 2' - printf '[[ -f %q ]] || exit 91\n' "${MOCK_CLEANUP_MARKER:?}" - echo '[[ -z "${GH_TOKEN:-}${GITHUB_TOKEN:-}${NOTARYTOOL_KEYCHAIN_PROFILE:-}${CODESIGN_IDENTITY:-}" ]] || exit 92' - printf 'printf "candidate\\n" >> %q\n' "${MOCK_RELEASE_EVENT_LOG:?}" - printf 'echo %q\n' "$version" -} > "$output" -chmod 0755 "$output" -EOF - -cat > "$FAKE_BIN/lipo" <<'EOF' -#!/usr/bin/env bash -if [[ -n "${MOCK_LIPO_ARCHS:-}" ]]; then - echo "$MOCK_LIPO_ARCHS" - exit 0 -fi -case "${2:-}" in - */x86_64/*) echo x86_64 ;; - *) echo arm64 ;; -esac -EOF - -cat > "$FAKE_BIN/codesign" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' "$*" >> "${MOCK_CODESIGN_LOG:?}" -if [[ " $* " == *' --check-notarization '* && "${MOCK_NOTARY_TICKET:-accepted}" != accepted ]]; then - exit 1 -fi -case " $* " in - *' -d -r- '*) - echo "designated => ${MOCK_DESIGNATED_REQUIREMENT:-identifier org.openclaw.crawlctl and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = FWJYW4S8P8}" >&2 - ;; - *' -dvvv '*) - { - echo 'CodeDirectory v=20500 size=123 flags=0x10000(runtime) hashes=1+2 location=embedded' - echo 'Identifier=org.openclaw.crawlctl' - echo "Authority=${MOCK_CODESIGN_AUTHORITY:-Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)}" - echo "TeamIdentifier=${MOCK_CODESIGN_TEAM_ID:-FWJYW4S8P8}" - } >&2 - ;; -esac -EOF - -cat > "$FAKE_BIN/csreq" <<'EOF' -#!/usr/bin/env bash -[[ "$#" == 3 && "$1" == -r && "$2" == =* && "$3" == -t ]] -printf '%s\n' "${2#=}" | tr -d '"' -EOF - -cat > "$FAKE_BIN/ditto" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' "$*" >> "${MOCK_DITTO_LOG:?}" -[[ "$#" == 5 && "$1" == -c && "$2" == -k && "$3" == --keepParent ]] -[[ -f "$4" && "$5" == *.zip ]] -/bin/cp "$4" "$5" -EOF - -cat > "$FAKE_BIN/xcrun" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' "$*" >> "${MOCK_XCRUN_LOG:?}" -[[ "${1:-}" == notarytool && "${2:-}" == submit ]] -archive=${3:-} -[[ -f "$archive" && "$archive" == *.zip ]] -shift 3 -profile= -no_s3=0 -wait_for_result=0 -json_output=0 -while [[ "$#" -gt 0 ]]; do - case "$1" in - --keychain-profile) - profile=${2:-} - shift 2 - ;; - --no-s3-acceleration) - no_s3=1 - shift - ;; - --wait) - wait_for_result=1 - shift - ;; - --output-format) - [[ "${2:-}" == json ]] && json_output=1 - shift 2 - ;; - *) exit 2 ;; - esac -done -[[ -n "$profile" && "$no_s3" == 1 && "$wait_for_result" == 1 && "$json_output" == 1 ]] -[[ "${MOCK_NOTARY_EXIT:-0}" == 0 ]] || exit 1 -printf '{"id":"mock-submission","status":"%s"}\n' "${MOCK_NOTARY_STATUS:-Accepted}" -EOF - -cat > "$FAKE_BIN/curl" <<'EOF' -#!/usr/bin/env bash -output= -url= -while [[ "$#" -gt 0 ]]; do - case "$1" in - -o) - output=$2 - shift 2 - ;; - -w) - shift 2 - ;; - -*) shift ;; - *) - url=$1 - shift - ;; - esac -done -[[ -n "$output" && -n "$url" ]] -cp "${MOCK_ASSET_DIR:?}/${url##*/}" "$output" -EOF - -cat > "$FAKE_BIN/gh" <<'EOF' -#!/usr/bin/env bash -[[ "${1:-}" == api ]] || exit 2 -shift -if [[ "${1:-}" == --paginate ]]; then - shift -fi -endpoint=${1:-} -case "$endpoint" in - repos/*/releases\?per_page=100) cat "${MOCK_GH_RELEASES_JSON:?}" ;; - repos/*/releases/*/assets\?per_page=100) cat "${MOCK_GH_ASSETS_JSON:?}" ;; - https://api.github.com/repos/*/releases/assets/*) cat "${MOCK_GH_ASSET_DIR:?}/${endpoint##*/}" ;; - *) exit 2 ;; -esac -EOF - -cat > "$FAKE_BIN/mac-release" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' "$*" >> "${MOCK_HELPER_LOG:?}" -[[ "${1:-}" == codesign-run && "${2:-}" == --with-package-secrets && "${3:-}" == -- ]] -shift 3 -rm -f "${MOCK_CLEANUP_MARKER:?}" -CODESIGN_IDENTITY='Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)' \ - NOTARYTOOL_KEYCHAIN_PROFILE=test-profile "$@" -result=$? -printf 'cleanup\n' >> "${MOCK_RELEASE_EVENT_LOG:?}" -touch "$MOCK_CLEANUP_MARKER" -exit "$result" -EOF - -cat > "$FAKE_BIN/mv" <<'EOF' -#!/usr/bin/env bash -destination=${!#} -if [[ -n "${MOCK_FAIL_PROMOTION_DIR:-}" && "$(dirname "$destination")" == "$MOCK_FAIL_PROMOTION_DIR" ]]; then - count=0 - [[ ! -f "${MOCK_PROMOTION_COUNTER:?}" ]] || count=$(<"$MOCK_PROMOTION_COUNTER") - count=$((count + 1)) - printf '%s\n' "$count" > "$MOCK_PROMOTION_COUNTER" - [[ "$count" -lt 2 ]] || exit 70 -fi -exec /bin/mv "$@" -EOF - -chmod 0755 "$FAKE_BIN"/* -export PATH="$FAKE_BIN:$PATH" -export MOCK_CODESIGN_LOG="$WORK_DIR/codesign.log" -export MOCK_XCRUN_LOG="$WORK_DIR/xcrun.log" -export MAC_RELEASE_HELPER="$FAKE_BIN/mac-release" - -verified_commit=$(bash "$ROOT/scripts/verify-crawlctl-release-provenance.sh" v0.13.4) -[[ "$verified_commit" == "$MOCK_COMMIT" ]] || fail "wrong verified remote release commit" -if MOCK_TAG_COMMIT="$MOCK_SIDE_COMMIT" \ - bash "$ROOT/scripts/verify-crawlctl-release-provenance.sh" v0.13.4 >/dev/null 2>&1; then - fail "signed side-commit release tag matched the protected branch head" -fi -if MOCK_VERIFY_TAG_RESULT=rejected \ - bash "$ROOT/scripts/verify-crawlctl-release-provenance.sh" v0.13.4 >/dev/null 2>&1; then - fail "release tag outside the repository-pinned signer policy was accepted" -fi -if MOCK_TAG_SIGNATURE_FORMAT=PGP \ - bash "$ROOT/scripts/verify-crawlctl-release-provenance.sh" v0.13.4 >/dev/null 2>&1; then - fail "non-SSH release tag signature was accepted" -fi -if MOCK_TAG_NAME=v0.13.3 \ - bash "$ROOT/scripts/verify-crawlctl-release-provenance.sh" v0.13.4 >/dev/null 2>&1; then - fail "signed tag object naming another release was accepted" -fi -if MOCK_REMOTE_URL=https://github.com/steipete/crawlkit.git \ - bash "$ROOT/scripts/verify-crawlctl-release-provenance.sh" v0.13.4 >/dev/null 2>&1; then - fail "non-official release origin was accepted" -fi -if MOCK_DEFAULT_BRANCH=develop \ - bash "$ROOT/scripts/verify-crawlctl-release-provenance.sh" v0.13.4 >/dev/null 2>&1; then - fail "non-main official remote default branch was accepted" -fi -grep -F 'refs/tags/v0.13.4:' "$MOCK_GIT_LOG" >/dev/null || fail "remote tag was not fetched" -grep -F 'refs/heads/main:' "$MOCK_GIT_LOG" >/dev/null || fail "protected branch head was not fetched" - -if CRAWLCTL_EXPECTED_COMMIT="$MOCK_COMMIT" CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \ - bash "$ROOT/scripts/package-crawlctl-release.sh" --produce \ - v0.13.4 "$WORK_DIR/missing-profile" >/dev/null 2>&1; then - fail "missing notarization profile was accepted" -fi -[[ ! -e "$WORK_DIR/missing-profile" ]] || fail "missing profile mutated the artifact destination" - -if CRAWLCTL_EXPECTED_COMMIT="$MOCK_COMMIT" \ - CODESIGN_IDENTITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \ - NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \ - bash "$ROOT/scripts/package-crawlctl-release.sh" --produce \ - v0.13.4 "$WORK_DIR/wrong-identity" >/dev/null 2>&1; then - fail "personal signing identity was accepted" -fi -[[ ! -e "$WORK_DIR/wrong-identity" ]] || fail "wrong identity mutated the artifact destination" - -if CRAWLCTL_EXPECTED_COMMIT="$MOCK_COMMIT" CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \ - NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \ - MOCK_DESIGNATED_REQUIREMENT="$WRONG_REQUIREMENT" \ - bash "$ROOT/scripts/package-crawlctl-release.sh" --produce \ - v0.13.4 "$WORK_DIR/wrong-requirement" >/dev/null 2>&1; then - fail "mismatched embedded designated requirement was accepted" -fi -[[ ! -e "$WORK_DIR/wrong-requirement" ]] || - fail "wrong designated requirement mutated the artifact destination" - -if CRAWLCTL_EXPECTED_COMMIT="$MOCK_COMMIT" CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \ - NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \ - MOCK_LIPO_ARCHS='arm64 x86_64' \ - bash "$ROOT/scripts/package-crawlctl-release.sh" --produce \ - v0.13.4 "$WORK_DIR/universal-package" >/dev/null 2>&1; then - fail "universal binary was accepted for an architecture-specific release asset" -fi -[[ ! -e "$WORK_DIR/universal-package" ]] || - fail "universal binary mutated the artifact destination" - -if CRAWLCTL_EXPECTED_COMMIT="$MOCK_COMMIT" CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \ - NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \ - MOCK_NOTARY_STATUS=Invalid \ - bash "$ROOT/scripts/package-crawlctl-release.sh" --produce \ - v0.13.4 "$WORK_DIR/rejected-submission" >/dev/null 2>&1; then - fail "rejected notarization submission was accepted" -fi -[[ ! -e "$WORK_DIR/rejected-submission" ]] || fail "rejected submission mutated the artifact destination" - -if CRAWLCTL_EXPECTED_COMMIT="$MOCK_COMMIT" CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \ - NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \ - MOCK_NOTARY_TICKET=missing \ - bash "$ROOT/scripts/package-crawlctl-release.sh" --produce \ - v0.13.4 "$WORK_DIR/missing-ticket" >/dev/null 2>&1; then - fail "missing online notarization ticket was accepted" -fi -[[ ! -e "$WORK_DIR/missing-ticket" ]] || fail "missing ticket mutated the artifact destination" - -for version in v0.13.4 v0.14.0; do - GH_TOKEN=caller-token GITHUB_TOKEN=caller-token \ - bash "$ROOT/scripts/package-crawlctl-release.sh" "$version" "$WORK_DIR/$version" >/dev/null - for arch in arm64 x86_64; do - archive="$WORK_DIR/$version/crawlctl-${version}-macos-${arch}.tar.gz" - [[ -f "$archive" && -f "$archive.sha256" ]] || fail "missing $version $arch artifact" - done - [[ "$(find "$WORK_DIR/$version" -maxdepth 1 -type f | wc -l | tr -d ' ')" == 4 ]] || - fail "unexpected release artifact inventory for $version" -done -if MOCK_STATUS_RESULT=fail bash "$ROOT/scripts/preflight-crawlctl-release.sh" \ - v0.14.3 "$WORK_DIR/preflight-status-error" >/dev/null 2>&1; then - fail "preflight accepted a failed checkout cleanliness probe" -fi -[[ ! -e "$WORK_DIR/preflight-status-error" ]] || - fail "failed checkout probe mutated the preflight artifact destination" -if MOCK_REPLACE_REF=refs/replace/$MOCK_COMMIT \ - bash "$ROOT/scripts/preflight-crawlctl-release.sh" \ - v0.14.3 "$WORK_DIR/preflight-replace-ref" >/dev/null 2>&1; then - fail "preflight accepted a Git replacement ref" -fi -[[ ! -e "$WORK_DIR/preflight-replace-ref" ]] || - fail "replacement ref mutated the preflight artifact destination" -if MOCK_REMOTE_URL=https://github.com/steipete/crawlkit.git \ - bash "$ROOT/scripts/preflight-crawlctl-release.sh" \ - v0.14.3 "$WORK_DIR/preflight-wrong-origin" >/dev/null 2>&1; then - fail "preflight accepted a non-official origin" -fi -[[ ! -e "$WORK_DIR/preflight-wrong-origin" ]] || - fail "non-official origin mutated the preflight artifact destination" -if MOCK_LOCAL_HEAD="$MOCK_SIDE_COMMIT" bash "$ROOT/scripts/preflight-crawlctl-release.sh" \ - v0.14.3 "$WORK_DIR/preflight-side-commit" >/dev/null 2>&1; then - fail "preflight accepted a checkout outside protected main" -fi -[[ ! -e "$WORK_DIR/preflight-side-commit" ]] || - fail "side-commit preflight mutated the artifact destination" -preflight_commit=$(bash "$ROOT/scripts/preflight-crawlctl-release.sh" \ - v0.14.3 "$WORK_DIR/preflight") -[[ "$preflight_commit" == "$MOCK_COMMIT" ]] || fail "preflight reported the wrong commit" -for arch in arm64 x86_64; do - archive="$WORK_DIR/preflight/crawlctl-v0.14.3-macos-${arch}.tar.gz" - [[ -f "$archive" && -f "$archive.sha256" ]] || fail "missing preflight $arch artifact" -done -[[ "$(find "$WORK_DIR/preflight" -maxdepth 1 -type f | wc -l | tr -d ' ')" == 4 ]] || - fail "unexpected preflight artifact inventory" -grep -F 'codesign-run --with-package-secrets --' "$MOCK_HELPER_LOG" >/dev/null || - fail "package producer did not use the managed release helper" -awk ' - $0 == "cleanup" { cleaned = 1; next } - $0 == "candidate" && !cleaned { exit 1 } -' "$MOCK_RELEASE_EVENT_LOG" || fail "candidate executed before managed keychain cleanup" -grep -F 'candidate' "$MOCK_RELEASE_EVENT_LOG" >/dev/null || fail "candidate probe was not instrumented" - -if MOCK_BUILDINFO_REVISION="$MOCK_SIDE_COMMIT" \ - bash "$ROOT/scripts/package-crawlctl-release.sh" v0.14.0 "$WORK_DIR/wrong-build-revision" \ - >/dev/null 2>&1; then - fail "package accepted a binary built from the wrong revision" -fi -[[ ! -e "$WORK_DIR/wrong-build-revision" ]] || - fail "wrong build revision mutated the artifact destination" -if MOCK_BUILDINFO_MODIFIED=true \ - bash "$ROOT/scripts/package-crawlctl-release.sh" v0.14.0 "$WORK_DIR/modified-build" \ - >/dev/null 2>&1; then - fail "package accepted a binary with vcs.modified=true" -fi -[[ ! -e "$WORK_DIR/modified-build" ]] || - fail "modified build mutated the artifact destination" - -if MOCK_BUILDINFO_PATH=github.com/openclaw/crawlkit/cmd/other \ - bash "$ROOT/scripts/package-crawlctl-release.sh" v0.14.0 "$WORK_DIR/wrong-package-path" \ - >/dev/null 2>&1; then - fail "package accepted another main package built from the release commit" -fi -[[ ! -e "$WORK_DIR/wrong-package-path" ]] || - fail "wrong Go package path mutated the artifact destination" +release_output=$(bash "$ROOT/scripts/package-crawlctl-release.sh" v0.14.4 2>&1) && + fail "local package script unexpectedly succeeded" +grep -Fx 'gh workflow run release-unified.yml --repo openclaw/crawlkit -f version=0.14.4' \ + <<<"$release_output" >/dev/null || fail "local package refusal omitted the dispatch command" -promotion_failure="$(cd "$WORK_DIR" && pwd)/promotion-failure" -promotion_counter="$WORK_DIR/promotion-counter" -if MOCK_FAIL_PROMOTION_DIR="$promotion_failure" MOCK_PROMOTION_COUNTER="$promotion_counter" \ - bash "$ROOT/scripts/package-crawlctl-release.sh" v0.14.0 "$promotion_failure" \ - >/dev/null 2>&1; then - fail "injected failure after the first artifact promotion was accepted" +grep -F 'crawlkit_${release_version}_darwin_${arch}.tar.gz' \ + "$ROOT/scripts/install-crawlctl.sh" >/dev/null +grep -F 'checksums.txt' "$ROOT/scripts/install-crawlctl.sh" >/dev/null +if grep -Eq 'crawlctl-\$\{VERSION\}-macos-|\.tar\.gz\.sha256' "$ROOT/scripts"/*.sh; then + fail "release scripts still reference the retired asset contract" fi -[[ ! -e "$promotion_failure" ]] || fail "failed promotion left partial official artifacts" -bash "$ROOT/scripts/package-crawlctl-release.sh" v0.14.0 "$promotion_failure" >/dev/null -[[ "$(find "$promotion_failure" -maxdepth 1 -type f | wc -l | tr -d ' ')" == 4 ]] || - fail "release packaging was not rerunnable after promotion rollback" - -grep -F 'notarytool submit' "$MOCK_XCRUN_LOG" >/dev/null || fail "notarytool was not invoked" -grep -F -- '-c -k --keepParent' "$MOCK_DITTO_LOG" >/dev/null || - fail "notarization submission ZIP was not created through the mocked macOS contract" -grep -F -- '--no-s3-acceleration --wait --output-format json' "$MOCK_XCRUN_LOG" >/dev/null || - fail "notarytool did not use the required synchronous upload contract" -grep -F -- '--check-notarization' "$MOCK_CODESIGN_LOG" >/dev/null || - fail "online notarization tickets were not checked" -grep -F -- '-R=notarized' "$MOCK_CODESIGN_LOG" >/dev/null || - fail "online notarization checks did not require a notarized binary" -grep -F -- '-d -r-' "$MOCK_CODESIGN_LOG" >/dev/null || - fail "embedded designated requirements were not inspected" -MOCK_GH_ASSET_DIR="$WORK_DIR/gh-assets" -mkdir -p "$MOCK_GH_ASSET_DIR" -cp "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" "$MOCK_GH_ASSET_DIR/1" -cp "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz.sha256" "$MOCK_GH_ASSET_DIR/2" -cp "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-x86_64.tar.gz" "$MOCK_GH_ASSET_DIR/3" -cp "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-x86_64.tar.gz.sha256" "$MOCK_GH_ASSET_DIR/4" -MOCK_GH_RELEASES_JSON="$WORK_DIR/releases.json" -MOCK_GH_ASSETS_JSON="$WORK_DIR/assets.json" -cat > "$MOCK_GH_RELEASES_JSON" <<'EOF' -[{"id":42,"tag_name":"v0.13.4","draft":true}] -EOF -cat > "$MOCK_GH_ASSETS_JSON" <<'EOF' -[ - {"name":"crawlctl-v0.13.4-macos-arm64.tar.gz","url":"https://api.github.com/repos/openclaw/crawlkit/releases/assets/1"}, - {"name":"crawlctl-v0.13.4-macos-arm64.tar.gz.sha256","url":"https://api.github.com/repos/openclaw/crawlkit/releases/assets/2"}, - {"name":"crawlctl-v0.13.4-macos-x86_64.tar.gz","url":"https://api.github.com/repos/openclaw/crawlkit/releases/assets/3"}, - {"name":"crawlctl-v0.13.4-macos-x86_64.tar.gz.sha256","url":"https://api.github.com/repos/openclaw/crawlkit/releases/assets/4"} -] -EOF -export MOCK_GH_ASSET_DIR MOCK_GH_RELEASES_JSON MOCK_GH_ASSETS_JSON -api_download="$WORK_DIR/api-download" -GITHUB_REPOSITORY=openclaw/crawlkit GH_TOKEN=test \ - bash "$ROOT/scripts/download-crawlctl-release-assets.sh" v0.13.4 arm64 true "$api_download" -cmp "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" \ - "$api_download/crawlctl-v0.13.4-macos-arm64.tar.gz" -cmp "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz.sha256" \ - "$api_download/crawlctl-v0.13.4-macos-arm64.tar.gz.sha256" -if GITHUB_REPOSITORY=openclaw/crawlkit GH_TOKEN=test \ - bash "$ROOT/scripts/download-crawlctl-release-assets.sh" v0.13.4 arm64 false "$WORK_DIR/wrong-draft" \ - >/dev/null 2>&1; then - fail "draft release matched published-release lookup" -fi - -if MOCK_CODESIGN_AUTHORITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \ - bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.13.4 "$MOCK_COMMIT" \ - "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" >/dev/null 2>&1; then - fail "personal signature was accepted" -fi -if MOCK_NOTARY_TICKET=missing \ - bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.13.4 "$MOCK_COMMIT" \ - "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" >/dev/null 2>&1; then - fail "verifier accepted a missing notarization ticket" -fi -if MOCK_DESIGNATED_REQUIREMENT="$WRONG_REQUIREMENT" \ - bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.13.4 "$MOCK_COMMIT" \ - "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" >/dev/null 2>&1; then - fail "verifier accepted a mismatched embedded designated requirement" -fi -if MOCK_LIPO_ARCHS='arm64 x86_64' \ - bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.13.4 "$MOCK_COMMIT" \ - "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" >/dev/null 2>&1; then - fail "verifier accepted a universal binary in an architecture-specific asset" -fi -if MOCK_BUILDINFO_REVISION="$MOCK_SIDE_COMMIT" \ - bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.13.4 "$MOCK_COMMIT" \ - "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" >/dev/null 2>&1; then - fail "verifier accepted a binary built from the wrong revision" -fi -if MOCK_BUILDINFO_MODIFIED=true \ - bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.13.4 "$MOCK_COMMIT" \ - "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" >/dev/null 2>&1; then - fail "verifier accepted a binary with vcs.modified=true" -fi -if MOCK_BUILDINFO_PATH=github.com/openclaw/crawlkit/cmd/other \ - bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.13.4 "$MOCK_COMMIT" \ - "$WORK_DIR/v0.13.4/crawlctl-v0.13.4-macos-arm64.tar.gz" >/dev/null 2>&1; then - fail "verifier accepted another main package built from the release commit" -fi -INSTALL_DIR="$WORK_DIR/install" -for version in v0.13.4 v0.14.0; do - GH_TOKEN=caller-token GITHUB_TOKEN=caller-token MOCK_ASSET_DIR="$WORK_DIR/$version" \ - CRAWLCTL_DOWNLOAD_BASE_URL=https://example.invalid/releases/download/"$version" \ - bash "$ROOT/scripts/install-crawlctl.sh" "$version" "$INSTALL_DIR" >/dev/null - [[ "$("$INSTALL_DIR/crawlctl" --version)" == "${version#v}" ]] || fail "installed version mismatch" - if [[ "$version" == v0.13.4 ]]; then - first_hash=$(shasum -a 256 "$INSTALL_DIR/crawlctl" | awk '{print $1}') - fi +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/crawlctl-release-contract.XXXXXX") +trap 'rm -rf "$WORK_DIR"' EXIT +for arch in amd64 arm64; do + CGO_ENABLED=0 GOOS=linux GOARCH="$arch" GOWORK=off \ + go build -trimpath -ldflags '-s -w -X main.version=0.14.4' \ + -o "$WORK_DIR/crawlctl-linux-$arch" "$ROOT/cmd/crawlctl" done -second_hash=$(shasum -a 256 "$INSTALL_DIR/crawlctl" | awk '{print $1}') -[[ "$first_hash" != "$second_hash" ]] || fail "update did not replace the executable" - -installed_hash=$second_hash -if MOCK_NOTARY_TICKET=missing MOCK_ASSET_DIR="$WORK_DIR/v0.14.0" \ - CRAWLCTL_DOWNLOAD_BASE_URL=https://example.invalid/releases/download/v0.14.0 \ - bash "$ROOT/scripts/install-crawlctl.sh" v0.14.0 "$INSTALL_DIR" >/dev/null 2>&1; then - fail "installer accepted a missing notarization ticket" -fi -[[ "$(shasum -a 256 "$INSTALL_DIR/crawlctl" | awk '{print $1}')" == "$installed_hash" ]] || - fail "missing-ticket install changed the destination" - -if MOCK_DESIGNATED_REQUIREMENT="$WRONG_REQUIREMENT" \ - MOCK_ASSET_DIR="$WORK_DIR/v0.14.0" \ - CRAWLCTL_DOWNLOAD_BASE_URL=https://example.invalid/releases/download/v0.14.0 \ - bash "$ROOT/scripts/install-crawlctl.sh" v0.14.0 "$INSTALL_DIR" >/dev/null 2>&1; then - fail "installer accepted a mismatched embedded designated requirement" -fi -[[ "$(shasum -a 256 "$INSTALL_DIR/crawlctl" | awk '{print $1}')" == "$installed_hash" ]] || - fail "wrong-designated-requirement install changed the destination" - -if MOCK_LIPO_ARCHS='arm64 x86_64' MOCK_ASSET_DIR="$WORK_DIR/v0.14.0" \ - CRAWLCTL_DOWNLOAD_BASE_URL=https://example.invalid/releases/download/v0.14.0 \ - bash "$ROOT/scripts/install-crawlctl.sh" v0.14.0 "$INSTALL_DIR" >/dev/null 2>&1; then - fail "installer accepted a universal binary in an architecture-specific asset" +if [[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]]; then + [[ "$($WORK_DIR/crawlctl-linux-amd64 --version)" == 0.14.4 ]] || + fail "Linux amd64 version probe failed" +elif command -v file >/dev/null; then + file "$WORK_DIR/crawlctl-linux-amd64" | grep -F 'ELF 64-bit' >/dev/null || + fail "Linux amd64 build is not an ELF executable" fi -[[ "$(shasum -a 256 "$INSTALL_DIR/crawlctl" | awk '{print $1}')" == "$installed_hash" ]] || - fail "universal-binary install changed the destination" - -if MOCK_CODESIGN_AUTHORITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \ - MOCK_ASSET_DIR="$WORK_DIR/v0.14.0" \ - CRAWLCTL_DOWNLOAD_BASE_URL=https://example.invalid/releases/download/v0.14.0 \ - bash "$ROOT/scripts/install-crawlctl.sh" v0.14.0 "$INSTALL_DIR" >/dev/null 2>&1; then - fail "installer accepted the wrong signing identity" -fi -[[ "$(shasum -a 256 "$INSTALL_DIR/crawlctl" | awk '{print $1}')" == "$installed_hash" ]] || - fail "wrong-identity install changed the destination" - -BAD_ASSETS="$WORK_DIR/bad-assets" -BAD_STAGE="$WORK_DIR/bad-stage" -mkdir -p "$BAD_ASSETS" "$BAD_STAGE" -cp "$INSTALL_DIR/crawlctl" "$BAD_STAGE/crawlctl" -echo unexpected > "$BAD_STAGE/unexpected" -bad_asset=crawlctl-v0.14.1-macos-arm64.tar.gz -tar -czf "$BAD_ASSETS/$bad_asset" -C "$BAD_STAGE" crawlctl unexpected -( - cd "$BAD_ASSETS" - shasum -a 256 "$bad_asset" > "$bad_asset.sha256" -) -if MOCK_ASSET_DIR="$BAD_ASSETS" \ - CRAWLCTL_DOWNLOAD_BASE_URL=https://example.invalid/releases/download/v0.14.1 \ - bash "$ROOT/scripts/install-crawlctl.sh" v0.14.1 "$INSTALL_DIR" >/dev/null 2>&1; then - fail "archive with extra entries was accepted" -fi -[[ "$("$INSTALL_DIR/crawlctl" --version)" == 0.14.0 ]] || fail "failed update changed the installed executable" -if bash "$ROOT/scripts/verify-crawlctl-release.sh" v0.14.1 "$MOCK_COMMIT" \ - "$BAD_ASSETS/$bad_asset" >/dev/null 2>&1; then - fail "verifier accepted an archive with extra entries" -fi - -grep -F -- '--identifier org.openclaw.crawlctl' "$MOCK_CODESIGN_LOG" >/dev/null || fail "missing fixed identifier" -grep -F "$EXPECTED_TEAM_ID" "$MOCK_CODESIGN_LOG" >/dev/null || fail "missing fixed Team ID requirement" -echo "crawlctl release script tests passed" +echo "crawlctl release contract tests passed" diff --git a/scripts/verify-crawlctl-release-provenance.sh b/scripts/verify-crawlctl-release-provenance.sh index 658a8b6..74ac6b9 100755 --- a/scripts/verify-crawlctl-release-provenance.sh +++ b/scripts/verify-crawlctl-release-provenance.sh @@ -78,13 +78,8 @@ git -C "$ROOT" \ tag_commit=$(git -C "$ROOT" rev-parse "$tag_ref^{commit}") branch_commit=$(git -C "$ROOT" rev-parse "$branch_ref^{commit}") -head_commit=$(git -C "$ROOT" rev-parse HEAD) -[[ "$tag_commit" == "$branch_commit" ]] || { - echo "remote release tag does not target the protected $DEFAULT_BRANCH head: $VERSION" >&2 - exit 1 -} -[[ "$head_commit" == "$tag_commit" ]] || { - echo "local HEAD does not match the verified remote release commit: $VERSION" >&2 +git -C "$ROOT" merge-base --is-ancestor "$tag_commit" "$branch_commit" || { + echo "remote release tag is not reachable from protected $DEFAULT_BRANCH: $VERSION" >&2 exit 1 } diff --git a/scripts/verify-crawlctl-release.sh b/scripts/verify-crawlctl-release.sh index f350425..6540753 100755 --- a/scripts/verify-crawlctl-release.sh +++ b/scripts/verify-crawlctl-release.sh @@ -26,18 +26,6 @@ verify_designated_requirement() { } } -verify_thin_architecture() { - local binary=$1 expected_arch=$2 architecture_output - local -a slices - architecture_output=$(lipo -archs "$binary") - architecture_output=${architecture_output//$'\n'/ } - read -r -a slices <<<"$architecture_output" - if [[ "${#slices[@]}" -ne 1 || "${slices[0]}" != "$expected_arch" ]]; then - echo "crawlctl must contain exactly one $expected_arch architecture slice: $binary" >&2 - return 1 - fi -} - verify_build_provenance() { local binary=$1 buildinfo buildinfo=$(go version -m "$binary") @@ -60,33 +48,31 @@ verify_build_provenance() { } verify_checksum() { - local archive_path=$1 checksum_path=$2 expected_hash expected_name extra actual_hash - [[ "$(wc -l < "$checksum_path" | tr -d ' ')" == 1 ]] || { - echo "invalid checksum file: $checksum_path" >&2 - return 1 - } - read -r expected_hash expected_name extra < "$checksum_path" - [[ "$expected_hash" =~ ^[[:xdigit:]]{64}$ && "$expected_name" == "$(basename "$archive_path")" && -z "${extra:-}" ]] || { - echo "invalid checksum record: $checksum_path" >&2 + local archive=$1 checksum_file=$2 name matches expected_hash actual_hash + name=$(basename "$archive") + matches=$(awk -v name="$name" '$2 == name || $2 == "*" name { print $1 }' "$checksum_file") + [[ -n "$matches" && "$matches" != *$'\n'* && "$matches" =~ ^[[:xdigit:]]{64}$ ]] || { + echo "checksums file must contain exactly one valid record for $name" >&2 return 1 } - actual_hash=$(shasum -a 256 "$archive_path" | awk '{print $1}') + expected_hash=$matches + actual_hash=$(shasum -a 256 "$archive" | awk '{print $1}') [[ "$actual_hash" == "$expected_hash" ]] || { - echo "checksum mismatch: $archive_path" >&2 + echo "checksum mismatch: $archive" >&2 return 1 } } -if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ || +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ || ! "$EXPECTED_COMMIT" =~ ^[0-9a-f]{40}$ || "$#" -eq 0 ]]; then - echo "usage: $0 vX.Y.Z EXPECTED-COMMIT crawlctl-vX.Y.Z-macos-ARCH.tar.gz [...]" >&2 + echo "usage: $0 X.Y.Z EXPECTED-COMMIT crawlkit_X.Y.Z_PLATFORM_ARCH.tar.gz [...]" >&2 exit 2 fi [[ "$(uname -s)" == Darwin ]] || { - echo "crawlctl macOS signature verification must run on macOS" >&2 + echo "crawlctl release verification must run on macOS" >&2 exit 1 } -for tool in codesign csreq env go lipo shasum tar; do +for tool in awk codesign csreq env file go lipo sed shasum tar; do command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2 exit 1 @@ -99,43 +85,64 @@ trap 'rm -rf "$WORK_DIR"' EXIT for archive in "$@"; do archive=$(cd "$(dirname "$archive")" && pwd)/$(basename "$archive") - checksum="$archive.sha256" - [[ -f "$archive" && -f "$checksum" ]] || { - echo "missing artifact or checksum: $archive" >&2 + checksum_file="$(dirname "$archive")/checksums.txt" + [[ -f "$archive" && -f "$checksum_file" ]] || { + echo "missing artifact or checksums.txt: $archive" >&2 exit 1 } + platform= + expected_arch= case "$(basename "$archive")" in - "crawlctl-${VERSION}-macos-arm64.tar.gz") expected_arch=arm64 ;; - "crawlctl-${VERSION}-macos-x86_64.tar.gz") expected_arch=x86_64 ;; + "crawlkit_${VERSION}_darwin_arm64.tar.gz") platform=darwin; expected_arch=arm64 ;; + "crawlkit_${VERSION}_darwin_amd64.tar.gz") platform=darwin; expected_arch=x86_64 ;; + "crawlkit_${VERSION}_linux_arm64.tar.gz") platform=linux; expected_arch=arm64 ;; + "crawlkit_${VERSION}_linux_amd64.tar.gz") platform=linux; expected_arch=amd64 ;; *) - echo "unexpected crawlctl artifact name: $(basename "$archive")" >&2 + echo "unexpected crawlkit artifact name: $(basename "$archive")" >&2 exit 1 ;; esac - verify_checksum "$archive" "$checksum" - [[ "$(tar -tzf "$archive")" == crawlctl ]] || { + verify_checksum "$archive" "$checksum_file" + members=$(tar -tzf "$archive" | sed 's#^\./##; /^$/d') + [[ "$members" == crawlctl ]] || { echo "release archive must contain only crawlctl: $archive" >&2 exit 1 } - stage="$WORK_DIR/$expected_arch" + stage="$WORK_DIR/${platform}-${expected_arch}" mkdir -p "$stage" + tar -xzf "$archive" -C "$stage" binary="$stage/crawlctl" - tar -xOf "$archive" crawlctl > "$binary" chmod 0755 "$binary" + file_output=$(file -b "$binary") - codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$binary" - codesign --verify --strict --check-notarization -R=notarized --verbose=2 "$binary" - signature=$(codesign -dvvv "$binary" 2>&1) - grep -Fx "Identifier=$IDENTIFIER" <<<"$signature" >/dev/null - grep -Fx "TeamIdentifier=$EXPECTED_TEAM_ID" <<<"$signature" >/dev/null - grep -Fx "Authority=$EXPECTED_AUTHORITY" <<<"$signature" >/dev/null - grep -F '(runtime)' <<<"$signature" >/dev/null - verify_designated_requirement "$binary" - verify_thin_architecture "$binary" "$expected_arch" - verify_build_provenance "$binary" + if [[ "$platform" == darwin ]]; then + [[ "$file_output" == *"Mach-O 64-bit executable"* && "$file_output" == *"$expected_arch"* ]] || { + echo "unexpected Mach-O architecture for $archive: $file_output" >&2 + exit 1 + } + codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$binary" + codesign --verify --strict --check-notarization -R=notarized --verbose=2 "$binary" + signature=$(codesign -dvvv "$binary" 2>&1) + grep -Fx "Identifier=$IDENTIFIER" <<<"$signature" >/dev/null + grep -Fx "TeamIdentifier=$EXPECTED_TEAM_ID" <<<"$signature" >/dev/null + grep -Fx "Authority=$EXPECTED_AUTHORITY" <<<"$signature" >/dev/null + grep -F '(runtime)' <<<"$signature" >/dev/null + verify_designated_requirement "$binary" + [[ "$(lipo -archs "$binary")" == "$expected_arch" ]] + elif [[ "$expected_arch" == arm64 ]]; then + [[ "$file_output" == *"ELF 64-bit"* && "$file_output" == *"ARM aarch64"* ]] || { + echo "unexpected Linux arm64 binary: $file_output" >&2 + exit 1 + } + else + [[ "$file_output" == *"ELF 64-bit"* && "$file_output" == *"x86-64"* ]] || { + echo "unexpected Linux amd64 binary: $file_output" >&2 + exit 1 + } + fi - [[ "$(env -i PATH=/usr/bin:/bin "$binary" --version)" == "${VERSION#v}" ]] + verify_build_provenance "$binary" done