Skip to content

About

Reusable release workflows for the OpenClaw/steipete fleet: uniform draft → verify → publish pipelines per archetype (go-cli, npm-cli, sparkle-app, electron, web)

Resources

Contributing

Security policy

Stars

9 stars

Watchers

0 watching

Forks

Latest commit

 

History

104 Commits

Folders and files

Repository files navigation

OpenClaw release workflows

Fleet-standard reusable release pipelines. Every archetype follows one trust boundary:

  1. freeze a protected-branch commit, reusing an immutable annotated tag on retries;
  2. build and freeze an immutable same-run artifact payload, staging an unpublished draft where the archetype requires it;
  3. independently verify that artifact on arm64 and x86_64 without signing or release-write credentials, emitting the exact verified ASSET-INVENTORY.json, configured checksum asset (SHA256SUMS by default), plus a verdict;
  4. let the publisher require exact name and digest equality with both verifier attestations before publication, re-downloading staged draft assets before un-drafting.

All archetypes reject an unprotected default branch. Only a missing tag ref starts a release from the protected head; a missing or unreadable annotated-tag object fails validation without exporting a build target.

After binding the draft assets to both verifier attestations, every publisher rechecks that the release tag is still annotated and points directly to the frozen commit. A missing, unreadable, moved, or lightweight replacement tag blocks publication. The draft must also still name the expected version tag.

The configurable checksum filename must be distinct from release controls and payload filenames, including case aliases. Swift rejects collisions with its configured archives during input validation; Electron also checks caller-produced asset names during assembly, before writing the manifest.

All archetypes reject checksum filenames ending in a dot during input validation because GitHub renames those assets on upload.

Callers pin the stable @v1 compatibility tag, never a branch or pre-release tag:

jobs:
  release:
    uses: openclaw/release-workflows/.github/workflows/release-go-cli.yml@v1

Go CLI no-publish rehearsals

The Go archetype accepts dry-run: true. It freezes protected main, validates release metadata and CI, builds, signs, notarizes, and runs both independent verifiers using retained Actions artifacts. It never creates a tag, draft, GitHub Release, tap dispatch, or closeout PR. Supply a dated changelog version; existing historical tags do not redirect the rehearsal away from current main.

Tag-push callers can require signed tags and set ci-check-events: '["push"]' to require push CI on the frozen source. Real releases reject existing GitHub Releases before building. The workflow reuses an existing annotated tag and never replaces it.

npm package archetype

release-npm-package.yml releases one package or an ordered monorepo package list. It uses a protected frozen commit, the existing dated-changelog and exact-commit CI gate, an immutable same-run payload, and two write-free Linux verifiers. The publisher never checks out caller source or runs package lifecycle scripts. It publishes only the verified .tgz bytes with GitHub OIDC trusted publishing, then independently verifies registry integrity, Sigstore signatures, and provenance binding the tarball to the caller workflow and frozen commit before publishing the GitHub Release.

Use examples/release-npm-package-caller.yml. The caller grants id-token: write; no NODE_AUTH_TOKEN or npm token secret is accepted. Configure npm trusted publishing for the caller filename, for example release.yml, not this repository's release-npm-package.yml. Every package must already exist in npm and have its own trusted publisher; newly reserved names are outside this workflow.

Configure trust right before the first release: an unused trusted-publisher record expires after 2 days without a successful OIDC publish and can no longer be used or edited. If a record is older than 2 days and has never published successfully, delete the expired record, recreate it with the same repository and caller workflow filename, and complete the first publish within 2 days. An expired record can surface as 404 OIDC token exchange error - package not found or ENEEDAUTH even when the package exists. The publisher preflights OIDC exchange for every unpublished package before any publish attempt and names the package and recovery steps on failure; it never retries publication. Already-published versions retain the exact-byte resume checks below.

Input Contract
version Release SemVer, optionally prefixed by v; every package has this exact version.
dry_run Defaults to true; validates, builds, packs, installs, and smoke-tests without creating tags or publishing. Callers explicitly pass false for tag pushes.
node-version Build and smoke Node version; defaults to 24. OIDC publication and provenance verification use Node 24 and its modern npm CLI.
package-manager / package-manager-version npm, pnpm, or bun; pnpm and bun require an exact manager version.
package-dirs JSON array, default ["."]; dependency-first order, unique names and archive filenames, no symlinked directories.
install-command Optional install override. Defaults to npm ci, pnpm install --frozen-lockfile, or bun install --frozen-lockfile.
build-command / test-command Credential-free caller commands; build defaults to npm run build --if-present, tests default empty.
smoke-command Required command run outside the source checkout after installing all frozen tarballs on clean x64 and arm64 runners. Install scripts are disabled.

Packing uses npm pack for all three build managers, including its lifecycle scripts. Script output is streamed to the build log; tarball identity comes from the frozen package plan and independent manifest checks. Prepare publishable manifests before release: sibling dependencies and peer dependencies must use exact versions; runtime dependencies must precede dependents in package-dirs. Both source and packed manifests are checked. Packed manifests may not leak workspace:, file:, or link: references, including development dependencies. Loose peer ranges are deliberately rejected for siblings. All packages must be public and use the npm registry.

Stable releases publish with latest, prereleases with next. Visibility is polled for up to ten minutes per package, with a job summary when waiting exceeds one minute. Downloads bind to producer-exported immutable artifact IDs; recreating an artifact under the same name cannot replace validation metadata or payloads. Packed publishConfig accepts only public access, the npm registry, and provenance enabled. An existing version resumes only when its registry integrity and downloaded tarball bytes equal the frozen payload; it is never republished. A partial multi-package publish is recoverable by dispatching the caller on the immutable version tag (--ref vX.Y.Z) with dry_run=false. New npm publication requires GITHUB_SHA to equal the frozen commit; a newer-main dispatch may verify already published bytes but cannot publish missing siblings with incorrect provenance. If rebuilding produces different bytes, stop and investigate; existing registry versions cannot be overwritten. Provenance has its own ten-minute visibility allowance per package. The GitHub Release contains all tarballs, exact changelog notes, the package plan, inventory, and SHA256SUMS.

Source-only archetype

Source catalogs with an established unprefixed tag convention may set tag-prefix: ''; the default remains v. Go proxy releases require the default v prefix. release-name optionally preserves project-branded titles such as Agent Scripts 0.12.0; without it the title remains the tag. Neither option changes version metadata validation or permits moving an existing tag.

