mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
+4344
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "openclaw-release-vercel-cli",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"vercel": "58.4.4"
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
Reference in New Issue
Block a user