mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix(release): resume public GitHub release pages (#152435)
* fix(release): resume public GitHub release pages Resume canonical public pages with incomplete evidence while preserving immutable asset identity and the later docs publication owner. Add explicitly requested activation before Docker with one approval, the existing finalizer, and unchanged latest policy. * test(release): update guards for resumable publication Include the optional early finalizer in publication dependency assertions and model its explicit false default. Keep failed-child evidence visible without claiming that a resumed public release is a draft.
This commit is contained in:
parent
dab779b426
commit
c09841409e
8 changed files with 285 additions and 58 deletions
61
.github/workflows/openclaw-release-publish.yml
vendored
61
.github/workflows/openclaw-release-publish.yml
vendored
|
|
@ -97,6 +97,11 @@ on:
|
|||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
finalize_release_before_docker:
|
||||
description: Explicitly activate the verified GitHub release before Docker publication (direct publication only)
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
release_profile:
|
||||
description: Release coverage profile used for release evidence summaries; default reads it from the validation manifest
|
||||
required: false
|
||||
|
|
@ -163,6 +168,8 @@ jobs:
|
|||
FOCUSED_RELEASE_EVIDENCE_RUN_ATTEMPT: ${{ inputs.focused_release_evidence_run_attempt }}
|
||||
OPENCLAW_NPM_RESUME_RUN_ID: ${{ inputs.openclaw_npm_resume_run_id }}
|
||||
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
|
||||
FINALIZE_RELEASE_BEFORE_DOCKER: ${{ inputs.finalize_release_before_docker }}
|
||||
PREPARED_PLUGINS: ${{ inputs.prepared_plugins }}
|
||||
PUBLISH_DOCKER_ONLY: ${{ inputs.publish_docker_only && 'true' || 'false' }}
|
||||
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
||||
PLUGINS: ${{ inputs.plugins }}
|
||||
|
|
@ -251,6 +258,10 @@ jobs:
|
|||
echo "openclaw_npm_resume_run_id requires publish_openclaw_npm=true." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${FINALIZE_RELEASE_BEFORE_DOCKER:-false}" == "true" && ( "${PUBLISH_OPENCLAW_NPM}" != "true" || -n "${PREPARED_PLUGINS:-}" ) ]]; then
|
||||
echo "finalize_release_before_docker requires direct publication with publish_openclaw_npm=true and no prepared_plugins." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PUBLISH_DOCKER_ONLY}" == "true" ]]; then
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
|
||||
echo "publish_docker_only requires publish_openclaw_npm=false." >&2
|
||||
|
|
@ -1975,9 +1986,9 @@ jobs:
|
|||
append_release_proof_to_github_release
|
||||
record_postpublish_diagnostics assets-success
|
||||
if [[ "${failed}" == "0" ]]; then
|
||||
echo "- GitHub release: kept draft until required publication checks complete" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- GitHub release: evidence complete; activation follows required publication checks" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "- GitHub release: left as draft because a required publish child failed" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- GitHub release: evidence updated; a required publish child failed" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
fi
|
||||
if [[ "${failed}" != "0" ]]; then
|
||||
|
|
@ -2054,8 +2065,14 @@ jobs:
|
|||
|
||||
publish_docker:
|
||||
name: Publish Docker images
|
||||
needs: [resolve_release_target, publish, verify_core_npm_registry]
|
||||
if: ${{ always() && !contains(inputs.tag, '-alpha.') && ((inputs.publish_openclaw_npm && needs.publish.result == 'success') || (inputs.publish_docker_only && needs.verify_core_npm_registry.result == 'success')) }}
|
||||
needs:
|
||||
[
|
||||
resolve_release_target,
|
||||
publish,
|
||||
verify_core_npm_registry,
|
||||
finalize_github_release_before_docker,
|
||||
]
|
||||
if: ${{ always() && (!inputs.finalize_release_before_docker || needs.finalize_github_release_before_docker.result == 'success') && !contains(inputs.tag, '-alpha.') && ((inputs.publish_openclaw_npm && needs.publish.result == 'success') || (inputs.publish_docker_only && needs.verify_core_npm_registry.result == 'success')) }}
|
||||
uses: ./.github/workflows/docker-release.yml
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
|
|
@ -2096,7 +2113,7 @@ jobs:
|
|||
name: Approve verified GitHub release activation
|
||||
# Approval waits must not hold the Linux updater publication queue.
|
||||
needs: [publish, publish_docker]
|
||||
if: ${{ always() && inputs.prepared_plugins == '' && inputs.publish_openclaw_npm && needs.publish.result == 'success' && (needs.publish_docker.result == 'success' || (contains(inputs.tag, '-alpha.') && needs.publish_docker.result == 'skipped')) }}
|
||||
if: ${{ always() && !inputs.finalize_release_before_docker && inputs.prepared_plugins == '' && inputs.publish_openclaw_npm && needs.publish.result == 'success' && (needs.publish_docker.result == 'success' || (contains(inputs.tag, '-alpha.') && needs.publish_docker.result == 'skipped')) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: npm-release
|
||||
|
|
@ -2109,11 +2126,12 @@ jobs:
|
|||
|
||||
finalize_github_release:
|
||||
name: Finalize GitHub release
|
||||
# Reconcile the same release after Docker; the opt-in early path reuses its approval.
|
||||
# Apps attach independently after npm and Gateway Docker evidence is complete.
|
||||
needs: [publish, publish_docker, approve_github_release]
|
||||
needs: [publish, publish_docker, approve_github_release, finalize_github_release_before_docker]
|
||||
# Prepared releases become visible in the outer button workflow, after this
|
||||
# parent succeeds and ClawHub's terminal publication can be verified.
|
||||
if: ${{ always() && inputs.prepared_plugins == '' && inputs.publish_openclaw_npm && needs.publish.result == 'success' && (needs.publish_docker.result == 'success' || (contains(inputs.tag, '-alpha.') && needs.publish_docker.result == 'skipped')) && needs.approve_github_release.result == 'success' }}
|
||||
if: ${{ always() && inputs.prepared_plugins == '' && inputs.publish_openclaw_npm && needs.publish.result == 'success' && (needs.publish_docker.result == 'success' || (contains(inputs.tag, '-alpha.') && needs.publish_docker.result == 'skipped')) && (needs.approve_github_release.result == 'success' || (inputs.finalize_release_before_docker && needs.finalize_github_release_before_docker.result == 'success')) }}
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: linux-app-release-publish
|
||||
|
|
@ -2122,7 +2140,7 @@ jobs:
|
|||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
steps:
|
||||
steps: &finalize_github_release_steps
|
||||
- name: Checkout trusted updater publication tooling
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
|
@ -2176,12 +2194,37 @@ jobs:
|
|||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: linux-updater-continuity-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
name: linux-updater-continuity-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
|
||||
path: |
|
||||
${{ runner.temp }}/linux-updater-carry
|
||||
${{ runner.temp }}/core-finalization.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
approve_github_release_before_docker:
|
||||
name: Approve GitHub release before Docker
|
||||
needs: [publish]
|
||||
if: ${{ inputs.finalize_release_before_docker && inputs.prepared_plugins == '' && inputs.publish_openclaw_npm && needs.publish.result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: npm-release
|
||||
permissions: {}
|
||||
steps:
|
||||
- run: echo "Approved the explicitly requested GitHub activation before Docker"
|
||||
|
||||
finalize_github_release_before_docker:
|
||||
name: Activate GitHub release before Docker
|
||||
needs: [publish, approve_github_release_before_docker]
|
||||
if: ${{ inputs.finalize_release_before_docker && needs.publish.result == 'success' && needs.approve_github_release_before_docker.result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: linux-app-release-publish
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
steps: *finalize_github_release_steps
|
||||
|
||||
dispatch_linux_mirror:
|
||||
name: Dispatch legacy Linux mirror after publication
|
||||
needs: [resolve_release_target, finalize_github_release]
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ For beta, stable, and full profiles, Linux (`ubuntu`) cross-OS lanes gate npm pu
|
|||
|
||||
The helper uses the qualified npm artifact bound by Full Release Validation. Supply `--npm-preflight-run` only to recover a separately prepared historical release. It never silently rebuilds a missing qualified artifact. Docker publication consumes the prepared OCI artifacts after checking the finalized tag and exact producer tuple; only registry writes and selector promotion hold the publication lock.
|
||||
|
||||
`OpenClaw Release Publish` dispatches the selected or all-publishable plugin packages to npm and the same set to ClawHub in parallel, then promotes the prepared OpenClaw npm preflight artifact with the matching dist-tag once plugin npm publish succeeds. It keeps the GitHub release as a draft while it verifies registry readback, calls `Docker Release` with the immutable tag and Release SHA for beta and stable releases, and only then finalizes the GitHub release. npm-only alpha releases finalize after the required npm checks without scheduling Docker. The release checkout remains the product/data root, while planning and final verification execute from the exact trusted workflow-source checkout so an older release commit cannot silently use obsolete release tooling. Once publication binds the frozen Tooling SHA to an exact protected lightweight `release-publish/<12sha>-<provenance-run>` tag, that live tag-to-SHA mapping remains authoritative when `main` advances; the suffix records tag-creation provenance, not the current parent run id. Core and plugin npm publishers re-read that exact tag and revalidate the exact parent run tuple immediately before each npm publish or dist-tag mutation, failing closed on a missing, moved, annotated, or wrong-SHA tag, parent mismatch, or disallowed parent state. Other privileged writers require their dependent enforcement changes before the protected-tag publication route is globally complete. Before any publish child starts, it renders and caches the exact GitHub release body. When the complete selected `CHANGELOG/YYYY.M.PATCH.md` section fits GitHub's 125,000-character limit and the renderer's matching 125,000-byte safety ceiling, the page contains that exact `## YYYY.M.PATCH` section including its heading. When the source section does not fit, the page keeps the exact grouped editorial notes and replaces the oversized contribution record with a stable link to the full record in the tag-pinned `CHANGELOG/records/YYYY.M.PATCH.md` (historical monolithic tags retain their original record link); partial records and truncated bullets are never published. The workflow chooses that full or compact body before adding `### Release verification`; if the proof tail would exceed the limit, it keeps the canonical body and relies on the immutable attached evidence instead. Stable releases published to npm `latest` become the GitHub latest release, while stable maintenance releases kept on npm `beta` are created with GitHub `latest=false`. The workflow also uploads the preflight dependency evidence, the full-validation manifest, and postpublish registry verification evidence to the GitHub release for post-release incident response. It prints child run IDs immediately, auto-approves release environment gates the workflow token is allowed to approve, summarizes failed child jobs with log tails, creates the draft GitHub release page up front, runs native Android qualification independently for a matching tagged Android pin (otherwise recording an explicit skip and shared mobile cutter remedy) and dispatches its publisher after the npm publisher succeeds without making GitHub finalization wait, waits for ClawHub staging only when `wait_for_clawhub=true` (the default `false` leaves that child detached), then runs the trusted-main beta verifier and uploads postpublish evidence for the GitHub release, npm package, selected plugin npm packages, staged ClawHub child workflow run IDs, and optional NPM Telegram run ID. The ClawHub bootstrap verifier requires the exact trusted-main workflow path and SHA, producer and terminal run attempts, release SHA, requested package set, immutable package artifact tuple, and terminal registry readback artifact; a successful legacy release-ref run is not accepted.
|
||||
`OpenClaw Release Publish` dispatches the selected or all-publishable plugin packages to npm and the same set to ClawHub in parallel, then promotes the prepared OpenClaw npm preflight artifact with the matching dist-tag once plugin npm publish succeeds. By default, it keeps the GitHub release as a draft while it verifies registry readback, calls `Docker Release` with the immutable tag and Release SHA for beta and stable releases, and only then finalizes the GitHub release. npm-only alpha releases finalize after the required npm checks without scheduling Docker. The release checkout remains the product/data root, while planning and final verification execute from the exact trusted workflow-source checkout so an older release commit cannot silently use obsolete release tooling. Once publication binds the frozen Tooling SHA to an exact protected lightweight `release-publish/<12sha>-<provenance-run>` tag, that live tag-to-SHA mapping remains authoritative when `main` advances; the suffix records tag-creation provenance, not the current parent run id. Core and plugin npm publishers re-read that exact tag and revalidate the exact parent run tuple immediately before each npm publish or dist-tag mutation, failing closed on a missing, moved, annotated, or wrong-SHA tag, parent mismatch, or disallowed parent state. Other privileged writers require their dependent enforcement changes before the protected-tag publication route is globally complete. Before any publish child starts, it renders and caches the exact GitHub release body. When the complete selected `CHANGELOG/YYYY.M.PATCH.md` section fits GitHub's 125,000-character limit and the renderer's matching 125,000-byte safety ceiling, the page contains that exact `## YYYY.M.PATCH` section including its heading. When the source section does not fit, the page keeps the exact grouped editorial notes and replaces the oversized contribution record with a stable link to the full record in the tag-pinned `CHANGELOG/records/YYYY.M.PATCH.md` (historical monolithic tags retain their original record link); partial records and truncated bullets are never published. The workflow chooses that full or compact body before adding `### Release verification`; if the proof tail would exceed the limit, it keeps the canonical body and relies on the immutable attached evidence instead. Stable releases published to npm `latest` become the GitHub latest release, while stable maintenance releases kept on npm `beta` are created with GitHub `latest=false`. The workflow also uploads the preflight dependency evidence, the full-validation manifest, and postpublish registry verification evidence to the GitHub release for post-release incident response. It prints child run IDs immediately, auto-approves release environment gates the workflow token is allowed to approve, summarizes failed child jobs with log tails, creates the draft GitHub release page up front, runs native Android qualification independently for a matching tagged Android pin (otherwise recording an explicit skip and shared mobile cutter remedy) and dispatches its publisher after the npm publisher succeeds without making GitHub finalization wait, waits for ClawHub staging only when `wait_for_clawhub=true` (the default `false` leaves that child detached), then runs the trusted-main beta verifier and uploads postpublish evidence for the GitHub release, npm package, selected plugin npm packages, staged ClawHub child workflow run IDs, and optional NPM Telegram run ID. The ClawHub bootstrap verifier requires the exact trusted-main workflow path and SHA, producer and terminal run attempts, release SHA, requested package set, immutable package artifact tuple, and terminal registry readback artifact; a successful legacy release-ref run is not accepted.
|
||||
|
||||
Core npm dispatch and environment approval start as soon as plugin npm succeeds. Once the exact `npm-release` approval succeeds, the parent proceeds without waiting for core runner allocation. ClawHub inventory authorization and optional bootstrap completion can overlap the running core publish. A failed ClawHub authorization still fails the parent and leaves the GitHub release as a draft; the parent collects any already-started core result and records its evidence.
|
||||
|
||||
|
|
@ -1159,7 +1159,7 @@ release closeout. Full Release Validation and qualified package artifacts must a
|
|||
4. Dispatch `Plugin NPM Release` with `publish_scope=all-publishable` and `ref=<release-sha>`.
|
||||
5. Dispatch `Plugin ClawHub Release` with the same scope and SHA.
|
||||
6. After plugin npm succeeds, dispatch `OpenClaw NPM Release` with the release tag, npm dist-tag, and saved `preflight_run_id` after verifying the saved `full_release_validation_run_id` and exact run attempt. ClawHub proceeds in parallel.
|
||||
7. Verify the published npm package and selector readback, then call reusable `Docker Release` with the immutable tag and SHA. Finalize the draft GitHub release after npm and Docker evidence succeeds; Docker remains part of the Gateway distribution.
|
||||
7. Verify the published npm package and selector readback, then call reusable `Docker Release` with the immutable tag and SHA. By default, finalize the draft GitHub release after npm and Docker evidence succeeds. The explicitly requested `finalize_release_before_docker=true` fast path activates after npm verification and evidence uploads, then publishes Docker; Docker remains part of the Gateway distribution.
|
||||
8. For stable, optionally dispatch `Windows Node Release` after finalization with both `windows_node_tag` and candidate-approved `windows_node_installer_digests`. It attaches signed installers and checksums to the public release as a detached child. Omit both inputs to skip Windows dispatch. When the tagged `apps/android/version.json` matches the release train, qualify and dispatch `Android Release` independently for its exact-tag signed APK, checksum, and provenance; run macOS validation/preflight/publish through `openclaw/releases` in parallel or afterward. No app workflow delays npm or GitHub release finalization. Track app failures through their summaries and evidence, then recover only the failed platform.
|
||||
|
||||
The Android train is pinned independently. If its tagged version differs from
|
||||
|
|
@ -1232,6 +1232,24 @@ gh workflow run openclaw-release-publish.yml \
|
|||
|
||||
Include `plugin_sdk_api_acknowledgement` only when the npm preflight's Plugin SDK API report contains changes.
|
||||
|
||||
An already-public GitHub release can be resumed with the same frozen inputs.
|
||||
The publisher verifies its canonical notes and any recorded release SHA, leaves
|
||||
the public page visible, and completes missing evidence assets after registry
|
||||
verification. Existing immutable evidence must match; changed notes, conflicting
|
||||
assets, or a body already handed to the post-docs publisher stop the initial
|
||||
publisher. Finalization preserves the requested `make_latest` behavior and never
|
||||
moves latest back from a newer release.
|
||||
|
||||
When the operator explicitly wants the release page visible before Docker,
|
||||
add `-f finalize_release_before_docker=true` to the direct publication command.
|
||||
The default is `false`. This path still requires successful npm publication,
|
||||
registry verification, evidence uploads, and one activation environment approval;
|
||||
it activates the page before starting Docker. Docker remains required for the
|
||||
parent to finish successfully. If Docker then fails, the page stays public and
|
||||
the Docker-only recovery command below completes the missing distribution.
|
||||
This input requires `publish_openclaw_npm=true` and cannot be combined with
|
||||
`prepared_plugins`; prepared releases retain the button's final visibility owner.
|
||||
|
||||
If a beta or regular stable package is already published but its container images are missing,
|
||||
do not rerun npm or plugin publication. Reuse the immutable release tag plus its
|
||||
successful npm preflight and Full Release Validation evidence through the
|
||||
|
|
@ -1421,6 +1439,7 @@ readback confirms that every exact package and `extended-stable` tag converged.
|
|||
- `windows_node_installer_digests`: candidate-approved compact JSON map of the current Windows installer names to pinned `sha256:` digests; required only when `windows_node_tag` is supplied
|
||||
- `npm_telegram_run_id`: optional successful `NPM Telegram Beta E2E` run id to include in final release evidence
|
||||
- `npm_dist_tag`: npm target tag for the OpenClaw package, one of `alpha`, `beta`, `latest`, or `extended-stable`
|
||||
- `finalize_release_before_docker`: explicit direct-publication fast path; default `false`. Activates the verified GitHub release before Docker, preserving the same environment approval and latest policy. Requires `publish_openclaw_npm=true` and no `prepared_plugins`. Docker failure leaves the release public for Docker-only recovery.
|
||||
- `publish_docker_only`: beta, regular stable (`latest`), or extended-stable recovery/closeout path. It requires `publish_openclaw_npm=false`, complete preflight and Full Release Validation evidence, then verifies the exact npm package, selected dist-tag, and tarball digest before invoking Docker publication.
|
||||
- `plugin_publish_scope`: defaults to `all-publishable`; use `selected` only for focused plugin-only repair work with `publish_openclaw_npm=false`
|
||||
- `plugins`: comma-separated `@openclaw/*` package names when `plugin_publish_scope=selected`
|
||||
|
|
|
|||
|
|
@ -532,60 +532,34 @@ approve_clawhub_bootstrap_environments() {
|
|||
}
|
||||
|
||||
guard_existing_public_release() {
|
||||
local release_version asset_name release_json is_draft has_sha has_proof has_asset has_canonical_body release_url release_body release_body_file
|
||||
local release_json release_body release_body_file
|
||||
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! release_json="$(gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json isDraft,assets,body,url 2>/dev/null)"; then
|
||||
if ! release_json="$(gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json isDraft,body 2>/dev/null)"; then
|
||||
return 0
|
||||
fi
|
||||
release_body="$(printf '%s' "${release_json}" | jq -er '.body | strings')" || return 1
|
||||
assert_initial_release_body "${release_body}" || return 1
|
||||
|
||||
is_draft="$(printf '%s' "${release_json}" | jq -r '.isDraft')"
|
||||
if [[ "${is_draft}" == "true" ]]; then
|
||||
if [[ "$(printf '%s' "${release_json}" | jq -r '.isDraft')" == "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
asset_name="openclaw-${release_version}-dependency-evidence.zip"
|
||||
has_sha="$(printf '%s' "${release_json}" | jq --arg sha "${TARGET_SHA}" -r '.body | contains($sha)')"
|
||||
has_proof="$(printf '%s' "${release_json}" | jq -r '.body | contains("### Release verification")')"
|
||||
has_asset="$(printf '%s' "${release_json}" | jq --arg name "${asset_name}" -r 'any(.assets[]?; .name == $name)')"
|
||||
release_url="$(printf '%s' "${release_json}" | jq -r '.url')"
|
||||
release_body="$(printf '%s' "${release_json}" | jq -r '.body')"
|
||||
release_body_file="${RUNNER_TEMP}/existing-public-release-body.md"
|
||||
printf '%s' "${release_body}" > "${release_body_file}"
|
||||
has_canonical_body="false"
|
||||
if canonical_release_body_matches "${release_body_file}"; then
|
||||
has_canonical_body="true"
|
||||
if ! canonical_release_body_matches "${release_body_file}"; then
|
||||
echo "Public release notes are no longer canonical; refusing to overwrite them." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "${has_asset}" == "true" &&
|
||||
"${has_sha}" == "true" &&
|
||||
"${has_proof}" == "true" &&
|
||||
"${has_canonical_body}" == "true" ]]; then
|
||||
return 0
|
||||
if [[ "${release_body}" != "$(cat "${prepared_release_notes_file}")" ]] &&
|
||||
! grep -Fqx -- "- release SHA: \`${TARGET_SHA}\`" "${release_body_file}"; then
|
||||
echo "Public release verification does not match release SHA ${TARGET_SHA}." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The renderer omits the verification tail when the canonical body
|
||||
# already reaches GitHub's limit. A canonical proofless body with
|
||||
# intact dependency evidence is retry-safe: postpublish re-attempts
|
||||
# the proof append on this run.
|
||||
if [[ "${has_asset}" == "true" &&
|
||||
"${has_canonical_body}" == "true" &&
|
||||
"${has_proof}" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo "Release ${RELEASE_TAG} already has a public GitHub release page without complete postpublish evidence for ${TARGET_SHA}."
|
||||
echo "Refusing to reuse a public prerelease tag after publication started: ${release_url}"
|
||||
echo "Create a new beta tag or delete/draft the incomplete public release before retrying."
|
||||
} >&2
|
||||
exit 1
|
||||
# A partial public release is resumable. The upload owner compares existing
|
||||
# immutable evidence and attaches missing assets after registry verification.
|
||||
echo "- GitHub release: resuming canonical public page; evidence will be verified and completed" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
resolve_openclaw_npm_publish_state() {
|
||||
|
|
|
|||
|
|
@ -19624,10 +19624,16 @@ it.each(["publish", "promote"])(
|
|||
runInNewContext(finalize.if.replace(/^\$\{\{|\}\}$/gu, ""), {
|
||||
always: () => true,
|
||||
contains: (value: string, part: string) => value.includes(part),
|
||||
inputs: { tag: "v2026.9.4", prepared_plugins: "", publish_openclaw_npm: true },
|
||||
inputs: {
|
||||
tag: "v2026.9.4",
|
||||
prepared_plugins: "",
|
||||
publish_openclaw_npm: true,
|
||||
finalize_release_before_docker: false,
|
||||
},
|
||||
needs: {
|
||||
publish: { result: "success" },
|
||||
publish_docker: { result: "success" },
|
||||
finalize_github_release_before_docker: { result: "skipped" },
|
||||
verify: { result: "success" },
|
||||
[approvalId]: { result },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4915,6 +4915,27 @@ describe("package acceptance workflow", () => {
|
|||
expect(result.status, result.stderr).toBe(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ core: "true", prepared: "", allowed: true },
|
||||
{ core: "false", prepared: "", allowed: false },
|
||||
{ core: "true", prepared: '{"npm":{},"clawhub":{}}', allowed: false },
|
||||
])(
|
||||
"admits early GitHub activation only for direct core publication: $core/$prepared",
|
||||
({ core, prepared, allowed }) => {
|
||||
const result = runReleasePublishInputValidation({
|
||||
FINALIZE_RELEASE_BEFORE_DOCKER: "true",
|
||||
PUBLISH_OPENCLAW_NPM: core,
|
||||
PREPARED_PLUGINS: prepared,
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(allowed ? 0 : 1);
|
||||
if (!allowed) {
|
||||
expect(result.stderr).toContain(
|
||||
"finalize_release_before_docker requires direct publication",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("allows Docker-only recovery for beta, stable, and extended-stable releases", () => {
|
||||
for (const release of [
|
||||
{ distTag: "beta", tag: "v2026.8.1-beta.2" },
|
||||
|
|
@ -6069,7 +6090,8 @@ render_github_release_notes() { cp "$2" "$1"; printf '%s\\n' '{"verificationIncl
|
|||
);
|
||||
expect(events).toContain("release-evidence");
|
||||
expect(events).not.toContain("windows");
|
||||
expect(fixture.summary()).toContain("left as draft");
|
||||
expect(fixture.summary()).toContain("evidence updated; a required publish child failed");
|
||||
expect(fixture.summary()).not.toContain("left as draft");
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -6671,6 +6693,7 @@ wait_for_run openclaw-npm-release.yml 404 "$EXPECTED_SHA" "$STARTED_JOB" "$APPRO
|
|||
"publish",
|
||||
"publish_docker",
|
||||
"approve_github_release",
|
||||
"finalize_github_release_before_docker",
|
||||
]);
|
||||
expect(nativeJob["continue-on-error"]).toBe(true);
|
||||
expect(androidJob["continue-on-error"]).toBe(true);
|
||||
|
|
@ -13922,7 +13945,12 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
expect(createReleaseIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(verifyReleaseIndex).toBeGreaterThan(createReleaseIndex);
|
||||
expect(appendProofIndex).toBeGreaterThan(verifyReleaseIndex);
|
||||
expect(finalizeJob.needs).toEqual(["publish", "publish_docker", "approve_github_release"]);
|
||||
expect(finalizeJob.needs).toEqual([
|
||||
"publish",
|
||||
"publish_docker",
|
||||
"approve_github_release",
|
||||
"finalize_github_release_before_docker",
|
||||
]);
|
||||
expect(finalizeJob.if).toContain("needs.publish_docker.result == 'success'");
|
||||
expect(finalizeJob.if).toContain("inputs.prepared_plugins == ''");
|
||||
expect(finalizeJob.if).toContain("needs.approve_github_release.result == 'success'");
|
||||
|
|
@ -14132,7 +14160,12 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
expect(workflow.on?.workflow_dispatch?.inputs?.[input]).toMatchObject({ required: false });
|
||||
}
|
||||
expect(publish.needs).toEqual(["resolve_release_target"]);
|
||||
expect(finalize.needs).toEqual(["publish", "publish_docker", "approve_github_release"]);
|
||||
expect(finalize.needs).toEqual([
|
||||
"publish",
|
||||
"publish_docker",
|
||||
"approve_github_release",
|
||||
"finalize_github_release_before_docker",
|
||||
]);
|
||||
expect(windows.needs).toEqual(["resolve_release_target", "finalize_github_release"]);
|
||||
expect(windows["continue-on-error"]).toBe(true);
|
||||
expect(windows.if).toContain("needs.finalize_github_release.result == 'success'");
|
||||
|
|
|
|||
|
|
@ -1994,6 +1994,7 @@ describe("release validation no-push transport", () => {
|
|||
"resolve_release_target",
|
||||
"publish",
|
||||
"verify_core_npm_registry",
|
||||
"finalize_github_release_before_docker",
|
||||
]);
|
||||
expect(dockerCall.if).toContain("needs.publish.result == 'success'");
|
||||
expect(dockerCall.if).toContain("needs.verify_core_npm_registry.result == 'success'");
|
||||
|
|
@ -2036,6 +2037,7 @@ describe("release validation no-push transport", () => {
|
|||
"publish",
|
||||
"publish_docker",
|
||||
"approve_github_release",
|
||||
"finalize_github_release_before_docker",
|
||||
]);
|
||||
|
||||
const identity = step(
|
||||
|
|
@ -2051,8 +2053,15 @@ describe("release validation no-push transport", () => {
|
|||
expect(reusablePermissionViolations(DOCKER_RELEASE, "prepare")).toEqual([]);
|
||||
});
|
||||
|
||||
it("finalizes npm-only alpha releases while retaining required Docker gates for other trains", () => {
|
||||
it("keeps Docker required by default and activates early only after explicit approval", () => {
|
||||
const workflow = readWorkflow(".github/workflows/openclaw-release-publish.yml");
|
||||
expect(workflow.on?.workflow_dispatch?.inputs?.finalize_release_before_docker).toMatchObject({
|
||||
type: "boolean",
|
||||
default: false,
|
||||
});
|
||||
const early = job(workflow, "finalize_github_release_before_docker");
|
||||
expect(early.needs).toEqual(["publish", "approve_github_release_before_docker"]);
|
||||
expect(early.steps).toEqual(job(workflow, "finalize_github_release").steps);
|
||||
const cases = [
|
||||
{
|
||||
tag: "v2026.9.1-alpha.1",
|
||||
|
|
@ -2090,14 +2099,52 @@ describe("release validation no-push transport", () => {
|
|||
publishDocker: false,
|
||||
finalize: false,
|
||||
},
|
||||
{
|
||||
tag: "v2026.9.1",
|
||||
npm: "success",
|
||||
docker: "failure",
|
||||
beforeDocker: true,
|
||||
early: "success",
|
||||
publishDocker: true,
|
||||
finalize: false,
|
||||
},
|
||||
{
|
||||
tag: "v2026.9.1",
|
||||
npm: "success",
|
||||
docker: "success",
|
||||
beforeDocker: true,
|
||||
early: "success",
|
||||
publishDocker: true,
|
||||
finalize: true,
|
||||
},
|
||||
{
|
||||
tag: "v2026.9.1",
|
||||
npm: "failure",
|
||||
docker: "skipped",
|
||||
beforeDocker: true,
|
||||
early: "skipped",
|
||||
publishDocker: false,
|
||||
finalize: false,
|
||||
},
|
||||
{
|
||||
tag: "v2026.9.1",
|
||||
npm: "success",
|
||||
docker: "skipped",
|
||||
beforeDocker: true,
|
||||
early: "failure",
|
||||
publishDocker: false,
|
||||
finalize: false,
|
||||
},
|
||||
];
|
||||
for (const scenario of cases) {
|
||||
const beforeDocker = scenario.beforeDocker === true;
|
||||
const evaluate = (name: string, preparedPlugins = "") =>
|
||||
runInNewContext(job(workflow, name).if!.slice(3, -2), {
|
||||
always: () => true,
|
||||
contains: (value: string, search: string) => value.includes(search),
|
||||
inputs: {
|
||||
tag: scenario.tag,
|
||||
finalize_release_before_docker: beforeDocker,
|
||||
publish_openclaw_npm: true,
|
||||
publish_docker_only: false,
|
||||
prepared_plugins: preparedPlugins,
|
||||
|
|
@ -2105,10 +2152,19 @@ describe("release validation no-push transport", () => {
|
|||
needs: {
|
||||
publish: { result: scenario.npm },
|
||||
publish_docker: { result: scenario.docker },
|
||||
approve_github_release: { result: "success" },
|
||||
approve_github_release: { result: beforeDocker ? "skipped" : "success" },
|
||||
approve_github_release_before_docker: { result: beforeDocker ? "success" : "skipped" },
|
||||
finalize_github_release_before_docker: { result: scenario.early ?? "skipped" },
|
||||
verify_core_npm_registry: { result: "skipped" },
|
||||
},
|
||||
});
|
||||
expect(evaluate("approve_github_release_before_docker")).toBe(
|
||||
beforeDocker && scenario.npm === "success",
|
||||
);
|
||||
expect(evaluate("approve_github_release")).toBe(!beforeDocker && scenario.finalize);
|
||||
expect(evaluate("finalize_github_release_before_docker")).toBe(
|
||||
beforeDocker && scenario.npm === "success",
|
||||
);
|
||||
expect(evaluate("publish_docker"), JSON.stringify(scenario)).toBe(scenario.publishDocker);
|
||||
expect(evaluate("finalize_github_release"), JSON.stringify(scenario)).toBe(scenario.finalize);
|
||||
expect(evaluate("finalize_github_release", '{"npm":{},"clawhub":{}}')).toBe(false);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { createScriptTestHarness } from "./test-helpers.js";
|
|||
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
|
||||
it("renders and verifies an old pinned target using trusted publication tooling", () => {
|
||||
function publicationFixture() {
|
||||
const root = realpathSync(createTempDir("release-publish-historical-tooling-"));
|
||||
const repository = resolve(".");
|
||||
mkdirSync(join(root, "scripts"));
|
||||
|
|
@ -59,6 +59,11 @@ it("renders and verifies an old pinned target using trusted publication tooling"
|
|||
copyFileSync(join(repository, source), join(root, ".release-harness", source));
|
||||
}
|
||||
symlinkSync(join(repository, "node_modules"), join(root, "node_modules"), "dir");
|
||||
return { root, repository, targetSha };
|
||||
}
|
||||
|
||||
it("renders and verifies an old pinned target using trusted publication tooling", () => {
|
||||
const { root, repository, targetSha } = publicationFixture();
|
||||
const workflow = parse(
|
||||
readFileSync(join(repository, ".github/workflows/openclaw-release-publish.yml"), "utf8"),
|
||||
);
|
||||
|
|
@ -222,3 +227,89 @@ gh() {
|
|||
expect(existsSync(commands)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ state: "missing", body: "canonical", error: undefined },
|
||||
{ state: "matching", body: "proof", error: undefined },
|
||||
{ state: "different", body: "canonical", error: "differs from this release run" },
|
||||
{ state: "missing", body: "different", error: "Public release notes are no longer canonical" },
|
||||
{ state: "missing", body: "wrong-sha", error: "does not match" },
|
||||
])("resumes a public release with $state evidence and $body notes", ({ state, body, error }) => {
|
||||
const { root, targetSha } = publicationFixture();
|
||||
const notes = "## 2026.9.4\n\n### Fixes\n\n- Frozen release fix.";
|
||||
const releaseBody =
|
||||
body === "different"
|
||||
? "Unrelated release notes"
|
||||
: body === "proof" || body === "wrong-sha"
|
||||
? `${notes}\n\n### Release verification\n\n- release SHA: \`${body === "wrong-sha" ? "b".repeat(40) : targetSha}\``
|
||||
: notes;
|
||||
const assetName = "openclaw-2026.9.4-release-manifest.json";
|
||||
writeFileSync(
|
||||
join(root, "release.json"),
|
||||
JSON.stringify({
|
||||
isDraft: false,
|
||||
body: releaseBody,
|
||||
assets: state === "missing" ? [] : [{ name: assetName }],
|
||||
url: "https://github.com/fixture/repository/releases/tag/v2026.9.4",
|
||||
}),
|
||||
);
|
||||
writeFileSync(join(root, "manifest.json"), '{"source":"frozen"}');
|
||||
writeFileSync(
|
||||
join(root, "existing.json"),
|
||||
state === "different" ? '{"source":"changed"}' : '{"source":"frozen"}',
|
||||
);
|
||||
const result = spawnSync(
|
||||
"bash",
|
||||
[
|
||||
"-c",
|
||||
`
|
||||
source "$GITHUB_WORKSPACE/.release-harness/scripts/lib/release-publish-children.sh"
|
||||
verify_release_tag_target() { :; }
|
||||
gh() {
|
||||
case "$1 $2" in
|
||||
"release view") cat "$RUNNER_TEMP/release.json" ;;
|
||||
"release download")
|
||||
local destination=""
|
||||
while (( $# > 0 )); do
|
||||
if [[ "$1" == --dir ]]; then destination="$2"; break; fi
|
||||
shift
|
||||
done
|
||||
cp "$RUNNER_TEMP/existing.json" "$destination/$ASSET_NAME"
|
||||
;;
|
||||
"release upload") printf '%s\\n' "$@" >> "$RUNNER_TEMP/uploads" ;;
|
||||
*) echo "Unexpected mutation: $*" >&2; return 1 ;;
|
||||
esac
|
||||
}
|
||||
prepared_release_notes_file="$RUNNER_TEMP/prepared.md"
|
||||
render_github_release_notes "$prepared_release_notes_file"
|
||||
guard_existing_public_release
|
||||
create_or_update_github_release
|
||||
attach_or_verify_release_asset "$RUNNER_TEMP/manifest.json" "$ASSET_NAME"
|
||||
`,
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...createNestedGitEnv(),
|
||||
GITHUB_WORKSPACE: root,
|
||||
RUNNER_TEMP: root,
|
||||
GITHUB_STEP_SUMMARY: join(root, "summary"),
|
||||
GITHUB_REPOSITORY: "fixture/repository",
|
||||
RELEASE_TAG: "v2026.9.4",
|
||||
TARGET_SHA: targetSha,
|
||||
GITHUB_REF: "refs/tags/release-publish/aaaaaaaaaaaa-1",
|
||||
PARENT_WORKFLOW_SHA: "a".repeat(40),
|
||||
PUBLISH_OPENCLAW_NPM: "true",
|
||||
RELEASE_NPM_DIST_TAG: "latest",
|
||||
ASSET_NAME: assetName,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result.status, result.stderr).toBe(error ? 1 : 0);
|
||||
if (error) {
|
||||
expect(result.stderr).toContain(error);
|
||||
}
|
||||
expect(existsSync(join(root, "uploads"))).toBe(state === "missing" && !error);
|
||||
expect(JSON.parse(readFileSync(join(root, "release.json"), "utf8")).body).toBe(releaseBody);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -913,7 +913,12 @@ describe("Vercel Container Registry publishing", () => {
|
|||
expect(releasePublish.secrets).toEqual({
|
||||
VERCEL_TOKEN: "${{ secrets.VERCEL_TOKEN }}",
|
||||
});
|
||||
expect(finalizeRelease.needs).toEqual(["publish", "publish_docker", "approve_github_release"]);
|
||||
expect(finalizeRelease.needs).toEqual([
|
||||
"publish",
|
||||
"publish_docker",
|
||||
"approve_github_release",
|
||||
"finalize_github_release_before_docker",
|
||||
]);
|
||||
expect(finalizeRelease.if).not.toContain("publish_vcr");
|
||||
expect(recoveryValidation.if).toBe("${{ !inputs.advisory }}");
|
||||
expect(recoveryValidation.permissions).toEqual({});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue