Release: trim duplicate preflight work (#59117)

* Release: skip duplicate runtime-deps staging

* Release: trim public mac validation workflow

* Release: require promoted npm publish

* Release: verify promoted npm provenance

* Release: restore public mac validation build

* Release: skip pack check on npm promote

* Release: skip pack check on npm promote
This commit is contained in:
Onur
2026-04-01 19:24:37 +02:00
committed by GitHub
parent da64a978e5
commit f1f5a3fcf4
6 changed files with 140 additions and 48 deletions

View File

@@ -120,8 +120,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- The npm workflow and the private mac publish workflow accept
`preflight_only=true` to run validation/build/package steps without uploading
public release assets.
- Both workflows also accept a prior successful preflight run id so a real
publish can promote the prepared artifacts without rebuilding them again.
- Real npm publish requires a prior successful npm preflight run id so the
publish job promotes the prepared tarball instead of rebuilding it.
- Real private mac publish requires a prior successful private mac preflight
run id so the publish job promotes the prepared artifacts instead of
rebuilding or renotarizing them again.
- The private mac workflow also accepts `smoke_test_only=true` for branch-safe
workflow smoke tests that use ad-hoc signing, skip notarization, skip shared
appcast generation, and do not prove release readiness.
@@ -132,17 +135,23 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
workflow change before merge.
- `.github/workflows/macos-release.yml` in `openclaw/openclaw` is now a
public validation-only handoff. It validates the tag/release state and points
operators to the private repo; it does not build or publish macOS artifacts.
operators to the private repo. It still rebuilds the JS outputs needed for
release validation, but it does not sign, notarize, or publish macOS
artifacts.
- `openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml`
is the required private mac validation lane for `swift test`; keep it green
before any real mac publish run starts.
- Real mac preflight and real mac publish both use
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`.
- The private mac workflow runs on GitHub's xlarge macOS runner and uses a
SwiftPM cache because the Swift build/test/package path is CPU-heavy.
- The private mac validation lane runs on GitHub's standard macOS runner.
- The private mac preflight path runs on GitHub's xlarge macOS runner and uses
a SwiftPM cache because the build/sign/notarize/package path is CPU-heavy.
- Private mac preflight uploads notarized build artifacts as workflow artifacts
instead of uploading public GitHub release assets.
- Private smoke-test runs upload ad-hoc, non-notarized build artifacts as
workflow artifacts and intentionally skip stable `appcast.xml` generation.
- npm preflight, public mac validation, and private mac preflight must all pass
before any real publish run starts.
- npm preflight, public mac validation, private mac validation, and private mac
preflight must all pass before any real publish run starts.
- Real publish runs must be dispatched from `main`; branch-dispatched publish
attempts should fail before the protected environment is reached.
- The release workflows stay tag-based; rely on the documented release sequence
@@ -150,8 +159,8 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- The `npm-release` environment must be approved by `@openclaw/openclaw-release-managers` before publish continues.
- Mac publish uses
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml` for
build, signing, notarization, packaged mac artifact generation, and
stable-feed `appcast.xml` artifact generation.
private mac preflight artifact preparation and real publish artifact
promotion.
- Real private mac publish uploads the packaged `.zip`, `.dmg`, and
`.dSYM.zip` assets to the existing GitHub release in `openclaw/openclaw`
automatically when `OPENCLAW_PUBLIC_REPO_RELEASE_TOKEN` is present in the
@@ -209,29 +218,28 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
7. Create and push the git tag.
8. Create or refresh the matching GitHub release.
9. Start `.github/workflows/openclaw-npm-release.yml` with `preflight_only=true`
and wait for it to pass. Save that run id if you want the real publish to
reuse the prepared npm tarball.
and wait for it to pass. Save that run id because the real publish requires
it to reuse the prepared npm tarball.
10. Start `.github/workflows/macos-release.yml` in `openclaw/openclaw` and wait
for the public validation-only run to pass.
11. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml`
with the same tag and wait for the private mac validation lane to pass.
12. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
with `preflight_only=true` and wait for it to pass. Save that run id if you
want the real publish to reuse the notarized mac artifacts.
12. If any preflight or validation run fails, fix the issue on a new commit,
with `preflight_only=true` and wait for it to pass. Save that run id because
the real publish requires it to reuse the notarized mac artifacts.
13. If any preflight or validation run fails, fix the issue on a new commit,
delete the tag and matching GitHub release, recreate them from the fixed
commit, and rerun all relevant preflights from scratch before continuing.
Never reuse old preflight results after the commit changes.
13. Start `.github/workflows/openclaw-npm-release.yml` with the same tag for
the real publish. When the preflight run id is available, pass it via
`preflight_run_id` to skip the second npm rebuild.
14. Start the real private mac publish with the same tag. When the private
preflight run id is available, pass it via `preflight_run_id` to skip the
second mac build/sign/notarize cycle and promote those prepared artifacts
directly to the public release.
14. Start `.github/workflows/openclaw-npm-release.yml` with the same tag for
the real publish and pass the successful npm `preflight_run_id`.
15. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
16. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
for the real publish and wait for success.
for the real publish with the successful private mac `preflight_run_id` and
wait for success.
17. Verify the successful real private mac run uploaded the `.zip`, `.dmg`,
and `.dSYM.zip` artifacts to the existing GitHub release in
`openclaw/openclaw`.

View File

@@ -82,11 +82,12 @@ jobs:
{
echo "## Public macOS validation only"
echo
echo "This workflow no longer builds, signs, notarizes, or uploads macOS assets."
echo "This workflow validates the public release handoff and still builds JS artifacts needed for release checks."
echo "It does not sign, notarize, or upload macOS assets."
echo
echo "Next step:"
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml\` with tag \`${RELEASE_TAG}\`."
echo "- Use \`preflight_only=true\` there for the full private mac preflight."
echo "- For the real publish path, the private run uploads the packaged \`.zip\`, \`.dmg\`, and \`.dSYM.zip\` files to the existing GitHub release in \`openclaw/openclaw\` automatically."
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml\` with tag \`${RELEASE_TAG}\` and wait for the private mac validation lane to pass."
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml\` with tag \`${RELEASE_TAG}\` and \`preflight_only=true\` for the full private mac preflight."
echo "- For the real publish path, run the same private mac publish workflow from \`main\` with the successful private preflight \`preflight_run_id\` so it promotes the prepared artifacts instead of rebuilding them."
echo "- For stable releases, also download \`macos-appcast-${RELEASE_TAG}\` from the successful private run and commit \`appcast.xml\` back to \`main\` in \`openclaw/openclaw\`."
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -13,7 +13,7 @@ on:
default: false
type: boolean
preflight_run_id:
description: Existing preflight workflow run id to promote without rebuilding
description: Existing successful preflight workflow run id to promote without rebuilding
required: false
type: string
@@ -28,6 +28,7 @@ env:
jobs:
preflight_openclaw_npm:
if: ${{ inputs.preflight_only }}
runs-on: ubuntu-latest
permissions:
contents: read
@@ -42,6 +43,12 @@ jobs:
exit 1
fi
- name: Forbid preflight artifact promotion on validation-only runs
if: ${{ inputs.preflight_only && inputs.preflight_run_id != '' }}
run: |
echo "preflight_run_id is only valid for real publish runs."
exit 1
- name: Checkout
uses: actions/checkout@v6
with:
@@ -88,6 +95,7 @@ jobs:
- name: Validate release tag and package metadata
if: ${{ inputs.preflight_run_id == '' }}
env:
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "1"
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_MAIN_REF: origin/main
run: |
@@ -106,6 +114,7 @@ jobs:
id: packed_tarball
env:
OPENCLAW_PREPACK_PREPARED: "1"
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
PACK_JSON="$(npm pack --json)"
@@ -115,16 +124,23 @@ jobs:
echo "npm pack did not produce a tarball file." >&2
exit 1
fi
echo "path=$PACK_PATH" >> "$GITHUB_OUTPUT"
RELEASE_SHA="$(git rev-parse HEAD)"
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
cp "$PACK_PATH" "$ARTIFACT_DIR/"
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
- name: Upload prepared npm tarball
- name: Upload prepared npm publish bundle
uses: actions/upload-artifact@v7
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
path: ${{ steps.packed_tarball.outputs.path }}
path: ${{ steps.packed_tarball.outputs.dir }}
if-no-files-found: error
validate_publish_dispatch_ref:
validate_publish_request:
if: ${{ !inputs.preflight_only }}
runs-on: ubuntu-latest
permissions:
@@ -140,9 +156,19 @@ jobs:
exit 1
fi
- name: Require preflight artifact promotion on real publish
env:
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
run: |
set -euo pipefail
if [[ -z "${PREFLIGHT_RUN_ID}" ]]; then
echo "Real publish requires preflight_run_id from a successful npm preflight run." >&2
exit 1
fi
publish_openclaw_npm:
# npm trusted publishing + provenance requires a GitHub-hosted runner.
needs: [preflight_openclaw_npm, validate_publish_dispatch_ref]
needs: [validate_publish_request]
if: ${{ !inputs.preflight_only }}
runs-on: ubuntu-latest
environment: npm-release
@@ -187,8 +213,16 @@ jobs:
echo "Publishing openclaw@${PACKAGE_VERSION}"
- name: Verify preflight run metadata
env:
GH_TOKEN: ${{ github.token }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
run: |
set -euo pipefail
RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,conclusion,url)"
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw NPM Release"], ["headBranch", "main"], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID}: ${run.url}`);'
- name: Download prepared npm tarball
if: ${{ inputs.preflight_run_id != '' }}
uses: actions/download-artifact@v8
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
@@ -197,17 +231,10 @@ jobs:
run-id: ${{ inputs.preflight_run_id }}
github-token: ${{ github.token }}
- name: Build
if: ${{ inputs.preflight_run_id == '' }}
run: pnpm build
- name: Build Control UI
if: ${{ inputs.preflight_run_id == '' }}
run: pnpm ui:build
- name: Validate release tag and package metadata
if: ${{ inputs.preflight_run_id == '' }}
env:
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "1"
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_MAIN_REF: origin/main
run: |
@@ -219,12 +246,35 @@ jobs:
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
pnpm release:openclaw:npm:check
- name: Resolve publish tarball
id: publish_tarball
if: ${{ inputs.preflight_run_id != '' }}
- name: Verify prepared tarball provenance
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
TARBALL_PATH="$(find preflight-tarball -maxdepth 1 -type f -name '*.tgz' -print | sort | tail -n 1)"
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
TAG_FILE="preflight-tarball/release-tag.txt"
SHA_FILE="preflight-tarball/release-sha.txt"
if [[ ! -f "$TAG_FILE" || ! -f "$SHA_FILE" ]]; then
echo "Prepared preflight metadata is missing." >&2
ls -la preflight-tarball >&2 || true
exit 1
fi
ARTIFACT_RELEASE_TAG="$(tr -d '\r\n' < "$TAG_FILE")"
ARTIFACT_RELEASE_SHA="$(tr -d '\r\n' < "$SHA_FILE")"
if [[ "$ARTIFACT_RELEASE_TAG" != "$RELEASE_TAG" ]]; then
echo "Prepared preflight tag mismatch: expected $RELEASE_TAG, got $ARTIFACT_RELEASE_TAG" >&2
exit 1
fi
if [[ "$ARTIFACT_RELEASE_SHA" != "$EXPECTED_RELEASE_SHA" ]]; then
echo "Prepared preflight SHA mismatch: expected $EXPECTED_RELEASE_SHA, got $ARTIFACT_RELEASE_SHA" >&2
exit 1
fi
- name: Resolve publish tarball
id: publish_tarball
run: |
set -euo pipefail
TARBALL_PATH="$(find preflight-tarball -type f -name '*.tgz' -print | sort | tail -n 1)"
if [[ -z "$TARBALL_PATH" ]]; then
echo "Prepared preflight tarball not found." >&2
ls -la preflight-tarball >&2 || true

