mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
ci: publish extended-stable GitHub releases
This commit is contained in:
@@ -235,9 +235,9 @@ every human `Thanks @...` attribution.
|
||||
--release-tag v<YYYY.M.PATCH> \
|
||||
--check-github
|
||||
```
|
||||
- add one `--release-tag` for every beta and stable page in the train; a
|
||||
`### Release verification` tail is permitted, but any other body drift
|
||||
fails the check
|
||||
- add one `--release-tag` for every beta, stable, and extended-stable page in
|
||||
the train; a `### Release verification` tail is permitted, but any other
|
||||
body drift fails the check
|
||||
- `scripts/render-github-release-notes.mts` is the canonical release-body
|
||||
renderer used by candidate validation, publish, and verification. When the
|
||||
complete `## YYYY.M.PATCH` section fits GitHub's 125,000-character limit and
|
||||
@@ -269,11 +269,13 @@ every human `Thanks @...` attribution.
|
||||
|
||||
## Extended-Stable Variant
|
||||
|
||||
Extended-stable has one release commit and no GitHub Release body. After version
|
||||
prep and approved backports, regenerate `## YYYY.M.P` with the regular manifest
|
||||
and original-main-PR provenance rules. Land it by PR, then validate the final
|
||||
branch tip before tagging. Re-audit after a product backport; a tooling-only
|
||||
repair needs no changelog entry. Never rewrite a published tag or changelog.
|
||||
Extended-stable has one release commit and one canonical GitHub Release body.
|
||||
After version prep and approved backports, regenerate `## YYYY.M.P` with the
|
||||
regular manifest and original-main-PR provenance rules. Land it by PR, then
|
||||
validate the final branch tip before tagging. The Docker closeout renders that
|
||||
tag-owned section into a non-Latest release page. Re-audit after a product
|
||||
backport; a tooling-only repair needs no changelog entry. Never rewrite a
|
||||
published tag or changelog.
|
||||
|
||||
## Quota / API Outage Rule
|
||||
|
||||
|
||||
@@ -317,11 +317,11 @@ node scripts/full-release-validation-at-sha.mjs \
|
||||
--workflow-sha "$TOOLING_SHA"
|
||||
```
|
||||
|
||||
That helper is for regular releases. Extended-stable dispatches Full Release
|
||||
Validation directly from and against `extended-stable/YYYY.M.33` with
|
||||
`release_profile=stable`; its exact branch-tip evidence is fresh and cannot be
|
||||
replaced by a `release-ci/*` run. Use `$release-openclaw-ci` for its failure
|
||||
classification and run-identity rules.
|
||||
Extended-stable may use the helper's trusted main-pinned `release-ci/*` harness
|
||||
or a direct run from `extended-stable/YYYY.M.33` with
|
||||
`release_profile=stable`. In either case, require a complete v3 manifest that
|
||||
binds the exact canonical branch tip, workflow SHA, and run attempt. Use
|
||||
`$release-openclaw-ci` for failure classification and identity rules.
|
||||
|
||||
The helper verifies and pins the recorded Tooling SHA on trusted `main`, passes
|
||||
the resolved Code SHA as `expected_sha`, and records the canonical release
|
||||
|
||||
@@ -13,7 +13,8 @@ user.
|
||||
|
||||
Before drafting focus areas, read real release evidence:
|
||||
|
||||
1. GitHub release body, or the immutable tag and publish run for extended-stable.
|
||||
1. GitHub release body and immutable tag; for extended-stable, also confirm the
|
||||
npm/container-only scope and non-Latest classification.
|
||||
2. `CHANGELOG.md` section for the released base version.
|
||||
3. Commits since the previous shipped version or the operator-specified base.
|
||||
4. Registry/package metadata for the exact version and current dist-tag.
|
||||
@@ -79,8 +80,8 @@ openclaw --version
|
||||
|
||||
Do not add `--yes`: users moving from newer regular stable must see the downgrade
|
||||
warning because older versions may not understand newer configuration. Link the
|
||||
tag or changelog; do not imply a GitHub Release or inherit regular stable
|
||||
macOS, Windows, ClawHub, `latest`, or website claims.
|
||||
GitHub Release, but do not inherit regular stable macOS, Windows, ClawHub,
|
||||
`latest`, or website claims.
|
||||
|
||||
## Style
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface:
|
||||
display_name: "OpenClaw Release Announcement"
|
||||
short_description: "Draft Discord beta/stable release announcements from evidence."
|
||||
default_prompt: "Use this skill to draft an OpenClaw beta or stable Discord announcement from changelog, release notes, npm/GitHub release proof, and validation evidence."
|
||||
short_description: "Draft Discord beta/stable/extended-stable release announcements from evidence."
|
||||
default_prompt: "Use this skill to draft an OpenClaw beta, stable, or extended-stable Discord announcement from changelog, release notes, npm/GitHub release proof, and validation evidence."
|
||||
|
||||
@@ -214,9 +214,9 @@ publish workflow reads the effective profile from the full-validation manifest.
|
||||
|
||||
### Extended-stable validation
|
||||
|
||||
For `.33+`, dispatch from and target the canonical branch. This direct route is
|
||||
intentional: downstream extended-stable evidence requires the canonical branch
|
||||
identity, while Telegram still authenticates the exact branch SHA:
|
||||
For `.33+`, prefer the trusted main-pinned helper so current workflow tooling
|
||||
targets the immutable canonical branch tip. A direct canonical-branch run is
|
||||
also valid:
|
||||
|
||||
```bash
|
||||
RELEASE_SHA="$(git rev-parse HEAD)"
|
||||
@@ -227,10 +227,12 @@ gh workflow run full-release-validation.yml \
|
||||
-f release_profile=stable
|
||||
```
|
||||
|
||||
Accept only a complete `rerun_group=all` run whose branch, head/target SHAs,
|
||||
manifest `workflowRef`, and package versions identify the same commit. Save its
|
||||
successful `run_attempt` and require the final tag to resolve there. Reject
|
||||
`release-ci/*`, current-main, narrow, and earlier-attempt evidence.
|
||||
Accept only a complete `rerun_group=all` run from the canonical branch, from
|
||||
current `main` whose workflow SHA remains reachable from `main`, or from the
|
||||
trusted main-pinned `release-ci/*` harness with a v3 exact-target manifest.
|
||||
Require the head/target SHA, manifest identity, package versions, saved
|
||||
`run_attempt`, and final tag to identify the same commit. Reject narrow,
|
||||
untrusted-main, mismatched-target, and earlier-attempt evidence.
|
||||
|
||||
Product failures need an approved backport. Frozen-target tooling failures need
|
||||
the smallest behavior-preserving repair. Provider, approval, runner, or log
|
||||
|
||||
@@ -8,8 +8,8 @@ description: "Run or recover OpenClaw macOS release signing, notarization, appca
|
||||
Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, `$one-password`, and `$release-private` if it exists when stable macOS assets, release-ops mac preflight, notarization, appcast promotion, or mac release recovery is involved.
|
||||
|
||||
This is a regular stable-release skill. Do not invoke it for extended-stable;
|
||||
that track does not inherit macOS assets, appcast promotion, or a GitHub Release
|
||||
unless the current extended-stable release policy explicitly adds them.
|
||||
that track's notes-only GitHub Release does not inherit macOS assets or appcast
|
||||
promotion.
|
||||
|
||||
## Credentials
|
||||
|
||||
|
||||
@@ -303,8 +303,12 @@ on pinned current `main` as the exact command and validation contract.
|
||||
digest and refuse automatic rollback. For alias repair, dispatch the
|
||||
approval-gated `docker-channel-promote.yml` from current `main` with the exact
|
||||
tag; never rebuild or move the release tag.
|
||||
10. Do not create a GitHub Release or publish macOS, Windows, mobile, website,
|
||||
ClawHub, or private dist-tag artifacts from this path.
|
||||
10. Dispatch the Docker-only `OpenClaw Release Publish` closeout. It must
|
||||
reverify the exact npm selector and tarball, prepare a canonical
|
||||
non-prerelease GitHub Release draft with `latest=false`, publish Docker,
|
||||
and only then make the release public. Do not publish macOS, Windows,
|
||||
mobile, website, ClawHub, npm `latest`, or private dist-tag artifacts from
|
||||
this path.
|
||||
|
||||
## Keep release channel naming aligned
|
||||
|
||||
@@ -512,8 +516,8 @@ HEAD/worktree-bound manifest under git metadata for cutover review.
|
||||
body tail. Do not discard a fitting full contribution record to make room
|
||||
for proof.
|
||||
- Before publishing or closing a release, run
|
||||
`$openclaw-changelog-update`'s `verify-release-notes.mjs` with every stable
|
||||
and beta release tag in the train. Do not publish or leave a page live when
|
||||
`$openclaw-changelog-update`'s `verify-release-notes.mjs` with every stable,
|
||||
beta, and extended-stable release tag in the train. Do not publish or leave a page live when
|
||||
it is missing a source-history reference, eligible human credit, or the
|
||||
complete matching changelog body.
|
||||
- Treat the selected `--base` as a strict history boundary: it must be an
|
||||
|
||||
@@ -24,8 +24,9 @@ extended-stable package and publication constraints.
|
||||
- Carry the complete current-main Docker release-channel unit in the tagged
|
||||
tree: workflow, promoter, policy, shared release-version classifier, tests,
|
||||
and workflow validation. GitHub evaluates tag-push workflows from that tree.
|
||||
- Exclude ClawHub publication, GitHub Releases, the macOS app, Windows Hub,
|
||||
mobile apps, website downloads, and private-repository dist-tags.
|
||||
- Exclude ClawHub publication, GitHub Release assets, the macOS app, Windows
|
||||
Hub, mobile apps, website downloads, npm `latest`, and private-repository
|
||||
dist-tags. The Docker closeout creates a notes-only GitHub Release page.
|
||||
- Review the complete mainline delta using the shared evidence-driven audit.
|
||||
Do not stop after the first obvious fixes or consider public PRs, titles, or
|
||||
dependency bumps the complete source set.
|
||||
@@ -161,7 +162,8 @@ fi
|
||||
```
|
||||
|
||||
Do not use GitHub's latest nonprerelease Release as the source of truth. The
|
||||
extended-stable lane intentionally creates no GitHub Release. In bootstrap
|
||||
npm `extended-stable` selector remains authoritative for the active line; its
|
||||
notes-only GitHub Release is always created with `latest=false`. In bootstrap
|
||||
mode, record the approving maintainer and approved base commit. Stop before
|
||||
discovery or mutation if npm, the canonical branch, tags, package versions,
|
||||
approved base, or protected `main` disagree.
|
||||
@@ -238,6 +240,8 @@ path alone.
|
||||
out of scope.
|
||||
- Treat macOS-app-only, Windows-Hub-only, mobile-only, website-only, and GitHub
|
||||
Release-only fixes as `skip` for this Gateway extended-stable line.
|
||||
- Keep GitHub Release automation repairs in trusted current-main release
|
||||
tooling; they are not product backports for the maintenance branch.
|
||||
- Treat cross-repository or package-topology uncertainty as `blocked` until the
|
||||
shipped npm surface and release owner are proven.
|
||||
|
||||
@@ -369,8 +373,9 @@ Report:
|
||||
harness compatibility repair, and superseded validation runs;
|
||||
- remaining security, release, or maintainer approvals;
|
||||
- the coordinated PR URL or why no PR was opened;
|
||||
- exact intended Docker images and aliases, plus explicit confirmation that no
|
||||
other non-npm publication is planned.
|
||||
- exact intended Docker images and aliases, plus explicit confirmation that
|
||||
the notes-only, non-Latest GitHub Release is the only other publication
|
||||
planned.
|
||||
|
||||
Then follow the parent skill's publish and recovery sequence. Keep exact
|
||||
branch/tag/package/run identity, never republish for selector repair, and move
|
||||
|
||||
@@ -33,6 +33,7 @@ operator steering. Do not preserve superseded scope.
|
||||
- Plugin NPM Release: `<run id / URL or none>`
|
||||
- publish parent: `<run id / URL or none>`
|
||||
- Docker release/repair: `<run ids / tag / aliases or none>`
|
||||
- GitHub Release: `<draft/public / numeric id / non-Latest readback or none>`
|
||||
- immutable successful children: `<run ids / artifacts or none>`
|
||||
- registry/provenance readback: `<artifact or command result>`
|
||||
|
||||
|
||||
@@ -121,8 +121,10 @@ Use `source=npm -f package_spec=openclaw@beta` for published beta proof. Keep
|
||||
`workflow_ref` as trusted current harness code unless the release process says
|
||||
otherwise.
|
||||
|
||||
For extended-stable, branch-owned Full Release Validation is publication
|
||||
evidence; Package Acceptance is a post-publish selector smoke:
|
||||
For extended-stable, complete exact-target Full Release Validation from the
|
||||
canonical branch, trusted reachable current-main tooling, or the main-pinned
|
||||
`release-ci/*` harness is publication evidence; Package Acceptance is a
|
||||
post-publish selector smoke:
|
||||
|
||||
```bash
|
||||
gh workflow run package-acceptance.yml \
|
||||
|
||||
@@ -14,8 +14,9 @@ publish skill; use `$release-openclaw-maintainer` before changing release state.
|
||||
- Resolve short suffixes like `.27` to the concrete CalVer version from the
|
||||
current date/context, then say the resolved version.
|
||||
- Resolve the track first. Regular beta/stable uses a GitHub Release and the
|
||||
platform graph; extended-stable uses its canonical branch, npm selector, and
|
||||
Gateway surfaces. Do not require one track's artifacts from the other.
|
||||
platform graph; extended-stable uses a notes-only GitHub Release, its
|
||||
canonical branch, npm selector, and Gateway surfaces. Do not require one
|
||||
track's artifacts from the other.
|
||||
- Verify live state. Do not trust local checkout state, release notes, or old
|
||||
memory as current truth.
|
||||
- If the checkout is dirty or divergent, use it only for scripts/reference.
|
||||
@@ -76,13 +77,15 @@ Use these checks only for the regular orchestrated release track.
|
||||
|
||||
## Extended-stable checks
|
||||
|
||||
Extended-stable has no GitHub Release ledger. Verify live tag, workflow,
|
||||
registry, provenance, and image state directly.
|
||||
Extended-stable has a notes-only GitHub Release ledger. Verify it alongside
|
||||
the live tag, workflow, registry, provenance, and image state.
|
||||
|
||||
1. **Identity:** require final `v<VERSION>` at patch `33+`, with no suffix,
|
||||
contained in `extended-stable/YYYY.M.33`. Only an active candidate must equal
|
||||
the tip. Root and every publishable official plugin must declare `<VERSION>`.
|
||||
Require the Git tag and no GitHub Release.
|
||||
Require the Git tag and a public, non-prerelease GitHub Release whose title
|
||||
and canonical body match the tag. Require `isLatest=false` and no native or
|
||||
platform assets.
|
||||
2. **Workflow chain:** find successful preflight, complete validation, plugin
|
||||
npm, and core publish runs on the canonical branch and SHA. Validation must
|
||||
use `rerun_group=all`, `release_profile=stable`, blocking soak/performance,
|
||||
@@ -103,7 +106,8 @@ Promotion` for the exact tag, without rebuilding.
|
||||
6. **Recovery:** never republish. Use the generated command only for the root
|
||||
selector and approved credential-isolated tooling for others, then repeat
|
||||
complete readback. Do not require ClawHub, native/mobile apps, website,
|
||||
private dist-tags, regular `latest`, or a GitHub Release.
|
||||
private dist-tags, or regular `latest`. Require the notes-only GitHub
|
||||
Release, but do not require regular-release assets or platform evidence.
|
||||
|
||||
## Shared live smoke
|
||||
|
||||
|
||||
+2
-1
@@ -62,8 +62,9 @@
|
||||
/docs/reference/secretref-credential-surface.md @openclaw/openclaw-secops
|
||||
/docs/reference/secretref-user-supplied-credentials-matrix.json @openclaw/openclaw-secops
|
||||
|
||||
# Release workflow and its supporting release-path checks.
|
||||
# Release workflows and their supporting release-path checks.
|
||||
/.github/workflows/openclaw-npm-release.yml @openclaw/openclaw-release-managers
|
||||
/.github/workflows/openclaw-release-publish.yml @openclaw/openclaw-release-managers
|
||||
/docs/reference/RELEASING.md @openclaw/openclaw-release-managers
|
||||
/scripts/openclaw-npm-publish.sh @openclaw/openclaw-release-managers
|
||||
/scripts/openclaw-npm-release-check.ts @openclaw/openclaw-release-managers
|
||||
|
||||
@@ -46,8 +46,50 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release_policy:
|
||||
name: Classify release trigger
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.release.outputs.should_run }}
|
||||
steps:
|
||||
- name: Download trusted release classifier
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh api "repos/${GITHUB_REPOSITORY}/contents/scripts/lib/release-version.mjs?ref=${WORKFLOW_SHA}" \
|
||||
--jq .content | base64 --decode > "${RUNNER_TEMP}/release-version.mjs"
|
||||
|
||||
- name: Classify release track
|
||||
id: release
|
||||
env:
|
||||
CLASSIFIER_FILE: ${{ runner.temp }}/release-version.mjs
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
|
||||
run: |
|
||||
if [[ "${EVENT_NAME}" != "release" ]]; then
|
||||
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
node --input-type=module <<'NODE'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const { classifyReleaseTrain, parseReleaseVersion } = await import(
|
||||
pathToFileURL(process.env.CLASSIFIER_FILE).href
|
||||
);
|
||||
|
||||
const parsed = parseReleaseVersion(process.env.RELEASE_TAG.replace(/^v/u, ""));
|
||||
const track = parsed === null ? "unknown" : classifyReleaseTrain(parsed);
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `should_run=${track !== "extended-stable"}\n`);
|
||||
NODE
|
||||
|
||||
resolve-base:
|
||||
needs: release_policy
|
||||
if: >-
|
||||
needs.release_policy.outputs.should_run == 'true' &&
|
||||
github.repository == 'openclaw/openclaw' &&
|
||||
(github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -12,7 +12,36 @@ jobs:
|
||||
dispatch-translate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download trusted release classifier
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh api "repos/${GITHUB_REPOSITORY}/contents/scripts/lib/release-version.mjs?ref=${WORKFLOW_SHA}" \
|
||||
--jq .content | base64 --decode > "${RUNNER_TEMP}/release-version.mjs"
|
||||
|
||||
- name: Classify release track
|
||||
id: release
|
||||
env:
|
||||
CLASSIFIER_FILE: ${{ runner.temp }}/release-version.mjs
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
node --input-type=module <<'NODE'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const { classifyReleaseTrain, parseReleaseVersion } = await import(
|
||||
pathToFileURL(process.env.CLASSIFIER_FILE).href
|
||||
);
|
||||
|
||||
const parsed = parseReleaseVersion(process.env.RELEASE_TAG.replace(/^v/u, ""));
|
||||
const track = parsed === null ? "unknown" : classifyReleaseTrain(parsed);
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `track=${track}\n`);
|
||||
NODE
|
||||
|
||||
- name: Trigger translation coordinator in publish repo
|
||||
if: ${{ steps.release.outputs.track != 'extended-stable' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
|
||||
@@ -2469,13 +2469,17 @@ jobs:
|
||||
path: ${{ runner.temp }}/openclaw-release-postpublish-evidence
|
||||
if-no-files-found: error
|
||||
|
||||
verify_core_npm_registry:
|
||||
name: Verify already-published core npm package
|
||||
prepare_extended_stable_release:
|
||||
name: Verify npm and prepare extended-stable release
|
||||
needs: [resolve_release_target]
|
||||
if: ${{ inputs.publish_docker_only }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm-release
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
outputs:
|
||||
release_id: ${{ steps.release.outputs.release_id }}
|
||||
release_body_sha256: ${{ steps.release.outputs.release_body_sha256 }}
|
||||
steps:
|
||||
- name: Verify exact npm and selector readback matches preflight bytes
|
||||
env:
|
||||
@@ -2501,10 +2505,175 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout release SHA
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ needs.resolve_release_target.outputs.sha }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout trusted release tooling
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: .release-harness
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Create or resume the canonical draft release
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
release_title="openclaw ${release_version}"
|
||||
notes_file="${RUNNER_TEMP}/extended-stable-release-notes.md"
|
||||
verification_file="${RUNNER_TEMP}/extended-stable-release-verification.md"
|
||||
|
||||
verify_release_tag_target() {
|
||||
local direct_sha peeled_sha remote_refs remote_sha
|
||||
remote_refs="$(git ls-remote --tags origin \
|
||||
"refs/tags/${RELEASE_TAG}" \
|
||||
"refs/tags/${RELEASE_TAG}^{}")"
|
||||
direct_sha="$(printf '%s\n' "${remote_refs}" |
|
||||
awk -v ref="refs/tags/${RELEASE_TAG}" '$2 == ref { print $1 }')"
|
||||
peeled_sha="$(printf '%s\n' "${remote_refs}" |
|
||||
awk -v ref="refs/tags/${RELEASE_TAG}^{}" '$2 == ref { print $1 }')"
|
||||
remote_sha="${peeled_sha:-${direct_sha}}"
|
||||
if [[ -z "${remote_sha}" || "${remote_sha}" != "${TARGET_SHA}" ]]; then
|
||||
echo "Release tag ${RELEASE_TAG} must still resolve to ${TARGET_SHA}; found ${remote_sha:-<missing>}." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_release_id() {
|
||||
gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
|
||||
--json databaseId --jq '.databaseId' 2>/dev/null || true
|
||||
}
|
||||
|
||||
wait_for_release_id() {
|
||||
local attempt release_id
|
||||
for attempt in $(seq 1 12); do
|
||||
release_id="$(resolve_release_id)"
|
||||
if [[ "${release_id}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
printf '%s\n' "${release_id}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "${attempt}" != "12" ]]; then
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_release_resource() {
|
||||
local release_id="$1"
|
||||
local expected_draft="${2:-}"
|
||||
local body_file="${RUNNER_TEMP}/extended-stable-existing-release.md"
|
||||
local resource_file="${RUNNER_TEMP}/extended-stable-release.json"
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" > "${resource_file}"
|
||||
jq -r '.body // ""' "${resource_file}" > "${body_file}"
|
||||
RELEASE_BODY_FILE="${body_file}" \
|
||||
RELEASE_CHANGELOG_FILE="${GITHUB_WORKSPACE}/CHANGELOG.md" \
|
||||
RELEASE_REPOSITORY="${GITHUB_REPOSITORY}" \
|
||||
RELEASE_TAG="${RELEASE_TAG}" \
|
||||
RELEASE_TITLE="${release_title}" \
|
||||
RELEASE_RESOURCE_FILE="${resource_file}" \
|
||||
EXPECTED_DRAFT="${expected_draft}" \
|
||||
node --input-type=module <<'NODE'
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
releaseNotesVersionForTag,
|
||||
verifyGithubReleaseNotes,
|
||||
} from "./.release-harness/scripts/render-github-release-notes.mjs";
|
||||
|
||||
const release = JSON.parse(readFileSync(process.env.RELEASE_RESOURCE_FILE, "utf8"));
|
||||
const body = readFileSync(process.env.RELEASE_BODY_FILE, "utf8");
|
||||
const changelog = readFileSync(process.env.RELEASE_CHANGELOG_FILE, "utf8");
|
||||
const expectedDraft = process.env.EXPECTED_DRAFT;
|
||||
const notes = verifyGithubReleaseNotes({
|
||||
body,
|
||||
changelog,
|
||||
version: releaseNotesVersionForTag(process.env.RELEASE_TAG),
|
||||
tag: process.env.RELEASE_TAG,
|
||||
repository: process.env.RELEASE_REPOSITORY,
|
||||
});
|
||||
if (
|
||||
release.tag_name !== process.env.RELEASE_TAG ||
|
||||
release.name !== process.env.RELEASE_TITLE ||
|
||||
release.prerelease !== false ||
|
||||
(expectedDraft !== "" && release.draft !== (expectedDraft === "true")) ||
|
||||
!notes.matches
|
||||
) {
|
||||
console.error("Existing GitHub release does not match the canonical extended-stable contract.");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
verify_release_tag_target
|
||||
cat > "${verification_file}" <<EOF
|
||||
### Release verification
|
||||
|
||||
- Scope: \`openclaw@${release_version}\`, official npm plugins, and Gateway container images only.
|
||||
- Channel: npm and container \`extended-stable\`; regular \`latest\` is unchanged.
|
||||
- Container publication: this draft is made public only after Docker publication succeeds.
|
||||
- Excluded: ClawHub, macOS, Windows, mobile, website, and private dist-tags.
|
||||
EOF
|
||||
node .release-harness/scripts/render-github-release-notes.mjs \
|
||||
--changelog CHANGELOG.md \
|
||||
--tag "${RELEASE_TAG}" \
|
||||
--repository "${GITHUB_REPOSITORY}" \
|
||||
--verification-file "${verification_file}" \
|
||||
--output "${notes_file}"
|
||||
|
||||
release_id="$(resolve_release_id)"
|
||||
if [[ -n "${release_id}" ]]; then
|
||||
verify_release_resource "${release_id}"
|
||||
release_draft="$(gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" --jq '.draft')"
|
||||
if [[ "${release_draft}" == "true" ]]; then
|
||||
gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
|
||||
--title "${release_title}" \
|
||||
--notes-file "${notes_file}" \
|
||||
--prerelease=false \
|
||||
--latest=false
|
||||
verify_release_resource "${release_id}" true
|
||||
else
|
||||
echo "Existing canonical public release will be revalidated after Docker publication."
|
||||
fi
|
||||
else
|
||||
if ! gh release create "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
|
||||
--verify-tag \
|
||||
--draft \
|
||||
--title "${release_title}" \
|
||||
--notes-file "${notes_file}" \
|
||||
--prerelease=false \
|
||||
--latest=false; then
|
||||
echo "Draft creation raced with another writer; resolving the existing release."
|
||||
fi
|
||||
release_id="$(wait_for_release_id)" || {
|
||||
echo "GitHub did not expose a numeric release ID after draft creation." >&2
|
||||
exit 1
|
||||
}
|
||||
verify_release_resource "${release_id}"
|
||||
fi
|
||||
release_body_sha256="$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" |
|
||||
jq -j '.body // ""' |
|
||||
sha256sum |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
echo "release_id=${release_id}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_body_sha256=${release_body_sha256}" >> "$GITHUB_OUTPUT"
|
||||
echo "- Extended-stable GitHub release draft: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
publish_docker:
|
||||
name: Publish Docker images
|
||||
needs: [resolve_release_target, publish, verify_core_npm_registry]
|
||||
if: ${{ always() && ((inputs.publish_openclaw_npm && needs.publish.result == 'success') || (inputs.publish_docker_only && needs.verify_core_npm_registry.result == 'success')) }}
|
||||
needs: [resolve_release_target, publish, prepare_extended_stable_release]
|
||||
if: ${{ always() && ((inputs.publish_openclaw_npm && needs.publish.result == 'success') || (inputs.publish_docker_only && needs.prepare_extended_stable_release.result == 'success')) }}
|
||||
uses: ./.github/workflows/docker-release.yml
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -2556,3 +2725,89 @@ jobs:
|
||||
echo "Published GitHub release state does not match the requested draft/prerelease classification." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
finalize_extended_stable_github_release:
|
||||
name: Finalize extended-stable GitHub release
|
||||
needs: [resolve_release_target, prepare_extended_stable_release, publish_docker]
|
||||
if: ${{ always() && inputs.publish_docker_only && needs.prepare_extended_stable_release.result == 'success' && needs.publish_docker.result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm-release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Publish the verified extended-stable draft
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EXPECTED_BODY_SHA256: ${{ needs.prepare_extended_stable_release.outputs.release_body_sha256 }}
|
||||
RELEASE_ID: ${{ needs.prepare_extended_stable_release.outputs.release_id }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! "${RELEASE_ID}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Extended-stable finalization requires a numeric GitHub release ID." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "${EXPECTED_BODY_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
|
||||
echo "Extended-stable finalization requires a canonical release body digest." >&2
|
||||
exit 1
|
||||
fi
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
release_title="openclaw ${release_version}"
|
||||
resource_file="${RUNNER_TEMP}/extended-stable-final-release.json"
|
||||
|
||||
verify_release_state() {
|
||||
local expected_draft="$1"
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" > "${resource_file}"
|
||||
RELEASE_RESOURCE_FILE="${resource_file}" \
|
||||
RELEASE_TAG="${RELEASE_TAG}" \
|
||||
RELEASE_TITLE="${release_title}" \
|
||||
EXPECTED_BODY_SHA256="${EXPECTED_BODY_SHA256}" \
|
||||
EXPECTED_DRAFT="${expected_draft}" \
|
||||
node --input-type=module <<'NODE'
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const release = JSON.parse(readFileSync(process.env.RELEASE_RESOURCE_FILE, "utf8"));
|
||||
const bodySha256 = createHash("sha256").update(release.body ?? "").digest("hex");
|
||||
if (
|
||||
release.tag_name !== process.env.RELEASE_TAG ||
|
||||
release.name !== process.env.RELEASE_TITLE ||
|
||||
release.prerelease !== false ||
|
||||
release.draft !== (process.env.EXPECTED_DRAFT === "true") ||
|
||||
bodySha256 !== process.env.EXPECTED_BODY_SHA256
|
||||
) {
|
||||
console.error("GitHub release state does not match the extended-stable contract.");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
current_draft="$(gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --jq '.draft')"
|
||||
if [[ "${current_draft}" == "true" ]]; then
|
||||
verify_release_state true
|
||||
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
|
||||
-F draft=false \
|
||||
-F prerelease=false \
|
||||
-f make_latest=false >/dev/null
|
||||
elif [[ "${current_draft}" != "false" ]]; then
|
||||
echo "GitHub release ${RELEASE_ID} has an invalid draft state." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for attempt in $(seq 1 12); do
|
||||
if verify_release_state false; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" == "12" ]]; then
|
||||
echo "GitHub release ${RELEASE_ID} did not reach its public state." >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
latest_tag="$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name')"
|
||||
if [[ "${latest_tag}" == "${RELEASE_TAG}" ]]; then
|
||||
echo "Extended-stable release ${RELEASE_TAG} must not become GitHub Latest." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "- Extended-stable GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
+13
-5
@@ -186,11 +186,13 @@ gh workflow run full-release-validation.yml --ref main \
|
||||
|
||||
Gateway extended-stable runs npm preflight, Full Release Validation, and plugin
|
||||
npm release from `extended-stable/YYYY.M.33`; core publish consumes those three
|
||||
run IDs plus the validation attempt. `release-ci/*` evidence is invalid because
|
||||
publish binds every run to the canonical branch and release SHA. The tag
|
||||
publishes Gateway images and only the `extended-stable*` aliases; the path skips
|
||||
the regular orchestrator and its ClawHub, native-app, GitHub Release, website,
|
||||
and private dist-tag surfaces. See [Monthly Gateway extended-stable
|
||||
run IDs plus the validation attempt. Complete evidence may come from the
|
||||
canonical branch, reachable current-main tooling, or the trusted main-pinned
|
||||
`release-ci/*` harness when its v3 manifest binds the exact target and attempt.
|
||||
The post-npm `OpenClaw Release Publish` closeout publishes Gateway images and
|
||||
only the `extended-stable*` aliases, then publishes the notes-only GitHub
|
||||
Release with `latest=false`; it skips ClawHub, native-app, website, and private
|
||||
dist-tag surfaces. See [Monthly Gateway extended-stable
|
||||
publication](/reference/RELEASING#monthly-gateway-extended-stable-publication)
|
||||
for commands and recovery.
|
||||
|
||||
@@ -378,6 +380,12 @@ Focused plugin-only repairs use `plugin_publish_scope=selected` with a nonempty
|
||||
package list. Plugin-only `all-publishable` runs require the same immutable npm
|
||||
preflight and Full Release Validation evidence as a core publish.
|
||||
|
||||
Extended-stable uses this workflow only after npm publication, with
|
||||
`publish_openclaw_npm=false` and `publish_docker_only=true`. That closeout
|
||||
rechecks immutable npm evidence, publishes Docker, and finalizes the notes-only
|
||||
non-Latest GitHub Release without entering plugin, ClawHub, or native-app
|
||||
publication.
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref main \
|
||||
|
||||
+17
-11
@@ -17,7 +17,8 @@ OpenClaw exposes four user-facing update channels:
|
||||
- dev: the moving head of `main`
|
||||
|
||||
Extended-stable ships the trailing month's Gateway, official npm plugins, and
|
||||
Docker images without moving regular `latest` or `main` selectors.
|
||||
Docker images without moving regular `latest` or `main` selectors. Each release
|
||||
also has a notes-only GitHub Release page that is never marked Latest.
|
||||
|
||||
Tideclaw alpha builds are a separate internal prerelease track (npm dist-tag `alpha`), covered under [NPM workflow inputs](#npm-workflow-inputs) and [Release test boxes](#release-test-boxes).
|
||||
|
||||
@@ -92,7 +93,9 @@ gh workflow run full-release-validation.yml \
|
||||
|
||||
The SHA form is preflight-only. Run validation on the canonical branch; publish
|
||||
binds its workflow ref, head/target SHA, run ID, and attempt. Save both IDs and
|
||||
the successful `run_attempt`; reject `release-ci/*` evidence.
|
||||
the successful `run_attempt`. Accept a direct canonical-branch run, a direct
|
||||
current-`main` run whose workflow SHA remains reachable from `main`, or the
|
||||
trusted main-pinned `release-ci/*` harness; reject narrow reruns.
|
||||
|
||||
Classify failures before editing:
|
||||
|
||||
@@ -199,9 +202,11 @@ digest; regular aliases remain unchanged and automatic rollback is rejected.
|
||||
After that core registry readback succeeds, start Docker publication only through
|
||||
`OpenClaw Release Publish`. Its Docker-only extended-stable path rechecks the
|
||||
saved npm preflight artifact, exact `Full Release Validation` evidence, exact npm
|
||||
version and `extended-stable` selector, and published tarball digest before it
|
||||
calls the reusable `Docker Release` workflow. A tag push never publishes Docker
|
||||
images by itself:
|
||||
version and `extended-stable` selector, and published tarball digest. It then
|
||||
creates or resumes a canonical non-prerelease GitHub Release draft with
|
||||
`latest=false`, calls the reusable `Docker Release` workflow, and makes the
|
||||
draft public only after Docker succeeds. A tag push never publishes Docker
|
||||
images or a release page by itself:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
@@ -221,8 +226,9 @@ an explicit rollback, and never rebuilds images.
|
||||
|
||||
Slack, Discord, and Codex are the initial documented support surfaces, not a
|
||||
release allowlist: every npm-publishable official plugin ships. The regular
|
||||
checklist alone owns beta/`latest`, GitHub Releases, ClawHub, native apps, mobile,
|
||||
website, and private dist-tags; do not run those steps for this Gateway path.
|
||||
checklist alone owns beta/`latest`, GitHub Release assets, ClawHub, native apps,
|
||||
mobile, website, and private dist-tags; do not run those steps for this Gateway
|
||||
path. Extended-stable owns only its notes-only, non-Latest GitHub Release page.
|
||||
|
||||
## Regular release operator checklist
|
||||
|
||||
@@ -562,9 +568,9 @@ For package-candidate Telegram proof, enable `telegram_mode=mock-openai` or `tel
|
||||
|
||||
For beta, `latest`, plugin, GitHub Release, and platform publication,
|
||||
`OpenClaw Release Publish` is the normal mutating entrypoint. The monthly
|
||||
`.33+` Gateway extended-stable path does not use this orchestrator. The
|
||||
regular workflow orchestrates the trusted-publisher workflows in the order the
|
||||
release needs:
|
||||
`.33+` Gateway extended-stable path also uses its Docker-only closeout after npm
|
||||
publication. The regular path orchestrates the trusted-publisher workflows in
|
||||
the order the release needs:
|
||||
|
||||
1. Check out the release tag and resolve its commit SHA.
|
||||
2. Verify the tag is reachable from `main` or `release/*` (or a Tideclaw alpha branch for alpha prereleases).
|
||||
@@ -723,7 +729,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 their pinned `sha256:` digests; required for stable OpenClaw publish
|
||||
- `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`
|
||||
- `publish_docker_only`: beta 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.
|
||||
- `publish_docker_only`: extended-stable-only recovery/closeout path. It requires `publish_openclaw_npm=false`, complete preflight and Full Release Validation evidence, then verifies the exact npm package, selector, and tarball digest, prepares the notes-only GitHub Release draft, publishes Docker, and makes the release public with `latest=false`.
|
||||
- `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`
|
||||
- `publish_openclaw_npm`: defaults to `true`; set `false` only when using the workflow as a plugin-only repair orchestrator
|
||||
|
||||
@@ -53,10 +53,11 @@ not declare the current release-isolation contract or the `expected_sha`
|
||||
dispatch input; it never silently substitutes newer tooling. The workflow never
|
||||
creates or updates repository refs itself.
|
||||
|
||||
## Extended-stable exception
|
||||
## Extended-stable validation
|
||||
|
||||
Extended-stable publish requires a run whose workflow and target are both the
|
||||
canonical branch:
|
||||
Extended-stable publish accepts a complete exact-target run from the canonical
|
||||
branch, a direct current-main run whose workflow SHA remains reachable from
|
||||
current `main`, or the trusted main-pinned helper:
|
||||
|
||||
```bash
|
||||
RELEASE_SHA="$(git rev-parse HEAD)"
|
||||
@@ -67,9 +68,10 @@ gh workflow run full-release-validation.yml \
|
||||
-f release_profile=stable
|
||||
```
|
||||
|
||||
Do not use `pnpm ci:full-release` or `release-ci/*`. Publish binds the run's
|
||||
branch, head/target SHA, manifest `workflowRef`, ID, and attempt to the canonical
|
||||
branch and release commit.
|
||||
The helper's temporary `release-ci/*` ref is valid only when every child uses
|
||||
the pinned trusted workflow SHA and the v3 manifest binds the canonical branch,
|
||||
exact release commit, run ID, and attempt. Reject narrow runs, stale attempts,
|
||||
untrusted workflow SHAs, and mismatched targets.
|
||||
|
||||
Backport product failures; make the smallest behavior-preserving repair for
|
||||
frozen-target tooling; retry provider, approval, or runner failures without a
|
||||
@@ -331,8 +333,8 @@ Narrow evidence is not publish authorization by itself.
|
||||
For a regular release, record both Code SHA and Release SHA, the reuse policy
|
||||
and changed-path set, the green Code SHA parent run, and the lightweight Release
|
||||
SHA parent run. For extended-stable, record the canonical branch, exact release
|
||||
SHA, fresh parent run id and attempt, workflow ref, every child run, and any
|
||||
frozen-target compatibility repair or intentional omission.
|
||||
SHA, accepted producer identity, parent run id and attempt, workflow ref, every
|
||||
child run, and any frozen-target compatibility repair or intentional omission.
|
||||
|
||||
Useful artifacts:
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ const MANTIS_GITHUB_APP_CLIENT_ID = "Iv23liPJCozR0uHm6P7G";
|
||||
const OPENGREP_PR_DIFF_WORKFLOW = ".github/workflows/opengrep-precise.yml";
|
||||
const OPENGREP_FULL_WORKFLOW = ".github/workflows/opengrep-precise-full.yml";
|
||||
const CONTROL_UI_LOCALE_REFRESH_WORKFLOW = ".github/workflows/control-ui-locale-refresh.yml";
|
||||
const DOCS_TRANSLATE_RELEASE_WORKFLOW = ".github/workflows/docs-translate-trigger-release.yml";
|
||||
const NATIVE_APP_LOCALE_REFRESH_WORKFLOW = ".github/workflows/native-app-locale-refresh.yml";
|
||||
const CREATE_GENERATED_PR_TOKENS_ACTION = ".github/actions/create-generated-pr-tokens/action.yml";
|
||||
const PUBLISH_GENERATED_PR_ACTION = ".github/actions/publish-generated-pr/action.yml";
|
||||
@@ -1861,7 +1862,9 @@ NODE
|
||||
|
||||
it("keeps locale refresh matrices alive and publishes each aggregate through a PR", () => {
|
||||
const controlUiWorkflow = parse(readFileSync(CONTROL_UI_LOCALE_REFRESH_WORKFLOW, "utf8"));
|
||||
const docsTranslateWorkflow = parse(readFileSync(DOCS_TRANSLATE_RELEASE_WORKFLOW, "utf8"));
|
||||
const workflow = parse(readFileSync(NATIVE_APP_LOCALE_REFRESH_WORKFLOW, "utf8"));
|
||||
const controlUiReleasePolicy = controlUiWorkflow.jobs.release_policy;
|
||||
const controlUiResolveBase = controlUiWorkflow.jobs["resolve-base"];
|
||||
const nativeResolveBase = workflow.jobs["resolve-base"];
|
||||
const controlUiPreflight = controlUiWorkflow.jobs["publisher-preflight"];
|
||||
@@ -1914,8 +1917,23 @@ NODE
|
||||
expect(controlUiResolveBase.if).not.toContain("chore(ui): refresh control ui locales");
|
||||
const controlResolveCondition = controlUiResolveBase.if.replace(/\s+/gu, " ");
|
||||
expect(controlResolveCondition).toBe(
|
||||
"github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main')",
|
||||
"needs.release_policy.outputs.should_run == 'true' && github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main')",
|
||||
);
|
||||
expect(controlUiResolveBase.needs).toBe("release_policy");
|
||||
expect(controlUiReleasePolicy.outputs.should_run).toBe(
|
||||
"${{ steps.release.outputs.should_run }}",
|
||||
);
|
||||
expect(
|
||||
controlUiReleasePolicy.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Classify release track",
|
||||
).run,
|
||||
).toContain('track !== "extended-stable"');
|
||||
const docsDispatch = docsTranslateWorkflow.jobs["dispatch-translate"];
|
||||
expect(
|
||||
docsDispatch.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Trigger translation coordinator in publish repo",
|
||||
).if,
|
||||
).toBe("${{ steps.release.outputs.track != 'extended-stable' }}");
|
||||
expect(controlResolveCondition).not.toContain("inputs.token_preflight_only");
|
||||
expect(controlResolveCondition).not.toContain("github.ref_type");
|
||||
expect(nativeResolveBase.if).toBe(
|
||||
|
||||
@@ -6246,6 +6246,36 @@ describe("package artifact reuse", () => {
|
||||
expect(finalizeJob.needs).toEqual(["publish", "publish_docker"]);
|
||||
expect(finalizeJob.if).toContain("needs.publish_docker.result == 'success'");
|
||||
expect(finalizeRelease.run).toContain('gh release edit "${RELEASE_TAG}"');
|
||||
|
||||
const extendedPrepareJob = workflowJob(
|
||||
RELEASE_PUBLISH_WORKFLOW,
|
||||
"prepare_extended_stable_release",
|
||||
);
|
||||
const extendedPrepare = workflowStep(
|
||||
extendedPrepareJob,
|
||||
"Create or resume the canonical draft release",
|
||||
);
|
||||
const extendedFinalizeJob = workflowJob(
|
||||
RELEASE_PUBLISH_WORKFLOW,
|
||||
"finalize_extended_stable_github_release",
|
||||
);
|
||||
const extendedFinalize = workflowStep(
|
||||
extendedFinalizeJob,
|
||||
"Publish the verified extended-stable draft",
|
||||
);
|
||||
expect(extendedPrepareJob.needs).toEqual(["resolve_release_target"]);
|
||||
expect(extendedPrepare.run).toContain("verify_release_tag_target");
|
||||
expect(extendedPrepare.run).toContain("verifyGithubReleaseNotes");
|
||||
expect(extendedPrepare.run).toContain("--draft");
|
||||
expect(extendedPrepare.run).toContain("--latest=false");
|
||||
expect(extendedFinalizeJob.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"prepare_extended_stable_release",
|
||||
"publish_docker",
|
||||
]);
|
||||
expect(extendedFinalizeJob.if).toContain("needs.publish_docker.result == 'success'");
|
||||
expect(extendedFinalize.run).toContain("-f make_latest=false");
|
||||
expect(extendedFinalize.run).toContain("EXPECTED_BODY_SHA256");
|
||||
});
|
||||
|
||||
it("accepts tag-matched frozen release branches in OpenClaw npm preflight", () => {
|
||||
|
||||
@@ -1036,10 +1036,10 @@ describe("release validation no-push transport", () => {
|
||||
expect(dockerCall.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"publish",
|
||||
"verify_core_npm_registry",
|
||||
"prepare_extended_stable_release",
|
||||
]);
|
||||
expect(dockerCall.if).toContain("needs.publish.result == 'success'");
|
||||
expect(dockerCall.if).toContain("needs.verify_core_npm_registry.result == 'success'");
|
||||
expect(dockerCall.if).toContain("needs.prepare_extended_stable_release.result == 'success'");
|
||||
expect(dockerCall.with).toEqual({
|
||||
tag: "${{ inputs.tag }}",
|
||||
release_sha: "${{ needs.resolve_release_target.outputs.sha }}",
|
||||
@@ -1068,6 +1068,61 @@ describe("release validation no-push transport", () => {
|
||||
"publish_docker",
|
||||
]);
|
||||
|
||||
const prepareRelease = job(releasePublish, "prepare_extended_stable_release");
|
||||
const verifyNpm = step(
|
||||
prepareRelease,
|
||||
"Verify exact npm and selector readback matches preflight bytes",
|
||||
);
|
||||
const createDraft = step(prepareRelease, "Create or resume the canonical draft release");
|
||||
const finalizeRelease = job(releasePublish, "finalize_extended_stable_github_release");
|
||||
const publishDraft = step(finalizeRelease, "Publish the verified extended-stable draft");
|
||||
|
||||
expect(prepareRelease.needs).toEqual(["resolve_release_target"]);
|
||||
expect(prepareRelease.if).toBe("${{ inputs.publish_docker_only }}");
|
||||
expect(prepareRelease.environment).toBe("npm-release");
|
||||
expect(prepareRelease.permissions).toEqual({ contents: "write" });
|
||||
expect(prepareRelease.outputs).toEqual({
|
||||
release_id: "${{ steps.release.outputs.release_id }}",
|
||||
release_body_sha256: "${{ steps.release.outputs.release_body_sha256 }}",
|
||||
});
|
||||
expect(verifyNpm.run).toContain('npm view "openclaw@${version}" version');
|
||||
expect(verifyNpm.run).toContain("Published npm tarball does not match");
|
||||
expect(createDraft.run).toContain("verify_release_tag_target");
|
||||
expect(createDraft.run).toContain(".release-harness/scripts/render-github-release-notes.mjs");
|
||||
expect(createDraft.run).toContain('gh release create "${RELEASE_TAG}"');
|
||||
expect(createDraft.run).toContain("--verify-tag");
|
||||
expect(createDraft.run).toContain("--draft");
|
||||
expect(createDraft.run).toContain("--prerelease=false");
|
||||
expect(createDraft.run).toContain("--latest=false");
|
||||
expect(createDraft.run).toContain("Existing canonical public release will be revalidated");
|
||||
expect(createDraft.run).toContain("wait_for_release_id");
|
||||
expect(createDraft.run).toContain("release_body_sha256=");
|
||||
|
||||
expect(finalizeRelease.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"prepare_extended_stable_release",
|
||||
"publish_docker",
|
||||
]);
|
||||
expect(finalizeRelease.if).toContain("inputs.publish_docker_only");
|
||||
expect(finalizeRelease.if).toContain(
|
||||
"needs.prepare_extended_stable_release.result == 'success'",
|
||||
);
|
||||
expect(finalizeRelease.if).toContain("needs.publish_docker.result == 'success'");
|
||||
expect(finalizeRelease.environment).toBe("npm-release");
|
||||
expect(finalizeRelease.permissions).toEqual({ contents: "write" });
|
||||
expect(publishDraft.env).toMatchObject({
|
||||
EXPECTED_BODY_SHA256:
|
||||
"${{ needs.prepare_extended_stable_release.outputs.release_body_sha256 }}",
|
||||
RELEASE_ID: "${{ needs.prepare_extended_stable_release.outputs.release_id }}",
|
||||
});
|
||||
expect(publishDraft.run).toContain("repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}");
|
||||
expect(publishDraft.run).toContain("-F draft=false");
|
||||
expect(publishDraft.run).toContain("-F prerelease=false");
|
||||
expect(publishDraft.run).toContain("-f make_latest=false");
|
||||
expect(publishDraft.run).toContain("EXPECTED_BODY_SHA256");
|
||||
expect(publishDraft.run).toContain("releases/latest");
|
||||
expect(publishDraft.run).toContain("must not become GitHub Latest");
|
||||
|
||||
const identity = step(
|
||||
job(dockerRelease, "validate_release_identity"),
|
||||
"Verify tag, SHA, and package identity agree",
|
||||
|
||||
Reference in New Issue
Block a user