ci: persist extended-stable Docker completion

This commit is contained in:
Dallin Romney
2026-08-09 14:50:02 +08:00
parent 883eb91053
commit e323d862cb
12 changed files with 501 additions and 31 deletions
+38
View File
@@ -756,6 +756,7 @@ jobs:
permissions:
contents: read
packages: write
statuses: write
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -763,6 +764,14 @@ jobs:
ref: ${{ needs.resolve_build_provenance.outputs.source_sha }}
fetch-depth: 1
- 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
- *buildkit_prepull_step
- name: Set up Docker Builder
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
@@ -912,3 +921,32 @@ jobs:
--version "${VERSION}" \
--image "${GHCR_IMAGE}" \
--image "${DOCKERHUB_IMAGE}"
# This status is the durable resume boundary. Release visibility is not
# Docker evidence and must never suppress a missing publication.
- name: Record durable extended-stable Docker completion
if: ${{ needs.resolve_release_policy.outputs.channel == 'extended-stable' }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_SHA: ${{ inputs.release_sha }}
VERSION: ${{ needs.resolve_release_policy.outputs.version }}
run: |
set -euo pipefail
payload="$(node .release-harness/scripts/docker-channel-promote.mjs \
--status-payload \
--version "${VERSION}" \
--repository "${GITHUB_REPOSITORY}" \
--source-sha "${RELEASE_SHA}" \
--run-id "${GITHUB_RUN_ID}")"
for attempt in 1 2 3; do
if printf '%s' "${payload}" | gh api --method POST \
"repos/${GITHUB_REPOSITORY}/statuses/${RELEASE_SHA}" \
--input - >/dev/null; then
exit 0
fi
if [[ "${attempt}" != "3" ]]; then
sleep $((attempt * 5))
fi
done
echo "Failed to persist durable Docker completion after verified publication." >&2
exit 1
+133 -12
View File
@@ -103,12 +103,14 @@ jobs:
timeout-minutes: 20
outputs:
sha: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
expected_validation_branch: ${{ steps.inputs.outputs.expected_validation_branch }}
preflight_artifact_name: ${{ steps.preflight_artifact.outputs.name }}
preflight_tarball_sha256: ${{ steps.manifest.outputs.tarball_sha256 }}
full_release_validation_run_attempt: ${{ steps.full_run.outputs.attempt }}
windows_node_installer_digests: ${{ steps.windows_source.outputs.installer_digests }}
steps:
- name: Validate inputs
id: inputs
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
@@ -247,6 +249,16 @@ jobs:
exit 1
;;
esac
expected_validation_branch="${WORKFLOW_REF#refs/heads/}"
if [[ "${PUBLISH_DOCKER_ONLY}" == "true" ]]; then
if [[ ! "${RELEASE_TAG}" =~ ^v([0-9]{4})\.([1-9][0-9]*)\.([1-9][0-9]*)$ ]] || \
(( 10#${BASH_REMATCH[3]:-0} < 33 )); then
echo "Extended-stable Docker closeout requires a final .33+ release tag." >&2
exit 1
fi
expected_validation_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"
fi
echo "expected_validation_branch=${expected_validation_branch}" >> "$GITHUB_OUTPUT"
- name: Validate stable Windows source release
id: windows_source
@@ -486,7 +498,7 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }}
EXPECTED_SHA: ${{ steps.ref.outputs.sha }}
EXPECTED_RELEASE_PROFILE: ${{ inputs.release_profile }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
EXPECTED_WORKFLOW_BRANCH: ${{ steps.inputs.outputs.expected_validation_branch }}
RUN_JSON_FILE: ${{ runner.temp }}/full-release-validation-run.json
TRUSTED_MAIN_REF: refs/remotes/origin/main
VALIDATOR_FILE: ${{ runner.temp }}/release-validation-tooling/validate-full-release-validation-evidence.mjs
@@ -537,6 +549,8 @@ jobs:
- name: Validate release tag is reachable from a trusted release branch
env:
EXPECTED_VALIDATION_BRANCH: ${{ steps.inputs.outputs.expected_validation_branch }}
PUBLISH_DOCKER_ONLY: ${{ inputs.publish_docker_only && 'true' || 'false' }}
RELEASE_TAG: ${{ inputs.tag }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
run: |
@@ -545,6 +559,15 @@ jobs:
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*' \
'+refs/heads/extended-stable/*:refs/remotes/origin/extended-stable/*'
if [[ "${PUBLISH_DOCKER_ONLY}" == "true" ]]; then
expected_ref="refs/remotes/origin/${EXPECTED_VALIDATION_BRANCH}"
if git show-ref --verify --quiet "${expected_ref}" && \
git merge-base --is-ancestor HEAD "${expected_ref}"; then
exit 0
fi
echo "Extended-stable tag ${RELEASE_TAG} must be reachable from ${EXPECTED_VALIDATION_BRANCH}." >&2
exit 1
fi
if git merge-base --is-ancestor HEAD origin/main; then
exit 0
fi
@@ -2385,11 +2408,14 @@ jobs:
runs-on: ubuntu-latest
environment: npm-release
permissions:
actions: read
contents: write
statuses: read
outputs:
docker_already_published: ${{ steps.release.outputs.docker_already_published }}
docker_status_run_id: ${{ steps.release.outputs.docker_status_run_id }}
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:
@@ -2550,7 +2576,7 @@ jobs:
- 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.
- Container publication: this closeout requires durable Docker completion on the immutable release SHA.
- Excluded: ClawHub, macOS, Windows, mobile, website, and private dist-tags.
EOF
node .release-harness/scripts/render-github-release-notes.mjs \
@@ -2560,7 +2586,7 @@ jobs:
--verification-file "${verification_file}" \
--output "${notes_file}"
release_already_public=false
release_is_public=false
release_id="$(resolve_release_id)"
if [[ -n "${release_id}" ]]; then
verify_release_resource "${release_id}"
@@ -2591,8 +2617,8 @@ jobs:
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."
release_is_public=true
echo "Existing canonical public release found; Docker completion will be checked independently."
else
echo "GitHub release ${release_id} has an invalid draft state." >&2
exit 1
@@ -2600,11 +2626,39 @@ jobs:
release_body_sha256="$(
sha256sum "${notes_file}" | awk '{print $1}'
)"
docker_status_file="${RUNNER_TEMP}/extended-stable-docker-status.json"
gh api "repos/${GITHUB_REPOSITORY}/commits/${TARGET_SHA}/status" > "${docker_status_file}"
docker_status_run_id="$(node .release-harness/scripts/docker-channel-promote.mjs \
--find-status-file "${docker_status_file}" \
--version "${release_version}" \
--repository "${GITHUB_REPOSITORY}" \
--source-sha "${TARGET_SHA}")"
docker_already_published=false
if [[ -n "${docker_status_run_id}" ]]; then
docker_run_file="${RUNNER_TEMP}/extended-stable-docker-run.json"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${docker_status_run_id}" > "${docker_run_file}"
jq -e --arg repository "${GITHUB_REPOSITORY}" \
--arg run_id "${docker_status_run_id}" '
(.id | tostring) == $run_id and
.name == "OpenClaw Release Publish" and
(.path | split("@")[0]) == ".github/workflows/openclaw-release-publish.yml" and
.event == "workflow_dispatch" and
.status == "completed" and
.repository.full_name == $repository
' "${docker_run_file}" >/dev/null || {
echo "Docker completion status does not reference a completed OpenClaw Release Publish run." >&2
exit 1
}
docker_already_published=true
fi
echo "release_id=${release_id}" >> "$GITHUB_OUTPUT"
echo "release_body_sha256=${release_body_sha256}" >> "$GITHUB_OUTPUT"
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"
echo "docker_already_published=${docker_already_published}" >> "$GITHUB_OUTPUT"
echo "docker_status_run_id=${docker_status_run_id}" >> "$GITHUB_OUTPUT"
if [[ "${docker_already_published}" == "true" ]]; then
echo "- Docker completion: durable status from run ${docker_status_run_id}; rebuild skipped" >> "$GITHUB_STEP_SUMMARY"
elif [[ "${release_is_public}" == "true" ]]; then
echo "- Extended-stable GitHub release: public without Docker completion; Docker publication required" >> "$GITHUB_STEP_SUMMARY"
else
echo "- Extended-stable GitHub release draft: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
fi
@@ -2612,7 +2666,7 @@ jobs:
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' && needs.prepare_extended_stable_release.outputs.release_already_public != 'true')) }}
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.docker_already_published != 'true')) }}
uses: ./.github/workflows/docker-release.yml
with:
tag: ${{ inputs.tag }}
@@ -2623,6 +2677,7 @@ jobs:
permissions:
contents: read
packages: write
statuses: write
finalize_github_release:
name: Finalize GitHub release
@@ -2651,10 +2706,76 @@ jobs:
exit 1
fi
verify_extended_stable_docker_completion:
name: Verify durable extended-stable Docker completion
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' || (needs.publish_docker.result == 'skipped' && needs.prepare_extended_stable_release.outputs.docker_already_published == 'true')) }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
statuses: read
steps:
- name: Checkout trusted release tooling
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify durable Docker completion status
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}"
status_file="${RUNNER_TEMP}/extended-stable-docker-status.json"
docker_status_run_id=""
for attempt in $(seq 1 12); do
gh api "repos/${GITHUB_REPOSITORY}/commits/${TARGET_SHA}/status" > "${status_file}"
docker_status_run_id="$(node scripts/docker-channel-promote.mjs \
--find-status-file "${status_file}" \
--version "${release_version}" \
--repository "${GITHUB_REPOSITORY}" \
--source-sha "${TARGET_SHA}")"
if [[ -n "${docker_status_run_id}" ]]; then
break
fi
if [[ "${attempt}" == "12" ]]; then
echo "Docker publication completed without a durable completion status." >&2
exit 1
fi
sleep 5
done
docker_run_file="${RUNNER_TEMP}/extended-stable-docker-run.json"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${docker_status_run_id}" > "${docker_run_file}"
jq -e --arg current_run_id "${GITHUB_RUN_ID}" \
--arg repository "${GITHUB_REPOSITORY}" \
--arg run_id "${docker_status_run_id}" '
(.id | tostring) == $run_id and
.name == "OpenClaw Release Publish" and
(.path | split("@")[0]) == ".github/workflows/openclaw-release-publish.yml" and
.event == "workflow_dispatch" and
(.status == "completed" or ($run_id == $current_run_id and .status == "in_progress")) and
.repository.full_name == $repository
' "${docker_run_file}" >/dev/null || {
echo "Docker completion status does not reference the expected release workflow run." >&2
exit 1
}
echo "- Durable Docker completion: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${docker_status_run_id}" >> "$GITHUB_STEP_SUMMARY"
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' }}
needs:
[
resolve_release_target,
prepare_extended_stable_release,
verify_extended_stable_docker_completion,
]
if: ${{ always() && inputs.publish_docker_only && needs.prepare_extended_stable_release.result == 'success' && needs.verify_extended_stable_docker_completion.result == 'success' }}
runs-on: ubuntu-latest
environment: npm-release
permissions: