mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
ci: make extended-stable closeout idempotent
This commit is contained in:
@@ -46,50 +46,8 @@ 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,36 +12,7 @@ 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 }}
|
||||
|
||||
@@ -2389,6 +2389,7 @@ jobs:
|
||||
outputs:
|
||||
release_id: ${{ steps.release.outputs.release_id }}
|
||||
release_body_sha256: ${{ steps.release.outputs.release_body_sha256 }}
|
||||
release_already_public: ${{ steps.release.outputs.release_already_public }}
|
||||
steps:
|
||||
- name: Verify exact npm and selector readback matches preflight bytes
|
||||
env:
|
||||
@@ -2480,17 +2481,17 @@ jobs:
|
||||
verify_release_resource() {
|
||||
local release_id="$1"
|
||||
local expected_draft="${2:-}"
|
||||
local body_file="${RUNNER_TEMP}/extended-stable-existing-release.md"
|
||||
local require_exact_body="${3:-false}"
|
||||
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_BODY_FILE="${notes_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}" \
|
||||
REQUIRE_EXACT_BODY="${require_exact_body}" \
|
||||
node --input-type=module <<'NODE'
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
@@ -2499,9 +2500,10 @@ jobs:
|
||||
} 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 body = release.body ?? "";
|
||||
const changelog = readFileSync(process.env.RELEASE_CHANGELOG_FILE, "utf8");
|
||||
const expectedDraft = process.env.EXPECTED_DRAFT;
|
||||
const expectedBody = readFileSync(process.env.RELEASE_BODY_FILE, "utf8");
|
||||
const notes = verifyGithubReleaseNotes({
|
||||
body,
|
||||
changelog,
|
||||
@@ -2514,6 +2516,9 @@ jobs:
|
||||
release.name !== process.env.RELEASE_TITLE ||
|
||||
release.prerelease !== false ||
|
||||
(expectedDraft !== "" && release.draft !== (expectedDraft === "true")) ||
|
||||
!Array.isArray(release.assets) ||
|
||||
release.assets.length !== 0 ||
|
||||
(process.env.REQUIRE_EXACT_BODY === "true" && body !== expectedBody) ||
|
||||
!notes.matches
|
||||
) {
|
||||
console.error("Existing GitHub release does not match the canonical extended-stable contract.");
|
||||
@@ -2522,6 +2527,23 @@ jobs:
|
||||
NODE
|
||||
}
|
||||
|
||||
wait_until_not_latest() {
|
||||
local attempt latest_tag
|
||||
for attempt in $(seq 1 12); do
|
||||
latest_tag="$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>/dev/null || true
|
||||
)"
|
||||
if [[ -n "${latest_tag}" && "${latest_tag}" != "${RELEASE_TAG}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "${attempt}" != "12" ]]; then
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
echo "Extended-stable release ${RELEASE_TAG} must not be GitHub Latest." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_release_tag_target
|
||||
cat > "${verification_file}" <<EOF
|
||||
### Release verification
|
||||
@@ -2538,20 +2560,10 @@ jobs:
|
||||
--verification-file "${verification_file}" \
|
||||
--output "${notes_file}"
|
||||
|
||||
release_already_public=false
|
||||
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 \
|
||||
@@ -2568,20 +2580,39 @@ jobs:
|
||||
}
|
||||
verify_release_resource "${release_id}"
|
||||
fi
|
||||
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 true
|
||||
elif [[ "${release_draft}" == "false" ]]; then
|
||||
verify_release_resource "${release_id}" false true
|
||||
wait_until_not_latest
|
||||
release_already_public=true
|
||||
echo "Existing canonical public release is already complete; Docker will not be rebuilt."
|
||||
else
|
||||
echo "GitHub release ${release_id} has an invalid draft state." >&2
|
||||
exit 1
|
||||
fi
|
||||
release_body_sha256="$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" |
|
||||
jq -j '.body // ""' |
|
||||
sha256sum |
|
||||
awk '{print $1}'
|
||||
sha256sum "${notes_file}" | 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"
|
||||
echo "release_already_public=${release_already_public}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${release_already_public}" == "true" ]]; then
|
||||
echo "- Extended-stable GitHub release: already public; Docker rebuild skipped" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "- Extended-stable GitHub release draft: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
publish_docker:
|
||||
name: Publish Docker images
|
||||
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')) }}
|
||||
if: ${{ always() && ((inputs.publish_openclaw_npm && needs.publish.result == 'success') || (inputs.publish_docker_only && needs.prepare_extended_stable_release.result == 'success' && needs.prepare_extended_stable_release.outputs.release_already_public != 'true')) }}
|
||||
uses: ./.github/workflows/docker-release.yml
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -2669,6 +2700,8 @@ jobs:
|
||||
release.name !== process.env.RELEASE_TITLE ||
|
||||
release.prerelease !== false ||
|
||||
release.draft !== (process.env.EXPECTED_DRAFT === "true") ||
|
||||
!Array.isArray(release.assets) ||
|
||||
release.assets.length !== 0 ||
|
||||
bodySha256 !== process.env.EXPECTED_BODY_SHA256
|
||||
) {
|
||||
console.error("GitHub release state does not match the extended-stable contract.");
|
||||
@@ -2677,9 +2710,30 @@ jobs:
|
||||
NODE
|
||||
}
|
||||
|
||||
verify_release_tag_target() {
|
||||
local direct_sha peeled_sha remote_refs remote_sha
|
||||
remote_refs="$(git ls-remote --tags "https://github.com/${GITHUB_REPOSITORY}.git" \
|
||||
"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
|
||||
}
|
||||
|
||||
# Docker publication can take long enough for the tag to change after
|
||||
# preparation. Recheck immediately before accepting or publishing the page.
|
||||
verify_release_tag_target
|
||||
current_draft="$(gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --jq '.draft')"
|
||||
if [[ "${current_draft}" == "true" ]]; then
|
||||
verify_release_state true
|
||||
# GITHUB_TOKEN publication intentionally does not fan out release.published
|
||||
# workflows; extended-stable scope stays owned by this closeout.
|
||||
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
|
||||
-F draft=false \
|
||||
-F prerelease=false \
|
||||
@@ -2699,9 +2753,17 @@ jobs:
|
||||
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
|
||||
for attempt in $(seq 1 12); do
|
||||
latest_tag="$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>/dev/null || true
|
||||
)"
|
||||
if [[ -n "${latest_tag}" && "${latest_tag}" != "${RELEASE_TAG}" ]]; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" == "12" ]]; then
|
||||
echo "Extended-stable release ${RELEASE_TAG} must not become GitHub Latest." >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
echo "- Extended-stable GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -41,7 +41,6 @@ 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";
|
||||
@@ -1635,9 +1634,7 @@ 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"];
|
||||
@@ -1691,23 +1688,8 @@ NODE
|
||||
expect(nativeResolveBase.if).not.toContain("chore(i18n): refresh native locales");
|
||||
const controlResolveCondition = controlUiResolveBase.if.replace(/\s+/gu, " ");
|
||||
expect(controlResolveCondition).toBe(
|
||||
"needs.release_policy.outputs.should_run == 'true' && github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main')",
|
||||
"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(
|
||||
|
||||
@@ -5363,6 +5363,9 @@ describe("package artifact reuse", () => {
|
||||
expect(extendedPrepareJob.needs).toEqual(["resolve_release_target"]);
|
||||
expect(extendedPrepare.run).toContain("verify_release_tag_target");
|
||||
expect(extendedPrepare.run).toContain("verifyGithubReleaseNotes");
|
||||
expect(extendedPrepare.run).toContain("body !== expectedBody");
|
||||
expect(extendedPrepare.run).toContain("release.assets.length !== 0");
|
||||
expect(extendedPrepare.run).toContain("release_already_public=true");
|
||||
expect(extendedPrepare.run).toContain("--draft");
|
||||
expect(extendedPrepare.run).toContain("--latest=false");
|
||||
expect(extendedFinalizeJob.needs).toEqual([
|
||||
@@ -5373,6 +5376,14 @@ describe("package artifact reuse", () => {
|
||||
expect(extendedFinalizeJob.if).toContain("needs.publish_docker.result == 'success'");
|
||||
expect(extendedFinalize.run).toContain("-f make_latest=false");
|
||||
expect(extendedFinalize.run).toContain("EXPECTED_BODY_SHA256");
|
||||
expect(extendedFinalize.run).toContain("release.assets.length !== 0");
|
||||
expect(extendedFinalize.run).toContain("for attempt in $(seq 1 12)");
|
||||
expect(extendedFinalize.run).toContain(
|
||||
'git ls-remote --tags "https://github.com/${GITHUB_REPOSITORY}.git"',
|
||||
);
|
||||
expect(extendedFinalize.run.indexOf("verify_release_tag_target\n")).toBeLessThan(
|
||||
extendedFinalize.run.indexOf('current_draft="$(gh api'),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts tag-matched frozen release branches in OpenClaw npm preflight", () => {
|
||||
|
||||
@@ -187,6 +187,20 @@ function step(workflowJob: WorkflowJob, name: string): WorkflowStep {
|
||||
return value;
|
||||
}
|
||||
|
||||
function shellFunctionSource(source: string, functionName: string): string {
|
||||
const startMarker = `${functionName}() {`;
|
||||
const endMarker = "\n}";
|
||||
const start = source.indexOf(startMarker);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing shell function ${functionName}`);
|
||||
}
|
||||
const end = source.indexOf(endMarker, start);
|
||||
if (end < 0) {
|
||||
throw new Error(`missing shell function terminator ${functionName}`);
|
||||
}
|
||||
return source.slice(start, end + endMarker.length);
|
||||
}
|
||||
|
||||
function expectReadOnlyPackagePermission(workflowJob: WorkflowJob): void {
|
||||
expect(permissionAt(workflowJob.permissions, "packages", "none")).toBe("read");
|
||||
}
|
||||
@@ -1028,6 +1042,9 @@ describe("release validation no-push transport", () => {
|
||||
]);
|
||||
expect(dockerCall.if).toContain("needs.publish.result == 'success'");
|
||||
expect(dockerCall.if).toContain("needs.prepare_extended_stable_release.result == 'success'");
|
||||
expect(dockerCall.if).toContain(
|
||||
"needs.prepare_extended_stable_release.outputs.release_already_public != 'true'",
|
||||
);
|
||||
expect(dockerCall.with).toEqual({
|
||||
tag: "${{ inputs.tag }}",
|
||||
release_sha: "${{ needs.resolve_release_target.outputs.sha }}",
|
||||
@@ -1072,6 +1089,7 @@ describe("release validation no-push transport", () => {
|
||||
expect(prepareRelease.outputs).toEqual({
|
||||
release_id: "${{ steps.release.outputs.release_id }}",
|
||||
release_body_sha256: "${{ steps.release.outputs.release_body_sha256 }}",
|
||||
release_already_public: "${{ steps.release.outputs.release_already_public }}",
|
||||
});
|
||||
expect(verifyNpm.run).toContain('npm view "openclaw@${version}" version');
|
||||
expect(verifyNpm.run).toContain("Published npm tarball does not match");
|
||||
@@ -1082,9 +1100,14 @@ describe("release validation no-push transport", () => {
|
||||
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("body !== expectedBody");
|
||||
expect(createDraft.run).toContain("release.assets.length !== 0");
|
||||
expect(createDraft.run).toContain('verify_release_resource "${release_id}" false true');
|
||||
expect(createDraft.run).toContain("wait_until_not_latest");
|
||||
expect(createDraft.run).toContain("release_already_public=true");
|
||||
expect(createDraft.run).toContain("Docker will not be rebuilt");
|
||||
expect(createDraft.run).toContain("wait_for_release_id");
|
||||
expect(createDraft.run).toContain("release_body_sha256=");
|
||||
expect(createDraft.run).toContain('sha256sum "${notes_file}"');
|
||||
|
||||
expect(finalizeRelease.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
@@ -1108,8 +1131,19 @@ describe("release validation no-push transport", () => {
|
||||
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("release.assets.length !== 0");
|
||||
expect(publishDraft.run).toContain("releases/latest");
|
||||
expect(publishDraft.run).toContain("must not become GitHub Latest");
|
||||
expect(publishDraft.run).toContain("for attempt in $(seq 1 12)");
|
||||
expect(publishDraft.run).toContain(
|
||||
"GITHUB_TOKEN publication intentionally does not fan out release.published",
|
||||
);
|
||||
expect(publishDraft.run).toContain(
|
||||
'git ls-remote --tags "https://github.com/${GITHUB_REPOSITORY}.git"',
|
||||
);
|
||||
expect(publishDraft.run.indexOf("verify_release_tag_target\n")).toBeLessThan(
|
||||
publishDraft.run.indexOf('current_draft="$(gh api'),
|
||||
);
|
||||
|
||||
const identity = step(
|
||||
job(dockerRelease, "validate_release_identity"),
|
||||
@@ -1121,6 +1155,56 @@ describe("release validation no-push transport", () => {
|
||||
expect(identity.run).toContain("^v${package_version}-[1-9][0-9]*$");
|
||||
});
|
||||
|
||||
it("rechecks lightweight and annotated extended-stable tags before finalization", () => {
|
||||
const releasePublish = readWorkflow(".github/workflows/openclaw-release-publish.yml");
|
||||
const publishDraft = step(
|
||||
job(releasePublish, "finalize_extended_stable_github_release"),
|
||||
"Publish the verified extended-stable draft",
|
||||
);
|
||||
const verifyTag = shellFunctionSource(publishDraft.run ?? "", "verify_release_tag_target");
|
||||
const targetSha = "a".repeat(40);
|
||||
const tagObjectSha = "b".repeat(40);
|
||||
|
||||
const runVerify = (remoteRefs: string) =>
|
||||
spawnSync(
|
||||
"bash",
|
||||
[
|
||||
"-c",
|
||||
`
|
||||
set -euo pipefail
|
||||
GITHUB_REPOSITORY=openclaw/openclaw
|
||||
RELEASE_TAG=v2026.6.35
|
||||
TARGET_SHA=${targetSha}
|
||||
git() {
|
||||
printf '%s' "\${REMOTE_REFS}"
|
||||
}
|
||||
${verifyTag}
|
||||
verify_release_tag_target
|
||||
`,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, REMOTE_REFS: remoteRefs },
|
||||
},
|
||||
);
|
||||
|
||||
const lightweight = runVerify(`${targetSha}\trefs/tags/v2026.6.35\n`);
|
||||
expect(lightweight.status, lightweight.stderr).toBe(0);
|
||||
|
||||
const annotated = runVerify(
|
||||
`${tagObjectSha}\trefs/tags/v2026.6.35\n${targetSha}\trefs/tags/v2026.6.35^{}\n`,
|
||||
);
|
||||
expect(annotated.status, annotated.stderr).toBe(0);
|
||||
|
||||
const moved = runVerify(`${"c".repeat(40)}\trefs/tags/v2026.6.35\n`);
|
||||
expect(moved.status).toBe(1);
|
||||
expect(moved.stderr).toContain(`must still resolve to ${targetSha}`);
|
||||
|
||||
const missing = runVerify("");
|
||||
expect(missing.status).toBe(1);
|
||||
expect(missing.stderr).toContain("found <missing>");
|
||||
});
|
||||
|
||||
it("fails a missing required local live image before any registry pull", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-live-local-image-"));
|
||||
const bin = join(root, "bin");
|
||||
|
||||
Reference in New Issue
Block a user