ci: mirror Docker release images to Vercel registry (#120058)

* ci: publish release images to Vercel registry

* ci: publish beta images to Vercel registry

* fix(ci): allow beta Vercel dispatch

* fix(ci): publish VCR-compatible image indexes

* fix(ci): allow VCR readiness propagation

* fix(ci): use Sandbox as VCR readiness proof

* fix(ci): promote clean VCR channel indexes

* fix(release): harden VCR publication

* fix(release): bind VCR publishing to verified inputs

* fix(test): follow script declaration migration

* fix(release): isolate VCR mirroring

* test(release): align VCR secret ownership
This commit is contained in:
Patrick Erichsen
2026-08-10 14:45:05 -07:00
committed by GitHub
parent db879e73fa
commit 105038e658
11 changed files with 5842 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
{
"name": "openclaw-release-vercel-cli",
"private": true,
"version": "1.0.0",
"dependencies": {
"vercel": "58.4.4"
}
}
+69
View File
@@ -11,6 +11,16 @@ on:
description: Full immutable commit SHA resolved from tag
required: true
type: string
outputs:
version:
description: Resolved Docker release version without the v prefix
value: ${{ jobs.resolve_release_policy.outputs.version }}
include_browser:
description: Whether the tagged Docker release includes browser images
value: ${{ jobs.create-manifest.outputs.browser_supported }}
vcr_source_refs:
description: Newline-delimited attestation-verified immutable GHCR source refs
value: ${{ jobs.verify-attestations.outputs.vcr_source_refs }}
secrets:
DOCKERHUB_USERNAME:
required: true
@@ -631,6 +641,8 @@ jobs:
permissions:
packages: write
contents: read
outputs:
browser_supported: ${{ steps.tags.outputs.browser_supported }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -687,6 +699,11 @@ jobs:
exit 1
fi
{
if [[ "${browser_supported}" == "1" ]]; then
echo "browser_supported=true"
else
echo "browser_supported=false"
fi
echo "value<<EOF"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
@@ -753,6 +770,8 @@ jobs:
needs: [resolve_release_policy, resolve_build_provenance, create-manifest]
if: ${{ always() && needs.create-manifest.result == 'success' }}
runs-on: ubuntu-24.04
outputs:
vcr_source_refs: ${{ steps.vcr_source_refs.outputs.value }}
permissions:
contents: read
packages: write
@@ -901,6 +920,56 @@ jobs:
--platform linux/arm64 \
"${dockerhub_arm64_refs[@]}"
# Resolve the moving release tags once, then verify the exact immutable
# indexes that the VCR publisher will consume. Passing those digest refs
# across the job boundary prevents a later tag rewrite from changing the
# bytes copied after attestation verification.
- name: Resolve and verify immutable VCR source refs
id: vcr_source_refs
shell: bash
env:
GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
INCLUDE_BROWSER: ${{ needs.create-manifest.outputs.browser_supported }}
VERSION: ${{ needs.resolve_release_policy.outputs.version }}
run: |
set -euo pipefail
aliases=(default slim)
tags=("${VERSION}" "${VERSION}-slim")
if [[ "${INCLUDE_BROWSER}" == "true" ]]; then
aliases+=(browser)
tags+=("${VERSION}-browser")
fi
source_refs=()
immutable_refs=()
for index in "${!aliases[@]}"; do
tagged_ref="${GHCR_IMAGE}:${tags[$index]}"
descriptor="$(
docker buildx imagetools inspect \
"${tagged_ref}" \
--format '{{json .Manifest}}'
)"
digest="$(
jq -er \
'.digest | select(type == "string" and test("^sha256:[a-f0-9]{64}$"))' \
<<< "${descriptor}"
)"
immutable_ref="${GHCR_IMAGE}@${digest}"
source_refs+=("${aliases[$index]}=${immutable_ref}")
immutable_refs+=("${immutable_ref}")
done
node scripts/verify-docker-attestations.mjs \
--platform linux/amd64 \
--platform linux/arm64 \
"${immutable_refs[@]}"
{
echo "value<<EOF"
printf '%s\n' "${source_refs[@]}"
echo "EOF"
} >> "${GITHUB_OUTPUT}"
- name: Promote and verify channel aliases
if: ${{ needs.resolve_release_policy.outputs.channel != 'beta' }}
env:
@@ -2424,6 +2424,20 @@ jobs:
contents: read
packages: write
publish_vcr:
name: Mirror Docker images to Vercel Container Registry
needs: [publish_docker]
if: ${{ always() && needs.publish_docker.result == 'success' }}
uses: ./.github/workflows/vercel-container-registry-publish.yml
with:
include_browser: ${{ needs.publish_docker.outputs.include_browser == 'true' }}
source_refs: ${{ needs.publish_docker.outputs.vcr_source_refs }}
version: ${{ needs.publish_docker.outputs.version }}
secrets:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
permissions:
contents: read
finalize_github_release:
name: Finalize GitHub release
needs: [publish, publish_docker]
@@ -0,0 +1,226 @@
name: Vercel Container Registry Publish
on:
workflow_call:
inputs:
version:
description: Docker release version without the v prefix
required: true
type: string
source_refs:
description: Newline-delimited alias=immutable-ref entries verified by the caller
required: true
type: string
include_browser:
description: Whether the tagged Docker release includes browser images
required: true
type: boolean
secrets:
VERCEL_TOKEN:
required: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
concurrency:
# Alias promotion is read-then-write. Serialize VCR independently so a slow
# mirror never holds the Docker publication lock.
group: vcr-release-publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-24.04
timeout-minutes: 30
# VCR is a best-effort mirror. Failed steps remain visible, but the primary
# Docker publication and GitHub release finalization must continue.
continue-on-error: true
permissions:
contents: read
steps:
- name: Checkout trusted registry tooling
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Set up Docker Builder
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Setup trusted Node runtime
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
# Materialize the complete CLI dependency closure before the production
# token enters any step environment. The committed lock and integrity
# checks keep registry and Sandbox operations on reviewed executable code.
- name: Materialize locked Vercel CLI
id: vercel_cli
run: |
set -euo pipefail
bash scripts/materialize-vercel-cli.sh \
.github/release/vercel-cli \
"${RUNNER_TEMP}/vercel-cli" \
"${GITHUB_OUTPUT}"
- name: Validate Vercel Container Registry configuration
env:
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_SCOPE: ${{ vars.VERCEL_SCOPE }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_VCR_IMAGE: ${{ vars.VERCEL_VCR_IMAGE }}
run: |
set -euo pipefail
for name in VERCEL_PROJECT_ID VERCEL_SCOPE VERCEL_TOKEN VERCEL_VCR_IMAGE; do
if [[ -z "${!name}" ]]; then
echo "::error::${name} is required for Vercel Container Registry publishing."
exit 1
fi
done
if [[ "${VERCEL_VCR_IMAGE}" != vcr.vercel.com/*/*/* ]]; then
echo "::error::VERCEL_VCR_IMAGE must be a full vcr.vercel.com/team/project/repository image name."
exit 1
fi
- name: Authenticate Docker to Vercel Container Registry
env:
VERCEL_CLI: ${{ steps.vercel_cli.outputs.cli }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_SCOPE: ${{ vars.VERCEL_SCOPE }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
set -euo pipefail
"${VERCEL_CLI}" vcr login docker \
--project "${VERCEL_PROJECT_ID}" \
--scope "${VERCEL_SCOPE}" \
--format json
- name: Copy and verify immutable release images
id: copy_images
env:
INCLUDE_BROWSER: ${{ inputs.include_browser }}
SOURCE_REFS: ${{ inputs.source_refs }}
TARGET_IMAGE: ${{ vars.VERCEL_VCR_IMAGE }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
browser_args=()
if [[ "${INCLUDE_BROWSER}" == "true" ]]; then
browser_args+=(--include-browser)
fi
source_ref_args=()
while IFS= read -r source_ref; do
[[ -z "${source_ref}" ]] && continue
source_ref_args+=(--source-ref "${source_ref}")
done <<< "${SOURCE_REFS}"
node scripts/vercel-container-registry-publish.mjs \
--version "${VERSION}" \
"${source_ref_args[@]}" \
--target-image "${TARGET_IMAGE}" \
"${browser_args[@]}"
# VCR tag status can remain null after an image is usable, so Sandbox
# creation is the authoritative readiness check for this publication.
- name: Run custom-image Sandbox smoke
id: sandbox_smoke
env:
VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_CLI: ${{ steps.vercel_cli.outputs.cli }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_SCOPE: ${{ vars.VERCEL_SCOPE }}
VERCEL_VCR_IMAGE: ${{ vars.VERCEL_VCR_IMAGE }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
repository="${VERCEL_VCR_IMAGE##*/}"
retry_deadline=$((SECONDS + 600))
attempt=1
while true; do
set +e
output="$(
"${VERCEL_CLI}" sandbox run \
--silent \
--rm \
--non-persistent \
--network-policy deny-all \
--timeout 3m \
--project "${VERCEL_PROJECT_ID}" \
--scope "${VERCEL_SCOPE}" \
--image "${repository}:${VERSION}" \
--workdir /app \
--env "EXPECTED_VERSION=${VERSION}" \
-- /bin/sh -lc 'test "$(node -p "require(\"./package.json\").version")" = "${EXPECTED_VERSION}" && node openclaw.mjs --version' \
2>&1
)"
status=$?
set -e
printf '%s\n' "${output}"
if [[ "${status}" -eq 0 ]]; then
break
fi
if [[ "${output}" != *"image_not_ready"* ]]; then
exit "${status}"
fi
if (( SECONDS >= retry_deadline )); then
echo "::error::Vercel image preparation remained image_not_ready for 10 minutes."
exit "${status}"
fi
delay=10
remaining=$((retry_deadline - SECONDS))
if (( delay > remaining )); then
delay="${remaining}"
fi
echo "::notice::Vercel image is not ready; retrying Sandbox smoke in ${delay}s (attempt ${attempt})."
sleep "${delay}"
attempt=$((attempt + 1))
done
- name: Promote and verify channel aliases
id: promote_aliases
env:
INCLUDE_BROWSER: ${{ inputs.include_browser }}
TARGET_IMAGE: ${{ vars.VERCEL_VCR_IMAGE }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
browser_args=()
if [[ "${INCLUDE_BROWSER}" == "true" ]]; then
browser_args+=(--include-browser)
fi
node scripts/vercel-container-registry-publish.mjs \
--version "${VERSION}" \
--target-image "${TARGET_IMAGE}" \
--promote-aliases \
"${browser_args[@]}"
- name: Summarize Vercel publication
if: ${{ always() }}
env:
COPY_OUTCOME: ${{ steps.copy_images.outcome }}
INCLUDE_BROWSER: ${{ inputs.include_browser }}
PROMOTE_OUTCOME: ${{ steps.promote_aliases.outcome }}
SMOKE_OUTCOME: ${{ steps.sandbox_smoke.outcome }}
TARGET_IMAGE: ${{ vars.VERCEL_VCR_IMAGE }}
VERSION: ${{ inputs.version }}
run: |
variants="default, slim"
if [[ "${INCLUDE_BROWSER}" == "true" ]]; then
variants="${variants}, browser"
fi
status="passed"
if [[ "${COPY_OUTCOME}" != "success" || "${SMOKE_OUTCOME}" != "success" || "${PROMOTE_OUTCOME}" != "success" ]]; then
status="failed (non-blocking)"
echo "::warning::VCR mirror failed; Docker publication and GitHub release finalization are unaffected."
fi
{
echo "## Vercel Container Registry"
echo "- Image: ${TARGET_IMAGE}:${VERSION}"
echo "- Variants: ${variants}"
echo "- Platforms: linux/amd64, linux/arm64"
echo "- Status: ${status}"
echo "- Copy: ${COPY_OUTCOME}"
echo "- Sandbox smoke: ${SMOKE_OUTCOME}"
echo "- Alias promotion: ${PROMOTE_OUTCOME}"
} >> "${GITHUB_STEP_SUMMARY}"