View File

@@ -1127,7 +1127,7 @@
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
"release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:facades:check && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && node --import tsx scripts/release-check.ts",
"release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:facades:check && pnpm plugin-sdk:api:check && node --import tsx scripts/release-check.ts",
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
"release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts",
"release:plugins:npm:check": "node --import tsx scripts/plugin-npm-release-check.ts",

View File

@@ -58,6 +58,7 @@ const MAX_CALVER_DISTANCE_DAYS = 2;
const REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html"];
const CONTROL_UI_ASSET_PREFIX = "dist/control-ui/assets/";
const NPM_PACK_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
const skipPackValidationEnv = "OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK";
function normalizeRepoUrl(value: unknown): string {
if (typeof value !== "string") {
@@ -99,6 +100,14 @@ export function resolveNpmDistTagMirrorAuth(params?: {
}) as NpmDistTagMirrorAuth;
}
export function shouldSkipPackedTarballValidation(env = process.env): boolean {
const raw = env[skipPackValidationEnv];
if (!raw) {
return false;
}
return !/^(0|false)$/i.test(raw);
}
export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null {
const trimmed = version.trim();
if (!trimmed) {
@@ -412,6 +421,7 @@ function collectPackedTarballErrors(): string[] {
function main(): number {
const pkg = loadPackageJson();
const now = new Date();
const skipPackValidation = shouldSkipPackedTarballValidation();
const metadataErrors = collectReleasePackageMetadataErrors(pkg);
const tagErrors = collectReleaseTagErrors({
packageVersion: pkg.version ?? "",
@@ -420,7 +430,7 @@ function main(): number {
releaseMainRef: process.env.RELEASE_MAIN_REF,
now,
});
const tarballErrors = collectPackedTarballErrors();
const tarballErrors = skipPackValidation ? [] : collectPackedTarballErrors();
const errors = [...metadataErrors, ...tagErrors, ...tarballErrors];
if (errors.length > 0) {
@@ -435,7 +445,7 @@ function main(): number {
const dayDistance =
parsedVersion === null ? "unknown" : String(utcCalendarDayDistance(parsedVersion.date, now));
console.log(
`openclaw-npm-release-check: validated ${channel} release ${pkg.version} (${dayDistance} day UTC delta).`,
`openclaw-npm-release-check: validated ${channel} release ${pkg.version} (${dayDistance} day UTC delta${skipPackValidation ? "; metadata-only" : ""}).`,
);
return 0;
}

View File

@@ -10,6 +10,7 @@ import {
resolveNpmDistTagMirrorAuth,
resolveNpmPublishPlan,
resolveNpmCommandInvocation,
shouldSkipPackedTarballValidation,
utcCalendarDayDistance,
} from "../scripts/openclaw-npm-release-check.ts";
@@ -155,6 +156,28 @@ describe("resolveNpmDistTagMirrorAuth", () => {
});
});
describe("shouldSkipPackedTarballValidation", () => {
it("defaults to full pack validation", () => {
expect(shouldSkipPackedTarballValidation({})).toBe(false);
});
it("accepts truthy values for metadata-only validation", () => {
expect(
shouldSkipPackedTarballValidation({
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "1",
}),
).toBe(true);
});
it("treats false-like values as disabled", () => {
expect(
shouldSkipPackedTarballValidation({
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "false",
}),
).toBe(false);
});
});
describe("compareReleaseVersions", () => {
it("treats stable as newer than same-day beta", () => {
expect(compareReleaseVersions("2026.3.29", "2026.3.29-beta.2")).toBe(1);