release-source.yml fits SwiftPM libraries, Go libraries, skill catalogs, and this repository itself. It validates version metadata, a real calendar date in one unambiguous changelog section, protected-branch ancestry, and green independent push CI at the frozen commit. An optional test-command runs without write credentials. test-runs-on selects its GitHub-hosted runner (default ubuntu-latest); macOS-only SwiftPM libraries select xcode-27 and use the image’s Xcode 27. Existing v1 macOS runner inputs are routed to xcode-27. Keep the package platforms and build deployment targets explicit. The validation, archive assembly, and two independent verifier jobs remain on Linux. Two fresh runners verify the same-run inventory, checksums, source identity, and exact notes before publishing a GitHub Release.

Use examples/release-source-caller.yml. source-archives: true attaches GitHub's tar and zip archives of the frozen commit, with their SHA-256 checksums; the release page's automatic tag archives remain GitHub-managed. go-modules: '["."]' additionally warms proxy.golang.org, uses a fresh module cache and sum.golang.org, and verifies the downloaded module version, checksum, and source commit. Submodules require their existing annotated <directory>/vX.Y.Z tags to point at the same commit; this workflow never invents or moves those tags. Go module path major suffixes must agree with the release version. Proxy warming is skipped during dry runs.

Both new archetypes accept require-signed-tag, strict-checks, and ci-check-events; the default event filter is ["push"]. They accept an existing annotated tag on protected-branch ancestry, or create one from protected main on a live dispatch. Set require-signed-tag: true to require an existing SSH-signed tag verified against .github/release-allowed-signers from the frozen source. Neither workflow moves version tags or overwrites releases. Dispatch callers must run on the protected default branch's current head; exact tag-push callers are also supported. A dry run defaults to true and never publishes. A dry-run dispatch on the protected default branch always validates and builds that exact branch head, even if the requested version already has a tag; it neither reads nor modifies that unrelated tag, and signed-tag verification is deferred to a live release. A tag-triggered dry run still validates the tag and its signature when required.

Optional homebrew-formula and homebrew-tap inputs dispatch update-formula.yml using TAP_TOKEN, a unique request_id, and the exact release-asset URL. The workflow waits for that run and checks the literal stable formula URL and SHA-256. Source releases require source-archives: true; npm handoff uses the last package in dependency order. This mode supports single-URL source/npm formulas, not platform-matrix binary formulas. The tap workflow must support artifact_url and request_id with the request ID in its run title, as the fleet taps do.

See RELEASING.md for this repository's no-publish proof, release publication, and signed v1 compatibility-tag update.

Ruleset workflows

crabbox-release-check.yml is an organization-ruleset workflow for openclaw/crabbox. GitHub loads the workflow from this repository's protected default branch, then runs the credential-free macOS release snapshot against the target pull request merge commit. A Crabbox pull request therefore cannot rewrite the required release check that gates its own merge.

Target only openclaw/crabbox, bind the rule to this workflow on main, and configure no bypass actors. The workflow source paths are CODEOWNED by @vincentkoc and @steipete; the source repository must require a non-bypassable code-owner approval before changing them. Keep the ordinary Crabbox CI workflow separate; the ruleset workflow is the independent trust anchor for release-build proof.

SwiftPM CLI archetype

release-swift-cli.yml applies the same frozen-source, workflow-owned-tag, immutable-payload, dual-verifier, and exact-publication boundary to SwiftPM command-line tools. Its default contract matches imsg: the caller's version-generation and dependency-patch scripts prepare SwiftPM, the macOS build script emits a universal CLI, an arm64e/arm64/x86_64 companion dylib, and resource bundles, while the Linux script emits a static-Swift x86_64 archive.

Set dry_run: true to rehearse the exact current protected main commit, even when the requested version already has a historical tag. Validation, signing, notarization, and both write-free verifiers run from retained Actions artifacts; tag creation, draft creation, publication, Homebrew handoff, and closeout are skipped. Apple still receives the candidate ZIP for notarization. Existing callers retain dry_run: false; dispatch callers should default their user-facing input to true.

The Linux container explicitly trusts only its exact GITHUB_WORKSPACE checkout for Git operations before dependency preparation. The Package.resolved drift check remains required; unrelated repositories are not trusted.

include-helper: false removes the companion dylib from build requirements, signing, the signing manifest, and both verifiers. include-linux: false skips the Linux build/download and omits its archive from the inventory and checksums. Both default to true for existing imsg callers. Set require-resource-bundles: false for CLIs without SwiftPM resources. Disabled helper/Linux names and identifiers may be empty.

macos-runner and verify-arm64-runner default to xcode-27; xcode-27-xlarge is available for callers that need larger runners. Existing v1 macOS labels remain accepted as aliases for xcode-27. minimum-macos-version defaults to 14 and sets MACOSX_DEPLOYMENT_TARGET for the build; keep Swift package platforms and any explicit build-script targets aligned with it. Both verifiers run on macOS 27: arm64 natively and x86_64 explicitly under Rosetta. This proves execution on macOS 27, not runtime compatibility on the minimum supported OS. Empty prepare-script, generate-version-script, and entitlements-file disable those optional steps. An empty version-file requires metadata-check-script, a tracked credential-free executable called with vX.Y.Z during Linux validation; it must validate the caller's version metadata. macos-build-script must emit an executable into OUTPUT_DIR; a thin adapter may call an existing local universal builder.

For signed-tag release callers, set require-signed-tag: true and ci-check-events: '["push"]'. Live runs require an existing annotated SSH-signed version tag verified against .github/release-allowed-signers at the frozen commit, reachable from protected main. Exact version tag pushes are supported; a dry run never looks up that tag. Live attempts reject an existing GitHub Release before building. Signed-tag mode never creates a missing tag. Callers may expose dispatch rehearsals only and reserve live runs for authorized tag pushes.

The signing job never checks out or executes caller source. It re-signs the frozen raw macOS payload with the selected Developer ID policy, preserves stable CLI/helper identifiers, submits the exact published ZIP for notarization, requires online notarization on both Mach-O files, binds the macOS ZIP and Linux archive into ASSET-INVENTORY.json plus the configured checksum manifest, and uploads one immutable Actions payload. Independent arm64 and Rosetta x86_64 jobs verify checksums, inventory identity, CLI/helper slices, resource bundles, identifiers, Team ID, notarization, architecture-specific CLI execution, and the Linux executable format without release-write credentials. Publication re-downloads every draft asset and binds its bytes to both attestations before undrafting.

The signer snapshots the user keychain search list before creating its ephemeral keychain. Always-run cleanup restores that list and deletes the temporary keychain even if import fails before signer outputs are available; restoration and deletion failures are reported independently.

