diff --git a/.github/release-allowed-signers b/.github/release-allowed-signers new file mode 100644 index 00000000..f3564264 --- /dev/null +++ b/.github/release-allowed-signers @@ -0,0 +1,3 @@ +# Maintainer and fleet release-tag signing keys. Public keys only. +steipete@gmail.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINJpNX7Qza7z78FxujtboBJZC73Co1cgWJ5xrFXaek+W +steipete@gmail.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9XsaCcr8TInPnHcuTVfvXXcsoUFrOE7menfbEIHFW9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..0e11552f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + version: + description: Version with a dated CHANGELOG section (X.Y.Z) + required: true + type: string + dry_run: + description: Validate, build, and verify without publishing + default: true + type: boolean + +permissions: {} + +concurrency: + group: release-${{ github.repository }}-${{ inputs.version || github.ref_name }} + cancel-in-progress: false + +jobs: + release: + permissions: + actions: read + checks: read + contents: write + statuses: read + uses: openclaw/release-workflows/.github/workflows/release-source.yml@v1 + with: + version: ${{ inputs.version || github.ref_name }} + dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + require-signed-tag: true + strict-checks: true + ci-check-events: '["push"]' + source-archives: true + test-command: >- + npx --yes --package=node@24.15.0 --package=pnpm@11.28.4 + --call 'bash scripts/verify-release.sh' + release-name: ClawRouter diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7864ac..c027a711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add verified, signed-tag GitHub source releases with a no-publish rehearsal; keep service deployment separately dispatched. + - Select scoped inference grants in the admission transaction using fenced, secretless authority projections; keep credential-owner materialization authoritative for revocation and retain legacy projection fallback. - Collapse proxy-key admission into one canonical authority read and preserve atomic grant selection, migration fences and revocation checks; reuse compiled discovery metadata and instrument local authority RPC counts. diff --git a/README.md b/README.md index 823a033d..c18ec3a2 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ pnpm check `pnpm build` compiles the provider snapshot and admin UI, then typechecks the Worker. `pnpm check` runs Worker, admin, and script tests. +Numbered GitHub source releases use the [release workflow](RELEASING.md), independently of service deployments. + ## License MIT. See [LICENSE](LICENSE). diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..e58e0113 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,58 @@ +# Releasing ClawRouter + +Numbered releases publish the versioned source on GitHub. The package is private; +there is no npm publication, Homebrew handoff, or application-signing credential. +Deployment is separate from release publication. + +## Prepare + +1. Update `package.json` and any lockfile root version for the next release. +2. Finalize one `## X.Y.Z - YYYY-MM-DD` section in `CHANGELOG.md`, preserving + contributor credit and release highlights. Keep an empty `## Unreleased` above it. +3. Land the reviewed release preparation PR on protected `main`. Wait for every + independent push CI check on that exact merged commit to pass. + +## No-publish proof + +Run the **Release** workflow on current `main`, with the version from `package.json` +and `dry_run=true` (the dispatch default): + +```sh +gh workflow run release.yml --repo openclaw/clawrouter --ref main -f version=X.Y.Z -F dry_run=true +``` + +A rehearsal freezes current protected `main`, even if that version already has a +historical tag or release. It validates the version and dated changelog, requires +exact-commit push CI, runs `scripts/verify-release.sh` with Node.js 24.15.0, assembles +source archives, and independently verifies their inventory and SHA-256 checksums. +It creates no tag, draft, release, or deployment. No secrets are needed. + +The local verification script only installs dependencies, builds, and tests. Run +it with Node.js 24.15+ and the repository's package manager. It does not publish. + +## Publish an explicitly authorized release + +After the rehearsal succeeds, create an SSH-signed annotated `vX.Y.Z` tag on the +verified commit using a key listed in `.github/release-allowed-signers`. Verify it +locally and push the tag. The tag push is the publication entrypoint; do not run +`gh release create` or a local publish helper. + +`.github/workflows/release.yml` calls +`openclaw/release-workflows/.github/workflows/release-source.yml@v1`. It verifies +the signed tag, protected-branch ancestry, exact version metadata, dated notes, +and green push CI, then builds and verifies the frozen source before publishing. +Live dispatches also require a pre-existing signed tag; an unsigned workflow-created +tag cannot satisfy this repository's policy. Never move tags or republish a version. + +The GitHub Release contains the exact changelog section, commit-based `.tar.gz` +and `.zip` source archives, `ASSET-INVENTORY.json`, and `SHA256SUMS`. The two source +verifiers run without publication credentials. The publisher checks the frozen +inventory and tag again before publishing. Read back the release and verify the +checksums and tag commit after completion. Leave `main` current and clean. + +## Deployment boundary + +Cloudflare production and FakeCo deployments remain in their existing explicit +`workflow_dispatch` workflows with their `expected_sha` checks. The release proof +renders a local config and uses Wrangler `--dry-run` only; it sends no deployment. +Docker/self-hosting remains source-based, with no new image registry publication. diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh new file mode 100755 index 00000000..a04ec9d1 --- /dev/null +++ b/scripts/verify-release.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm install --frozen-lockfile +pnpm build +pnpm --dir admin check +pnpm --dir admin test +pnpm test:scripts +pnpm worker:e2e +pnpm provider:smoke-plan +# Render and pack locally; neither command sends a deployment. +CLAWROUTER_STRICT_CONFIG=0 pnpm cf:config +build_dir="$(mktemp -d)" +trap 'rm -rf "$build_dir"' EXIT +WRANGLER_SEND_METRICS=false pnpm exec wrangler deploy --dry-run --outdir "$build_dir" --config .wrangler.generated.toml +test -s "$build_dir/index.js"