Draft creation resolves the GitHub repository explicitly and downloads only the immutable signed payload into a fresh job workspace. It does not check out caller source, so tracked files named release-assets/* cannot contaminate the draft uploads.

Swift and Electron retain the release ID returned by draft creation and upload assets through that response's validated uploads.github.com URL. An older draft with the same tag cannot replace the ID passed to publication. Failed uploads do not export a draft ID.

When homebrew-formula is nonempty, the handoff dispatches the configured tap's update-formula.yml with the exact verified macOS archive, waits for the uniquely correlated run, then requires the resulting formula URL and SHA-256 to equal the attested release asset. A pre-existing versioned Unreleased changelog section satisfies closeout; otherwise the workflow opens a closeout PR.

See examples/release-swift-cli-caller.yml for the thin caller. The consumer must provision MACOS_SIGNING_P12, MACOS_SIGNING_P12_PASSWORD, ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY_P8; Homebrew handoff additionally needs TAP_TOKEN.

Sparkle macOS app archetype

release-sparkle-app.yml releases a universal macOS app with Sparkle updates, a Finder-layout DMG, and optional standalone CLI and npm archives. Caller code runs only in credential-free build and npm packaging jobs. The signer uses workflow-owned code, hash-pinned tools, frozen data files, and unsigned build artifacts; it never checks out or runs caller source. See examples/release-sparkle-app-caller.yml.

The job graph is validate → build → sign → package → tag → draft → verify → publish → handoff → closeout. Validation freezes a protected default-branch commit, or reuses an existing annotated version tag reachable from that branch, and extracts exactly one ## X.Y.Z - YYYY-MM-DD changelog section. Signing approval and successful signing precede every tag, draft, and publication write. Three independent jobs verify the same immutable Actions payload with only actions: read; the publisher independently downloads every draft asset, compares its names and bytes against all three attestations, rechecks the annotated tag, and only then publishes the creation-bound release ID. Alpha, beta, and rc versions are explicitly marked as GitHub prereleases.

Environment and caller setup

Create the caller repository's environment before dispatch, normally named release. Configure steipete as the required reviewer, disable administrator bypass, and select deployment branches and tags with two separate policies: the exact default branch (normally main) and tags matching v*. Keep the default branch protected. The environment rule applies to the triggering run's ref; this archetype requires dispatch from the current protected default-branch head, even when reusing an older annotated tag. The tag policy does not enable dispatching this archetype from a tag.

The sign job declares environment: ${{ inputs.signing-environment }}. Its job and secret access wait for environment approval. Provision all six secrets in that environment, and explicitly pass all six names from the caller:

Secret Required value
MACOS_SIGNING_P12 Base64 password-protected Developer ID Application certificate/private-key export matching repository-type.
MACOS_SIGNING_P12_PASSWORD Password for that export.
ASC_KEY_ID Apple notarization API key ID.
ASC_ISSUER_ID Matching issuer ID.
ASC_PRIVATE_KEY_P8 Full raw PEM contents of the API private key.
SPARKLE_PRIVATE_KEY Base64 32-byte Ed25519 seed exported by Sparkle generate_keys -x; never a filesystem path.

GitHub requires a caller secret mapping even for environment-only secrets. secrets: inherit is unnecessary; explicit mappings avoid passing unrelated secrets. Required declarations validate that the mapping exists; a value-presence check runs inside the approved signing job. Environment values override same-named repository/organization values. Keep release credentials environment-only so other workflows cannot bypass this policy. These semantics are documented in GitHub's reusable workflow guide.

Only one required reviewer must approve. If Peter dispatches and is the sole reviewer, leave prevention of self-review disabled; otherwise another eligible reviewer must dispatch or approve. Required-reviewer availability depends on repository visibility and plan. Environment names can be created automatically without protection, so an absent or misspelled environment is not an approval policy. See GitHub deployment protection rules. The caller needs actions: write, checks: read, contents: write, statuses: read, and pull-requests: write, narrowed by each called job. Enable Actions PR creation as described in the Go CLI Migration prerequisites section for closeout.

Inputs and build contract

Input Contract/default
version Required X.Y.Z or `X.Y.Z-(alpha
repository-type Required openclaw or personal; same identities and Team IDs as the other archetypes.
signing-environment release; must already have the reviewer and deployment policies above.
build-runner xcode-27; xcode-27-xlarge selects a larger runner. Existing v1 macOS labels route to xcode-27. Use fresh GitHub-hosted runners.
xcode-path Empty uses the image’s Xcode 27; otherwise select an installed Xcode 27 under /Applications/Xcode…app/Contents/Developer. Sets DEVELOPER_DIR for build/sign.
checksum-filename checksums.txt; safe filename distinct from every payload/control, ignoring case.
ci-check-events ["push","pull_request"]; filters Actions checks, never commit statuses. All independent eligible signals must be green.
app-name Required safe bundle basename without .app; produces <app-name>-<version>.app.zip and .dmg.
bundle-identifier Required stable app signing identifier.
minimum-system-version Required numeric macOS version at most 15.0, exactly matching LSMinimumSystemVersion.
app-build-script Required tracked executable, run from repository root with recursive submodules at the frozen SHA. Must produce only dist/sparkle-app/<app-name>.app, with an unsigned outer seal and universal arm64/x86_64 executable.
app-entitlements Required tracked plist path; only the main app receives these entitlements.
sparkle-public-key Required canonical base64 32-byte Ed25519 public key, exactly matching SUPublicEDKey.
appcast-feed-url Required HTTPS URL, exactly matching SUFeedURL.
appcast-path appcast.xml; tracked source feed and closeout destination.
sparkle-version 2.10.0; only versions with a workflow-owned release-archive SHA-256 entry are accepted.
dmg-background Required tracked PNG path, with pixel dimensions equal to the layout window.
dmg-layout Required data-only JSON schema below. No caller Python/settings/hooks run.
cli-build-script Empty disables standalone CLI; otherwise emits only the binary and companion .dylib files in dist/sparkle-cli/.
cli-binary-name, cli-identifier, cli-archive-prefix All required together with the CLI script; safe names and stable signing identifier.
cli-smoke-args []; JSON argv array, e.g. ["--help"], invoked after --version on every verifier.
npm-pack-script Empty disables npm packaging; otherwise requires the CLI and emits exactly one dist/sparkle-npm/*.tgz.
npm-publish-workflow Empty disables dispatch; otherwise caller workflow filename accepting version, requiring npm packaging.
homebrew-workflow Empty disables dispatch; otherwise caller workflow filename accepting version.
swift-runtime-audit true; on macOS 26, audit all app/CLI Mach-O slices against the oldest eligible installed SDK older than 27, following reexports.
dry-run false; true performs real signing, notarization, packaging, and three-verifier proof, skipping tag/draft/publish/handoffs/closeout.

CFBundleShortVersionString must equal the normalized version. CFBundleVersion must be a positive integer greater than every existing appcast build. The feed must have a single RSS channel and unique versions. Historical build numbers must not increase down the feed, but equal historical builds are preserved for apps that adopted monotonic numbering after their first releases. Every new build must strictly increase; a duplicate release version fails rather than rewriting release history. The generated item includes the child <sparkle:minimumSystemVersion> element, escaped metadata, signature, length, release link, and release-notes link. Alpha, beta, and rc versions also receive the matching <sparkle:channel>; stable versions have no channel. Stable clients therefore do not receive prereleases. Clients opting into previews must explicitly allow the corresponding Sparkle channel. DTDs are rejected.

Build/sign default to the GA xcode-27 label. Three independent verifiers run on xcode-27: arm64, x86_64 under Rosetta, and an arm64 Swift runtime audit. Verifier IDs, separate from runner labels, bind the three attestations without artifact-name collisions. GitHub announced the GA Xcode 27 image on October 15, 2026. The build sets MACOSX_DEPLOYMENT_TARGET from minimum-system-version; caller Xcode settings must preserve that same minimum. Build and sign record Xcode, Swift, and SDK versions in SIGNING-MANIFEST.json. The runtime audit fails closed when no eligible older SDK exists or a tbd format cannot be parsed. It rejects strong _swift_initBorrow imports, checks strong Swift runtime imports per architecture, and validates packaged @rpath companions using the executable’s inherited run paths before native CLI execution. Actual Mach-O deployment targets must not exceed the declared minimum. Each slice must declare a macOS minimum; zippered libraries may also declare Mac Catalyst minima, which are ignored because they use iOS version numbers. Other platforms and Catalyst-only slices are rejected.

DMG, CLI, and npm packaging

The complete DMG layout schema is:

{"window-size":[720,460],"icon-size":128,"app-position":[180,230],"applications-position":[540,230],"volume-name":"Example"}

Unknown keys are rejected. The window dimensions are integers from 200 through 4096, icon size is 16 through 512, and both icon positions must lie inside the window. Volume names have 1–80 characters and cannot contain path separators, colons, or control characters. Workflow-owned dmgbuild 1.6.7, ds-store 1.3.3, and mac-alias 2.2.3 wheels are installed with SHA-256 requirements. Finder contains the app, background, and /Applications link; the app's declared .icns icon is used when present. Both the app and DMG are independently signed, notarized, stapled, and assessed. Verifiers compare the DMG's app tree against the ZIP including file bytes, modes, and symlink targets.

The standalone CLI and dylibs must initially be ad-hoc signed universal Mach-O files; --version must report the requested version. The signer replaces their signatures, notarizes the universal payload, then uses lipo -thin without re-signing. It emits <prefix>-universal.tar.gz, <prefix>-arm64.tar.gz, and <prefix>-x86_64.tar.gz. Each contains one <prefix>-<arch>/ directory with the executable, companions, tracked LICENSE, generated README.md, and VERSION. Verification checks the thin archives against slices of the signed universal files and runs the native CLI on every runner.

The npm script runs from the frozen checkout with the signed universal CLI, companions, and archive documentation staged at dist/sparkle-cli/. It must install its own required packaging dependencies and emit exactly one .tgz under dist/sparkle-npm/. Both packaging and independent verification require exactly one executable with cli-binary-name inside the npm package, byte-identical to the signed universal binary, with byte-identical adjacent companion libraries. Symlinks, hardlinks, duplicate archive members, and unsafe paths are rejected. Signed assets are restored from the immutable signer artifact after caller packaging. Each verifier independently downloads the original signer artifact and frozen source inputs again, compares every signed asset and the release notes byte-for-byte, and regenerates the appcast from the frozen feed. Caller packaging cannot replace those trust anchors. Draft, verifier, and closeout jobs validate the transport as flat regular files and unpack it into a separate directory before reading it.

The Sparkle private seed exists only in a mode-0600 temporary file. Its derived public key must match sparkle-public-key before sign_update signs anything, and the ZIP signature is verified independently with that public key. Legacy expanded Sparkle key exports are not accepted; export a supported 32-byte seed before migration. Never rotate an existing app's update identity merely to satisfy a format check.

Downloader entitlements are extracted from the frozen app before signing, compared across its arm64/x86_64 signatures, and recorded as sorted XML in SIGNING-MANIFEST.json. The signer explicitly reapplies that record to both Downloader.xpc and its main executable, including ad-hoc inputs; all three independent verifiers require exact entitlement equality on both slices. A Downloader without entitlements keeps an empty entitlement dictionary. When present, com.apple.security.app-sandbox must remain true. Other helpers keep their existing signing behavior without added entitlements. This matches the optional DOWNLOADER_SANDBOXED_ENTITLEMENTS configuration in Sparkle 2.10.0 and Sparkle's signing guide for 2.6 and later: preserve Downloader entitlements, and sign Installer, Updater, and Autoupdate with hardened runtime without adding entitlements.

Handoffs, closeout, and limitations

After publishing, the workflow dispatches npm-publish-workflow and then homebrew-workflow, waiting up to 15 minutes for each. Keep consumers' established trusted publishers (for example npm-publish.yml) and accept the normalized version input. These workflows obtain their own credentials/OIDC permissions; the reusable signer never passes Apple or Sparkle secrets to them. Explicit dispatch is necessary because publishing with GITHUB_TOKEN does not trigger ordinary release: published workflows. Correlation uses a pre-dispatch run snapshot, exact default-branch SHA, dispatch event, and token actor; concurrent identical dispatches are rejected when ambiguous. With only a version input, this is not a cryptographic downstream run binding. Failure leaves the release published and provides an explicit resume hint.

Closeout creates one PR that updates the source appcast and opens the next Unreleased section. It refuses to overwrite a feed changed since the frozen source. Merge this PR to serve the new Sparkle update from the default branch. It never writes directly to the protected branch. The published assets include appcast.xml, appcast-entry.json, signing manifest, frozen release notes, inventory, and checksums alongside app/CLI/npm artifacts.

The generated appcast is unsigned. Apps requiring signed feeds (SURequireSignedFeed=true) are rejected as soon as the build job can inspect the built Info.plist, before signing, tag creation, or draft creation; signing and independent verification repeat this check. Do not migrate clients that already require signed feeds to this workflow. SUVerifyUpdateBeforeExtraction=true alone is supported: Sparkle 2.10.0's updater policy requires archive preverification when feed signing is required, but does not require feed signing when archive preverification is enabled. The ZIP still carries its verified Ed25519 signature.

Notarization retries at most three uploads, only for recognized pre-submission network failures. An assigned submission ID or terminal status stops resubmission, including uncertain completion; investigate the logged ID before retrying. Online ticket propagation uses bounded backoff. stapler validate retries only Apple network transients (NSURLErrorDomain -1001/-1003/-1004/-1005/-1009) up to four attempts with 10/20/40 s backoff; every other validation failure fails immediately. A dry run still requires real credentials and human approval, consumes signing/notarization time, and leaves immutable Actions artifacts for inspection. It does not prove GitHub publication, downstream dispatch, or closeout behavior; those paths have executable synthetic regression coverage. The archetype currently requires universal apps, integer build numbers, modern Sparkle key seeds, and the standard Sparkle helper topology. It does not build Linux/Windows assets, execute caller signing/DMG hooks, or automatically reconcile an already-published version.

Electron desktop archetype

release-electron.yml freezes a protected source commit and builds GoReleaser server archives/packages alongside Electron desktop artifacts. Credential-free jobs produce Windows NSIS/ZIP, Linux AppImage/DEB, and unsigned native x64 and arm64 .app bundles. The signing job never checks out or executes caller source. It re-signs the frozen unsigned apps with the selected Developer ID policy, submits and staples each app, then packages architecture-specific DMG and ZIP assets with workflow-owned hdiutil and ditto. All server and desktop bytes are merged into one immutable Actions payload with ASSET-INVENTORY.json, RELEASE-NOTES.md, and the configured checksum manifest.

The signer validates archive paths and bundle symlinks before importing credentials, signs nested code inside out with workflow-owned Electron entitlements, and separately signs and notarizes each DMG before stapling it. macOS packaging uses a standard app plus Applications-link layout; caller DMG customization and signing hooks are not run. Existing workflow inputs and published macOS asset names remain unchanged.

Independent arm64 and Intel macOS jobs verify the full checksum set, exact source identity, sealed bundle identifier and Team ID, Gatekeeper acceptance, stapled tickets in both ZIP and DMG payloads, native architecture, plus Windows PE and Linux AppImage formats. stapler validate retries only Apple network transients (NSURLErrorDomain -1001/-1003/-1004/-1005/-1009) up to four attempts with 10/20/40 s backoff; every other validation failure fails immediately. Publication re-downloads every draft asset and requires exact name and digest equality with both attestations before undrafting.

See examples/release-electron-caller.yml. The caller must provision the same five Apple secrets as the Swift archetype. The credential preflight runs before tag creation, so an unprovisioned consumer can validate its unsigned desktop contract in ordinary CI without leaving a partial release tag.

Electron draft creation uses an explicit GitHub repository context in its fresh, checkout-free workspace. It uploads the immutable assembled payload and validates the numeric draft ID before exporting it to later jobs.

The Electron CI gate checks both independent check runs and the latest commit status for each context on the frozen source commit. Failed, errored, or pending statuses block release even when an Actions check passes. Successful commit statuses can supply CI evidence by themselves; ci-check-events filters only Actions checks and never hides commit statuses.

Go CLI archetype

release-go-cli.yml is the first fleet archetype. It requires:

  • a protected default branch and a caller dispatched at its exact head;
  • every branch-protection or effective-ruleset required status context green on the frozen target commit;
  • CHANGELOG.md with a dated ## section containing the requested version;
  • go.mod and a GoReleaser config in its canonical lookup path (.config/goreleaser.yml, .config/goreleaser.yaml, .goreleaser.yml, .goreleaser.yaml, goreleaser.yml, or goreleaser.yaml);
  • a GoReleaser build matrix containing both darwin/amd64 and darwin/arm64 for every macOS binary name;
  • consistent versions in any present .release-version, VERSION, version.txt, root package.json, and extra-package package.json files.

Migration prerequisites

Before adding a caller, set the repository's Actions workflow permissions to both:

  • default_workflow_permissions=write (Settings → Actions → General → Workflow permissions → Read and write permissions);
  • can_approve_pull_request_reviews=true (enable Allow GitHub Actions to create and approve pull requests in the same section).

The second setting governs both pull-request creation and approval by GITHUB_TOKEN, despite the API field's approval-focused name. The closeout stage performs a best-effort policy preflight and maps GitHub's specific PR-creation denial to these setting names. Organization or enterprise policy may need to allow the repository override.

Because the write default is repository-wide, every workflow should still declare an explicit least-privilege top-level or job-level permissions block. The example caller starts from permissions: {} and grants only the reusable release job's required scopes.

When enabling Homebrew handoff, choose the formula's actual tap independently from the signing identity and provision TAP_TOKEN against that exact repository. The token must have Contents read and Actions write access to the configured tap; the handoff validates repository and workflow access before dispatch. The configured tap's update-formula.yml must accept the optional assets JSON input required by the @v1 compatibility contract. Both fleet taps retain their legacy filename-guessing fallback for older callers.

Tap repository and workflow preflight failures include the HTTP status and sanitized GitHub error message, preserving details such as token expiry, permission denial, or rate limiting when GitHub provides them. Diagnostics redact credentials, omit request/response objects, and bound the message to one line of at most 1,000 characters.

The homebrew-preflight-integration CI job executes the production preflight block through actions/github-script. It checks the public OpenClaw tap, then requests uniquely named missing repositories and workflows with the existing authenticated client to exercise both failure paths with GitHub's real 404 diagnostic. It never dispatches a workflow or intentionally fails authentication. The same job downloads the published clawdex v0.2.2 assets, compares them with both original verifier attestations, and runs the production formula parser against the pinned tap formula that exposed the skip_clean regression. Regression tests separately cover credential redaction and other failure responses.

See examples/release-go-cli-caller.yml for the complete thin caller.

Inputs:

Input Contract
version SemVer, with or without v. The workflow canonicalizes it to a v tag.
repository-type openclaw or personal. Selects only the signer and stable identifier namespace.
homebrew-tap Optional owner/repo. Empty defaults to openclaw/homebrew-tap for openclaw and steipete/homebrew-tap for personal; an explicit value always wins.
homebrew-formula Optional formula name. Empty skips handoff.
extra-packages JSON array of safe repo-relative files/directories. Basenames must be unique.
archive-files JSON array of safe repo-relative regular files copied by basename into every platform archive. Empty by default. Use it to preserve established archive members such as CHANGELOG.md, LICENSE, and README.md.
checksum-filename Safe published checksum asset filename, with no path separator or leading dot. Defaults to SHA256SUMS; set it to an established public contract such as checksums.txt or sha256sums.txt.
nfpm auto (default), enabled, or disabled. Auto builds .deb/.rpm assets when the frozen GoReleaser config has a nonempty nfpms list. Enabled requires that list; disabled preserves binary-only mode even when it exists.
build-runner ubuntu (default) or macos. macOS selects xcode-27 for the GoReleaser build job; use it for native Darwin/CGO dependencies.
split-goreleaser-config Optional tracked GoReleaser config for Linux/Windows CGO builds. When set, the canonical config builds on xcode-27, the split config builds on Ubuntu with the fleet aarch64-linux-gnu-gcc and x86_64-w64-mingw32-gcc toolchains, and the workflow merges their artifact metadata before signing. Requires nfpm: disabled.
stable-identifier Optional reverse-DNS code-signing identifier. Empty (default) preserves the derived repository policy identifier. Set only to retain an established identifier such as ai.openclaw.telecrawl.
require-signed-tag Boolean, default false. When true, the version tag must already exist, be SSH-signed, and verify against the frozen source's nonempty .github/release-allowed-signers; the workflow reverifies it immediately before publication.
darwin-universal auto (default), enabled, or disabled. auto and enabled preserve the existing universal Darwin archive; disabled emits only the configured native Darwin archives while retaining native signing, notarization, and dual-architecture verification.
strict-checks Boolean. Default false checks only branch-required contexts. true requires every independent check/status green. When no required contexts exist, both modes use the all-check fallback and require at least one completed, non-failed CI signal. Missing or pending exact-SHA signals are polled every 10 seconds for up to 15 minutes.
ci-check-events JSON array of GitHub Actions event names eligible for the CI gate, such as ["push","pull_request"]. Empty by default, preserving all events. Use this only when high-frequency scheduled or manual workflows share the release commit and are not release evidence; checks without a resolvable Actions run remain included.
reproducible-rebuild disabled (default) or non-darwin. The opt-in mode performs a second GoReleaser build from a fresh checkout of the frozen tag, then requires every staged Linux and Windows binary to be byte-identical before inventory creation. all is rejected because signing and notarization make published Darwin bytes intentionally different from an unsigned rebuild.

Repository policies:

Type Required identity Team ID Identifier prefix
openclaw Developer ID Application: OpenClaw Foundation FWJYW4S8P8 org.openclaw.<repo>.<binary>
personal Developer ID Application: Peter Steinberger Y5PE65HELJ com.steipete.<repo>.<binary>

Legacy OpenClaw repositories may undergo one expected designated-requirement transition when their first fleet release replaces an older identifier with org.openclaw.<repo>.<binary>. The slacrawl and graincrawl migrations both crossed this boundary. It is a one-time migration event: after that release, the stable identifier and embedded designated requirement must remain byte-equivalent under the workflow's normal signing and verification checks.

Repositories that cannot make that transition must pass their established reverse-DNS value through stable-identifier. The override is applied consistently during signing, manifest construction, both independent architecture verifiers, and designated-requirement checks. It does not change the signer identity or Team ID selected by repository-type.

The CI gate merges required status checks from legacy branch protection and effective branch rules, including any required GitHub App binding. With the default strict-checks: false, unrelated optional or dynamic failures do not block a release. Required checks must still be present and green on the frozen target commit. Repositories without required contexts fall back to requiring all independent checks/statuses completed and non-failed. Missing or pending signals on the exact frozen SHA are polled every 10 seconds for up to 15 minutes so a release dispatched alongside post-merge CI does not fail spuriously; completed failures still stop immediately. Set strict-checks: true to request that stricter all-check behavior even when required contexts exist.

ci-check-events narrows only Actions-backed check runs whose run event can be resolved exactly. It never removes commit statuses or checks with an unresolvable provenance URL, and the empty default preserves the existing gate. This lets a caller without required contexts exclude unrelated high-frequency schedule or workflow_dispatch runs without weakening push/PR CI evidence.

Self-check exclusion compares the complete Actions run ID in every archetype. For example, release run 42 excludes its own checks but retains an independent run 421, including any failure.

The first attempt, when no version tag exists, freezes the current protected default-branch head and creates an annotated tag there. On every retry, an existing exact annotated version tag takes precedence: its peeled commit becomes target-sha, even when the caller runs from a newer default-branch head. That frozen commit must still be reachable from the protected default branch; the workflow checks out that commit and evaluates its changelog, version metadata, and required CI signals. The tag is never moved or replaced. Lightweight tags, tags that do not peel directly to a commit, and tags outside protected-branch ancestry fail closed. The caller itself must still be dispatched at the current protected default-branch head.

With require-signed-tag: true, automatic tag creation is intentionally unavailable: the exact version tag must already exist. Git verifies its SSH signature against .github/release-allowed-signers from the frozen tag commit before any build, then repeats the verification from the same policy and tag immediately before the draft can be published. A missing, empty, symlinked, mismatched, or non-verifying policy/tag fails closed. The default remains false, so existing callers retain automatic annotated-tag creation.

Build host and nFPM packages

Existing callers need no changes: build-runner: ubuntu retains the cross-build job, and nfpm: auto behaves exactly like the prior binary-only build when the GoReleaser config has no nfpms entries.

GoReleaser now runs its archive phase before signing so its artifacts.json is authoritative for each configured platform archive name and format. Those preliminary archives are never released. After macOS signing and notarization, the workflow rebuilds every archive from the signed payload using the exact metadata, accepting only tar.gz and zip. This preserves .tar.gz on existing callers and retains mature crawlers' configured Windows .zip assets without trusting filename reconstruction. Multiple command build directories for one platform are combined into that platform's single configured archive; duplicate member names (including case-only collisions) fail before publication.

Because rebuilding from signed binary staging directories does not retain GoReleaser's separately sourced archive files, callers with an established archive-member contract must list those regular files in archive-files. The workflow freezes them from the tagged source, rejects symlinks, unsafe or duplicate basenames, and collisions with built payloads, then includes the same files in every native and optional universal archive. The empty default leaves existing callers' bytes and contents unchanged.

darwin-universal: disabled removes only the extra darwin_universal payload. The configured darwin/amd64 and darwin/arm64 pair remains mandatory, individually signed and notarized, and independently verified on native Intel and arm64 runners. auto is the backward-compatible default and has the same universal-output behavior as earlier @v1 releases.

reproducible-rebuild: non-darwin adds a fail-closed publication gate after signing/package assembly and before inventory creation. A read-only job checks out the exact frozen release tag into a clean directory, installs the Go version pinned by that source's go.mod, and runs the exact primary GoReleaser version, command, configuration, bounded two-build parallelism, and runner selection with a clean dist, disabled Go cache reuse, TZ=UTC, and the C locale. That job never receives the primary release payload and uploads only its raw outputs. A separate clean comparison job checks out no caller code, downloads both immutable artifacts, and compares each rebuilt Linux/Windows binary against the binary extracted from the already assembled staged archive. It never compares two outputs from the same build job, and caller-controlled build hooks never share a process environment with the comparison. The staged bytes are the ones subsequently recorded in ASSET-INVENTORY.json, uploaded as the draft, independently attested on both macOS architectures, and required byte-for-byte by the publisher.

The inventory embeds the Go and GoReleaser versions plus the staged and rebuilt SHA-256 for every selected target/binary. Both independent verifiers re-extract the staged binaries and bind those digests to their attestations. Darwin is deliberately excluded: Developer ID signing adds a trusted timestamp and notarization policy requires the signed bytes, so a clean unsigned rebuild cannot be byte-identical. reproducible-rebuild: all therefore fails input validation rather than passing vacuously.

Use build-runner: macos when Darwin binaries import native frameworks or otherwise require CGO on macOS. Set MACOSX_DEPLOYMENT_TARGET explicitly in the caller’s GoReleaser env for CGO Darwin builds and retain architecture-specific compiler targets. The complete GoReleaser build runs from the same frozen tag on xcode-27; signing, notarization, verification, draft binding, and publication remain separate later jobs. Native callers should list only targets that can build on that host. A Darwin-only CGO project should omit Linux from its GoReleaser goos list. Projects that need both native Darwin and Linux should use separate GoReleaser build IDs whose environment/toolchain works on the selected host; this workflow does not depend on GoReleaser Pro partial-build merging.

For CGO projects that cannot build every target on one host, set split-goreleaser-config to a second tracked config containing only Linux and Windows builds. The canonical config must contain Darwin and runs on macOS; the second config runs on Ubuntu after the workflow installs fixed cross-compilers for Linux arm64 and Windows amd64. A credential-free merge job accepts only GoReleaser binary, archive, universal-binary, and Linux-package records, rejects path escapes, symlinks, and cross-host filename collisions, and emits the same immutable raw payload consumed by the existing signing pipeline. With reproducible-rebuild: non-darwin, the independent rebuild repeats the Linux/Windows config on a clean Ubuntu checkout with the same frozen Go and GoReleaser versions.

Root package.json files may intentionally omit version when they are build-tool manifests rather than published packages. When a root version is present it must still match the release; package manifests supplied through extra-packages remain versioned release inputs and must match.

With nfpm: auto, a nonempty top-level nfpms list switches GoReleaser to its non-publishing release pipeline, skips unrelated package/publisher pipes, and retains only its Linux Package artifacts. The workflow accepts checksum-bound .deb and .rpm files with safe GitHub asset names; prerelease templates must avoid ~, which GitHub rewrites on upload. Use nfpm: disabled for a configured repository that intentionally wants binary archives only, or nfpm: enabled to fail closed unless packages are configured and emitted.

The selected configuration path is passed explicitly to GoReleaser, and GORELEASER_CURRENT_TAG is fixed to the validated annotated tag. Multiple tags on the same frozen commit therefore cannot change package versions, filenames, or build-time version templates.

nFPM assets are copied by exact artifacts.json path and name, never reconstructed. ASSET-INVENTORY.json records each as kind: nfpm with packageFormat and Linux platform, and the configured checksum asset covers the exact package bytes. Both independent macOS verifiers require those names, sizes, metadata, and digests. Debian and RPM containers are not Apple-signable; their provenance is the frozen-source GoReleaser build plus the same dual-verifier checksum and byte-bound publication chain as every other release asset. Embedded Darwin binaries continue to require Developer ID signing, hardened runtime, designated-requirement stability, and notarization.

Binary authenticity does not trust the tag signature or annotation. Each architecture verifier has only actions: read; it downloads the draft job's immutable Actions artifact by its producer-exported name, never calls the draft Releases API, and independently enforces the exact commit, inventory, SHA-256 checksums, Apple certificate chain, hardened-runtime flag, timestamp, stable embedded designated requirement, Team ID, notarization requirement, and native plus universal architectures. GoReleaser's own goos/goarch artifact metadata assigns canonical platform targets before inventory creation; verifiers check those target labels against archive binary formats and architectures. Each verifier uploads an attestation containing its verified verdict, source artifact name, configured checksum filename, and exact ASSET-INVENTORY.json and checksum bytes. Producer-bound names and 30-day artifact retention preserve this chain across GitHub's partial-job reruns, where consumer run_attempt values can differ.

Validation extracts exactly one dated level-two changelog section for the requested version from the frozen commit and preserves it as RELEASE-NOTES.md. The draft includes that file in ASSET-INVENTORY.json, the configured checksum asset, and the immutable verification payload. Both architecture attestations carry its exact text. Publication requires both attestations and the API-downloaded draft asset to agree byte-for-byte, then installs those verified bytes as the release body in the same call that removes draft status.

macOS verification matrix

Control Bare Mach-O CLI Future .app bundle
SHA-256 inventory and exact draft binding Required Required
codesign --verify --strict plus stable identifier-based designated requirement Required Required
Developer ID chain, Team ID, trusted timestamp, and hardened runtime Required Required
Online notarization ticket via codesign --verify --strict --check-notarization -R=notarized Required Required
Native-runner and universal architecture checks Required Artifact-specific
spctl --assess --type execute Not applicable; SecAssessment rejects valid raw CLIs because they are not app bundles Required behind the .app type condition

Bare CLI archives cannot carry a stapled ticket: stapler supports disk images, installer packages, and certain signed executable bundles, not raw Mach-O files. The online codesign --check-notarization requirement is therefore the CLI ticket-presence proof. The Go CLI verifier inspects regular executable files and does not run spctl; app-bundle assessment belongs to the Electron workflow.

The publisher is the only post-draft job with contents: write. After both verifiers pass, it requires byte-identical checksum attestations, lists and API-downloads every unpublished draft asset, rejects duplicate, extra, missing, or renamed assets, compares the configured checksum asset's bytes exactly, and hashes every remaining asset. It un-drafts only when the draft names and digests equal the write-free verification conclusion. On a retry after publication, it separately downloads the existing public release, requires its body and every asset to match the new dual-verifier conclusion, then deletes only the redundant retry draft and continues downstream. Thus draft-read access remains coupled to publication authority without extending that authority to either verifier.

The Homebrew handoff is independent of signing ownership. It requires both architecture attestations to carry byte-identical inventory and checksum controls, selects exactly one inventory-declared archive for each of darwin_amd64, darwin_arm64, linux_amd64, and linux_arm64, and dispatches update-formula.yml in the resolved homebrew-tap with formula, canonical tag, source repository, and optional assets JSON. Each asset entry is the exact verified filename and SHA-256; neither side reconstructs a filename. The tap downloads every supplied URL and requires its observed digest to equal the supplied digest before committing. Without assets, tap workflows retain their prior template/guessing behavior for older callers.

The handoff snapshots the tap workflow's runs, requires exactly one new matching run from the token actor, waits for it to succeed, then polls the tap's Formula/<formula>.rb within one 15-minute deadline. A parser-backed closed fetchable grammar rejects alternate setters, reflection, dynamic dispatch, heredoc/comment decoys, and non-literal URL/hash forms without executing tap code. Static formula metadata may include a literal non-negative version_scheme. It may also include skip_clean with literal string paths, preserving signed executables during Homebrew installation; interpolations, calls, splats, blocks, and symbol arguments remain rejected. Stable fetches must live inside platform CPU branches; class-root and unbranched platform fetch setters are rejected. Platform blocks may use only zero-argument Hardware::CPU.arm? or Hardware::CPU.intel? branches, optionally conjoined with Hardware::CPU.is_64_bit?, expressed as one exhaustive if/else or two adjacent complementary if blocks; both literal branches are inspected and bound to the corresponding verified darwin_* or linux_* inventory target, while every other receiver, predicate, argument, boolean form, or conditional is rejected. The formula must cover exactly all four verified Homebrew targets. Every accepted formula URL must name an exact release asset from that repository and tag, and every adjacent sha256 must equal the digest in both verifier attestations. A successful dispatcher run alone is never accepted as proof.

The live tap contract still cannot carry the numeric release ID, source tag object, verifier attestation document, or a correlation ID. Residual gap: run snapshot, title, and token actor cannot cryptographically distinguish an identical same-actor dispatch that appears after the final ambiguity check, nor bind the resulting tap commit directly to the numeric GitHub release. Exact inventory-derived asset names and digests now cross that boundary, the tap independently re-hashes their public bytes, and the reusable workflow proves the post-run formula state contains only release URLs and hashes from the verifier conclusion. The final stage opens a PR adding the next ## Unreleased section.

Secret provisioning

Provision secrets per caller repository. Do not centralize one credential across unrelated repositories or pass credentials through ordinary inputs.

Secret Value
MACOS_SIGNING_P12 Base64 of a password-protected .p12 containing the policy-matching Developer ID Application certificate and private key.
MACOS_SIGNING_P12_PASSWORD .p12 export password.
ASC_KEY_ID App Store Connect API key ID.
ASC_ISSUER_ID App Store Connect issuer ID.
ASC_PRIVATE_KEY_P8 Full raw contents of the matching AuthKey_*.p8.
TAP_TOKEN Optional fine-grained token scoped to the configured homebrew-tap, with Contents read and Actions write access. Required only with homebrew-formula; access is preflighted against that tap before dispatch.

The signing job imports the .p12 into a unique ephemeral keychain, adds that keychain to the scoped user search list, validates the policy identity, and signs by its SHA-1 hash. An always() cleanup restores the original search list and deletes the temporary keychain; the .p8 exists only inside runner temporary storage. Post-sign assertions are individually labeled and report only sanitized public signature metadata on failure. Designated-requirement comparison normalizes codesign's optional alphanumeric quotes and Executable= display header before comparing both sides. The verifier jobs receive only actions: read; their proof step explicitly removes GitHub, Actions runtime, Apple, signing, and tap token names from the environment.

Versioning policy

Reusable workflows are immutable at release tags. Callers use the moving major compatibility tag (@v1). We cut immutable SemVer tags for auditability and move v1 only for backward-compatible changes. Breaking input, secret, output, asset, or trust-policy changes require v2.

Pre-release tags such as v1.0.0-alpha.1 validate the pipeline before a compatibility tag moves. Caller migrations begin only after v1 exists.

Validation

Run:

scripts/validate-workflows.sh

The shell entry point orchestrates actionlint, executable regression tests and safe YAML loading. scripts/validate-workflow-contracts.py checks the release job topology, permissions, required controls and action pins; run it from the repository root for focused contract validation. Both validation entry points are code-owned with the workflow sources.

This runs actionlint on reusable, CI, and example workflows; parses every YAML document with Psych safe loading; executes adversarial frozen-tag, native-build selection, nFPM target/inventory, draft-binding, Homebrew tap-selection, explicit-assets, fallback, live slacrawl/graincrawl platform-formula, and post-dispatch formula-binding scenarios; and enforces the required job/input topology, actions-read-only dual-architecture verifier, publisher hash binding, and immutable action references.

The keychain lifecycle suite (scripts/test-swift-keychain-lifecycle.mjs) executes all three workflows' signer and cleanup shell blocks with synthetic key material and a simulated security command, without changing local keychains. Every signer snapshots the search list before creation; cleanup restores that snapshot and attempts deletion independently, including failed imports without step outputs and empty original lists under Bash 3. CI runs the same suite with --real-keychains on a disposable hosted macOS runner, exercising native creation, search-list restoration, deletion and injected failures without release credentials. This native mode refuses to run outside GitHub-hosted macOS CI; it does not test real certificate import or notarization.

CI also exercises the pinned actions with a named artifact upload/download and a read-only GitHub API request. Its build smoke check uses the Electron workflow's Node, pnpm, and GoReleaser defaults to build an unsigned native snapshot, unpack and run its binary against this README, and perform a frozen-lockfile pnpm install and build. The Electron default and example caller use pnpm 11.28.5; the GoReleaser default is 2.18.2. With Go, GoReleaser, Node, and pnpm installed, run the same check locally:

scripts/smoke-release-build.sh

The smoke check removes its temporary build directory and Go cache on exit. It does not sign, tag, or publish a release.

Validation and native keychain CI explicitly select Node 26.10.0; snapshot integration pins Go 1.27.2 and exercises the caller-configurable Electron tool defaults. Superseded pull-request runs are cancelled, while main runs complete. Swift's default Linux image pins the Swift 6.4.0 Noble manifest digest; callers can still override swift-linux-image.

With Docker available, bash scripts/smoke-swift-toolchain.sh compiles and runs a Linux executable with static Swift libraries inside that exact default image. CI runs the same proof alongside the Go snapshot integration.

All three archetypes extract the exact dated Markdown release section, ignoring headings inside backtick/tilde fences and HTML comments while preserving original UTF-8 bytes and line endings. node scripts/test-release-notes-headings.mjs executes each workflow's Python extractor against the same positive and ambiguous-heading fixtures.

Literal comment markers in inline code spans or escaped Markdown do not start HTML comments and cannot hide later release headings.

node scripts/test-electron-signing.mjs executes the production Electron unpacking and signing steps against synthetic apps, including unsafe archives, nested seals, independent DMG notarization, and failure cleanup. On macOS, --native-packaging also exercises real ditto/hdiutil packaging and versioned framework symlinks; Apple signing and notarization are simulated.

The dispatch-only dry-run-archetypes.yml exercises both new workflows on protected main. Its npm caller uses the committed synthetic monorepo, packs and installs both sibling packages on clean x64/arm64 runners, and runs the exported API assertion. dry_run is hardcoded true; no npm account or registry publication is involved. The source caller downloads and verifies frozen GitHub archives. Dispatch with the version matching the fixture and dated changelog.

About

Reusable release workflows for the OpenClaw/steipete fleet: uniform draft → verify → publish pipelines per archetype (go-cli, npm-cli, sparkle-app, electron, web)

Resources

Contributing

Security policy

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages