mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(release): isolate extended-stable Docker aliases
This commit is contained in:
@@ -224,7 +224,7 @@ this release skill, not as a separate release workflow.
|
||||
|
||||
The backport ability owns the complete mainline inventory, private-security
|
||||
reconciliation, candidate decisions, maintainer approval, coordinated staging
|
||||
PR, and proof handoff. After that PR lands, use the dedicated npm-only sequence
|
||||
PR, and proof handoff. After that PR lands, use the dedicated extended-stable sequence
|
||||
below. Never route `.33+` through the regular beta/stable release sequence.
|
||||
|
||||
## Publish extended-stable releases
|
||||
@@ -272,8 +272,15 @@ on pinned current `main` as the exact command and validation contract.
|
||||
version, use the approved credential-isolated release tooling for manual
|
||||
plugin tag repair; the OIDC source workflow cannot mutate that tag. Never
|
||||
republish an immutable version.
|
||||
9. Do not create a GitHub Release or publish macOS, Windows, Docker, mobile,
|
||||
website, ClawHub, or private dist-tag artifacts from this path.
|
||||
9. Require the tag-triggered `Docker Release` run to publish and verify the
|
||||
exact default, slim, browser, and architecture images in GHCR and Docker
|
||||
Hub. It must advance only `extended-stable`, `extended-stable-slim`, and
|
||||
`extended-stable-browser`. If immutable images already exist but channel
|
||||
aliases need repair, dispatch `docker-release.yml` from current `main` with
|
||||
the exact tag and `operation=promote-channel`; never rebuild or move the
|
||||
immutable release tag just to repair aliases.
|
||||
10. Do not create a GitHub Release or publish macOS, Windows, mobile, website,
|
||||
ClawHub, or private dist-tag artifacts from this path.
|
||||
|
||||
## Keep release channel naming aligned
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Extended-Stable Backport Preparation
|
||||
|
||||
Prepare the next npm maintenance patch for the active `extended-stable` line.
|
||||
Prepare the next npm and Docker maintenance patch for the active `extended-stable` line.
|
||||
Discover the complete candidate set, obtain maintainer approval, and prepare
|
||||
the approved commits as one coordinated PR. Treat commits as canonical; use
|
||||
PRs, issues, ClawSweeper reports, and advisories as supporting context.
|
||||
@@ -15,15 +15,15 @@ extended-stable package and publication constraints.
|
||||
- Read `docs/reference/RELEASING.md`,
|
||||
`scripts/openclaw-npm-extended-stable-release.mjs`, and the relevant release
|
||||
workflows from a pinned current `origin/main` before resolving the line.
|
||||
- Target npm `extended-stable` and the canonical
|
||||
- Target npm and Docker `extended-stable` and the canonical
|
||||
`extended-stable/YYYY.M.33` branch. The user-facing `extended-stable` update
|
||||
channel resolves that selector; user-facing `stable` continues to resolve
|
||||
npm `latest`.
|
||||
- Cover the core `openclaw` package and every npm-publishable official plugin
|
||||
included by the canonical `all-publishable` release inventory at the same
|
||||
exact version.
|
||||
- Exclude ClawHub publication, GitHub Releases, native apps, Docker images,
|
||||
mobile artifacts, website downloads, and private-repository dist-tags.
|
||||
- Exclude ClawHub publication, GitHub Releases, native apps, mobile artifacts,
|
||||
website downloads, and private-repository dist-tags.
|
||||
- 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.
|
||||
@@ -148,9 +148,10 @@ contracts.
|
||||
|
||||
## Filter by Publication Surface
|
||||
|
||||
Include only fixes that affect the core package or an npm-publishable official
|
||||
plugin in the exact release inventory. Prove package inclusion rather than
|
||||
inferring it from the source path alone.
|
||||
Include only fixes that affect the core package, an npm-publishable official
|
||||
plugin in the exact release inventory, or the official Docker image/runtime
|
||||
path. Prove package or image inclusion rather than inferring it from the source
|
||||
path alone.
|
||||
|
||||
- Do not exclude `extensions/**` by path. Determine whether the package appears
|
||||
in the canonical `all-publishable` inventory.
|
||||
@@ -158,8 +159,8 @@ inferring it from the source path alone.
|
||||
at the same intended version and can verify its exact package and selector.
|
||||
- Treat ClawHub-only, external, private, or otherwise unlisted plugin changes as
|
||||
out of scope.
|
||||
- Treat native-only, Docker-only, mobile-only, website-only, and GitHub
|
||||
Release-only fixes as `skip` for this npm-only line.
|
||||
- Treat native-only, mobile-only, website-only, and GitHub Release-only fixes
|
||||
as `skip` for this extended-stable line.
|
||||
- Treat cross-repository or package-topology uncertainty as `blocked` until the
|
||||
shipped npm surface and release owner are proven.
|
||||
|
||||
|
||||
@@ -14,9 +14,17 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Existing stable or beta release tag to backfill (for example v2026.3.22 or v2026.3.22-beta.1)
|
||||
description: Existing stable, extended-stable, or beta release tag
|
||||
required: true
|
||||
type: string
|
||||
operation:
|
||||
description: Build immutable images, or promote an existing immutable version to its moving channel aliases
|
||||
required: true
|
||||
type: choice
|
||||
default: backfill
|
||||
options:
|
||||
- backfill
|
||||
- promote-channel
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'workflow_dispatch' && format('docker-release-manual-{0}', inputs.tag) || format('docker-release-push-{0}', github.run_id) }}
|
||||
@@ -45,7 +53,7 @@ jobs:
|
||||
echo "Docker alpha image publishing is disabled."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then
|
||||
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-(beta\.)?[1-9][0-9]*)?$ ]]; then
|
||||
echo "Invalid release tag: ${RELEASE_TAG}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -56,18 +64,69 @@ jobs:
|
||||
ref: refs/tags/${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
resolve_release_policy:
|
||||
needs: validate_manual_backfill
|
||||
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.validate_manual_backfill.result == 'success') }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
version: ${{ steps.policy.outputs.version }}
|
||||
channel: ${{ steps.policy.outputs.channel }}
|
||||
default_aliases: ${{ steps.policy.outputs.default_aliases }}
|
||||
slim_aliases: ${{ steps.policy.outputs.slim_aliases }}
|
||||
browser_aliases: ${{ steps.policy.outputs.browser_aliases }}
|
||||
steps:
|
||||
- name: Checkout trusted workflow helpers
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: workflow-source
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve release channel policy
|
||||
id: policy
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
|
||||
RELEASE_OPERATION: ${{ github.event_name == 'workflow_dispatch' && inputs.operation || 'release' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${SOURCE_REF}" != refs/tags/v* ]]; then
|
||||
echo "::error::Docker releases require a v-prefixed release tag; got ${SOURCE_REF}."
|
||||
exit 1
|
||||
fi
|
||||
version="${SOURCE_REF#refs/tags/v}"
|
||||
policy="$(node workflow-source/scripts/lib/docker-release-policy.mjs "${version}")"
|
||||
channel="$(jq -r '.channel' <<< "${policy}")"
|
||||
if [[ "${RELEASE_OPERATION}" == "promote-channel" && "${channel}" == "beta" ]]; then
|
||||
echo "::error::Beta releases have no moving Docker channel aliases to promote."
|
||||
exit 1
|
||||
fi
|
||||
default_aliases="$(jq -r '.movingAliases.default | join(" ")' <<< "${policy}")"
|
||||
slim_aliases="$(jq -r '.movingAliases.slim | join(" ")' <<< "${policy}")"
|
||||
browser_aliases="$(jq -r '.movingAliases.browser | join(" ")' <<< "${policy}")"
|
||||
{
|
||||
echo "version=${version}"
|
||||
echo "channel=${channel}"
|
||||
echo "default_aliases=${default_aliases}"
|
||||
echo "slim_aliases=${slim_aliases}"
|
||||
echo "browser_aliases=${browser_aliases}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
approve_manual_backfill:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
needs: validate_manual_backfill
|
||||
needs: [validate_manual_backfill, resolve_release_policy]
|
||||
# WARNING: KEEP MANUAL BACKFILLS GATED BY THE docker-release ENVIRONMENT.
|
||||
runs-on: ubuntu-24.04
|
||||
environment: docker-release
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Approve Docker backfill
|
||||
- name: Approve Docker operation
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
run: echo "Approved Docker backfill for $RELEASE_TAG"
|
||||
RELEASE_OPERATION: ${{ inputs.operation }}
|
||||
run: echo "Approved Docker ${RELEASE_OPERATION} for ${RELEASE_TAG}"
|
||||
|
||||
validate_publish_config:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -88,8 +147,8 @@ jobs:
|
||||
echo "Docker Hub publishing configured for ${DOCKERHUB_IMAGE}."
|
||||
|
||||
resolve_build_provenance:
|
||||
needs: [approve_manual_backfill, validate_publish_config]
|
||||
if: ${{ always() && needs.validate_publish_config.result == 'success' && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
|
||||
needs: [approve_manual_backfill, resolve_release_policy, validate_publish_config]
|
||||
if: ${{ always() && needs.resolve_release_policy.result == 'success' && needs.validate_publish_config.result == 'success' && (github.event_name != 'workflow_dispatch' || (needs.approve_manual_backfill.result == 'success' && inputs.operation == 'backfill')) }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -555,6 +614,7 @@ jobs:
|
||||
needs:
|
||||
[
|
||||
approve_manual_backfill,
|
||||
resolve_release_policy,
|
||||
validate_publish_config,
|
||||
resolve_build_provenance,
|
||||
build-amd64,
|
||||
@@ -595,6 +655,9 @@ jobs:
|
||||
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
|
||||
IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
|
||||
DEFAULT_ALIASES: ${{ needs.resolve_release_policy.outputs.default_aliases }}
|
||||
SLIM_ALIASES: ${{ needs.resolve_release_policy.outputs.slim_aliases }}
|
||||
BROWSER_ALIASES: ${{ needs.resolve_release_policy.outputs.browser_aliases }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tags=()
|
||||
@@ -617,16 +680,25 @@ jobs:
|
||||
browser_tags+=("${GHCR_IMAGE}:${version}-browser")
|
||||
dockerhub_browser_tags+=("${DOCKERHUB_IMAGE}:${version}-browser")
|
||||
fi
|
||||
# Beta releases and manual backfills publish only immutable version tags;
|
||||
# do not advance latest/main aliases from those flows.
|
||||
if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
|
||||
tags+=("${GHCR_IMAGE}:latest" "${GHCR_IMAGE}:main")
|
||||
slim_tags+=("${GHCR_IMAGE}:slim" "${GHCR_IMAGE}:main-slim")
|
||||
dockerhub_tags+=("${DOCKERHUB_IMAGE}:latest" "${DOCKERHUB_IMAGE}:main")
|
||||
dockerhub_slim_tags+=("${DOCKERHUB_IMAGE}:slim" "${DOCKERHUB_IMAGE}:main-slim")
|
||||
# Manual backfills publish immutable tags only. Tag pushes advance
|
||||
# only the moving aliases assigned by the release-version policy.
|
||||
if [[ "${IS_MANUAL_BACKFILL}" != "1" ]]; then
|
||||
read -r -a default_aliases <<< "${DEFAULT_ALIASES}"
|
||||
read -r -a slim_aliases <<< "${SLIM_ALIASES}"
|
||||
read -r -a browser_aliases <<< "${BROWSER_ALIASES}"
|
||||
for alias in "${default_aliases[@]}"; do
|
||||
tags+=("${GHCR_IMAGE}:${alias}")
|
||||
dockerhub_tags+=("${DOCKERHUB_IMAGE}:${alias}")
|
||||
done
|
||||
for alias in "${slim_aliases[@]}"; do
|
||||
slim_tags+=("${GHCR_IMAGE}:${alias}")
|
||||
dockerhub_slim_tags+=("${DOCKERHUB_IMAGE}:${alias}")
|
||||
done
|
||||
if [[ "${browser_supported}" == "1" ]]; then
|
||||
browser_tags+=("${GHCR_IMAGE}:latest-browser" "${GHCR_IMAGE}:main-browser")
|
||||
dockerhub_browser_tags+=("${DOCKERHUB_IMAGE}:latest-browser" "${DOCKERHUB_IMAGE}:main-browser")
|
||||
for alias in "${browser_aliases[@]}"; do
|
||||
browser_tags+=("${GHCR_IMAGE}:${alias}")
|
||||
dockerhub_browser_tags+=("${DOCKERHUB_IMAGE}:${alias}")
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -696,8 +768,129 @@ jobs:
|
||||
"${dockerhub_browser_tags[@]}"
|
||||
fi
|
||||
|
||||
promote-channel-aliases:
|
||||
needs: [approve_manual_backfill, resolve_release_policy, validate_publish_config]
|
||||
if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.operation == 'promote-channel' && needs.approve_manual_backfill.result == 'success' && needs.resolve_release_policy.result == 'success' && needs.validate_publish_config.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout selected tag
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: refs/tags/${{ inputs.tag }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- *buildkit_prepull_step
|
||||
- name: Set up Docker Builder
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||
with:
|
||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Promote and verify channel aliases
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.resolve_release_policy.outputs.version }}
|
||||
RELEASE_CHANNEL: ${{ needs.resolve_release_policy.outputs.channel }}
|
||||
DEFAULT_ALIASES: ${{ needs.resolve_release_policy.outputs.default_aliases }}
|
||||
SLIM_ALIASES: ${{ needs.resolve_release_policy.outputs.slim_aliases }}
|
||||
BROWSER_ALIASES: ${{ needs.resolve_release_policy.outputs.browser_aliases }}
|
||||
GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
manifest_digest() {
|
||||
local ref="$1"
|
||||
docker buildx imagetools inspect "${ref}" --format '{{json .Manifest}}' |
|
||||
jq -er '.digest | select(startswith("sha256:"))'
|
||||
}
|
||||
|
||||
require_group_source() {
|
||||
local image="$1"
|
||||
local suffix="$2"
|
||||
local aliases_text="$3"
|
||||
if [[ -n "${aliases_text}" ]]; then
|
||||
manifest_digest "${image}:${VERSION}${suffix}" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
promote_group() {
|
||||
local image="$1"
|
||||
local suffix="$2"
|
||||
local aliases_text="$3"
|
||||
if [[ -z "${aliases_text}" ]]; then
|
||||
return
|
||||
fi
|
||||
local source_ref="${image}:${VERSION}${suffix}"
|
||||
local source_digest
|
||||
source_digest="$(manifest_digest "${source_ref}")"
|
||||
local aliases=()
|
||||
local create_args=()
|
||||
read -r -a aliases <<< "${aliases_text}"
|
||||
for alias in "${aliases[@]}"; do
|
||||
create_args+=(--tag "${image}:${alias}")
|
||||
done
|
||||
docker buildx imagetools create --prefer-index=false \
|
||||
"${create_args[@]}" "${image}@${source_digest}"
|
||||
for alias in "${aliases[@]}"; do
|
||||
local target_ref="${image}:${alias}"
|
||||
local target_digest
|
||||
target_digest="$(manifest_digest "${target_ref}")"
|
||||
if [[ "${target_digest}" != "${source_digest}" ]]; then
|
||||
echo "::error::${target_ref} resolved to ${target_digest}, expected ${source_digest}."
|
||||
exit 1
|
||||
fi
|
||||
echo "Verified ${target_ref} -> ${source_digest}."
|
||||
done
|
||||
}
|
||||
|
||||
browser_supported=0
|
||||
if grep -q '^ARG OPENCLAW_INSTALL_BROWSER' Dockerfile; then
|
||||
browser_supported=1
|
||||
fi
|
||||
|
||||
# Resolve every immutable source before moving any alias so missing
|
||||
# release images fail without leaving a partially promoted channel.
|
||||
for image in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||
require_group_source "${image}" "" "${DEFAULT_ALIASES}"
|
||||
require_group_source "${image}" "-slim" "${SLIM_ALIASES}"
|
||||
if [[ "${browser_supported}" == "1" ]]; then
|
||||
require_group_source "${image}" "-browser" "${BROWSER_ALIASES}"
|
||||
fi
|
||||
done
|
||||
|
||||
for image in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||
promote_group "${image}" "" "${DEFAULT_ALIASES}"
|
||||
promote_group "${image}" "-slim" "${SLIM_ALIASES}"
|
||||
if [[ "${browser_supported}" == "1" ]]; then
|
||||
promote_group "${image}" "-browser" "${BROWSER_ALIASES}"
|
||||
fi
|
||||
done
|
||||
|
||||
{
|
||||
echo "## Docker channel promotion"
|
||||
echo "- Version: ${VERSION}"
|
||||
echo "- Channel: ${RELEASE_CHANNEL}"
|
||||
echo "- Registries: ${GHCR_IMAGE}, ${DOCKERHUB_IMAGE}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
verify-attestations:
|
||||
needs: [resolve_build_provenance, create-manifest]
|
||||
needs: [resolve_release_policy, resolve_build_provenance, create-manifest]
|
||||
if: ${{ always() && needs.create-manifest.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
@@ -736,6 +929,9 @@ jobs:
|
||||
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
|
||||
IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
|
||||
DEFAULT_ALIASES: ${{ needs.resolve_release_policy.outputs.default_aliases }}
|
||||
SLIM_ALIASES: ${{ needs.resolve_release_policy.outputs.slim_aliases }}
|
||||
BROWSER_ALIASES: ${{ needs.resolve_release_policy.outputs.browser_aliases }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
multi_refs=()
|
||||
@@ -784,16 +980,25 @@ jobs:
|
||||
arm64_refs+=("${GHCR_IMAGE}:${version}-browser-arm64")
|
||||
dockerhub_arm64_refs+=("${DOCKERHUB_IMAGE}:${version}-browser-arm64")
|
||||
fi
|
||||
# Beta releases and manual backfills publish only immutable version tags;
|
||||
# do not advance latest/main aliases from those flows.
|
||||
if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
|
||||
multi_refs+=("${GHCR_IMAGE}:latest" "${GHCR_IMAGE}:main")
|
||||
slim_multi_refs+=("${GHCR_IMAGE}:slim" "${GHCR_IMAGE}:main-slim")
|
||||
dockerhub_multi_refs+=("${DOCKERHUB_IMAGE}:latest" "${DOCKERHUB_IMAGE}:main")
|
||||
dockerhub_slim_multi_refs+=("${DOCKERHUB_IMAGE}:slim" "${DOCKERHUB_IMAGE}:main-slim")
|
||||
# Manual backfills verify immutable tags only. Tag pushes also
|
||||
# verify the moving aliases selected by the release policy.
|
||||
if [[ "${IS_MANUAL_BACKFILL}" != "1" ]]; then
|
||||
read -r -a default_aliases <<< "${DEFAULT_ALIASES}"
|
||||
read -r -a slim_aliases <<< "${SLIM_ALIASES}"
|
||||
read -r -a browser_aliases <<< "${BROWSER_ALIASES}"
|
||||
for alias in "${default_aliases[@]}"; do
|
||||
multi_refs+=("${GHCR_IMAGE}:${alias}")
|
||||
dockerhub_multi_refs+=("${DOCKERHUB_IMAGE}:${alias}")
|
||||
done
|
||||
for alias in "${slim_aliases[@]}"; do
|
||||
slim_multi_refs+=("${GHCR_IMAGE}:${alias}")
|
||||
dockerhub_slim_multi_refs+=("${DOCKERHUB_IMAGE}:${alias}")
|
||||
done
|
||||
if [[ "${browser_supported}" == "1" ]]; then
|
||||
multi_refs+=("${GHCR_IMAGE}:latest-browser" "${GHCR_IMAGE}:main-browser")
|
||||
dockerhub_multi_refs+=("${DOCKERHUB_IMAGE}:latest-browser" "${DOCKERHUB_IMAGE}:main-browser")
|
||||
for alias in "${browser_aliases[@]}"; do
|
||||
multi_refs+=("${GHCR_IMAGE}:${alias}")
|
||||
dockerhub_multi_refs+=("${DOCKERHUB_IMAGE}:${alias}")
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
+1
-1
@@ -8739,7 +8739,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- Headings:
|
||||
- H2: Version naming
|
||||
- H2: Release cadence
|
||||
- H2: Monthly npm-only extended-stable publication
|
||||
- H2: Monthly extended-stable publication
|
||||
- H2: Regular release operator checklist
|
||||
- H2: Stable main closeout
|
||||
- H2: Release preflight
|
||||
|
||||
@@ -43,7 +43,7 @@ Hosting multiple users? See [Multi-tenant hosting](/gateway/multi-tenant-hosting
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
Use `ghcr.io/openclaw/openclaw` or `openclaw/openclaw` and avoid unofficial mirrors, which don't share OpenClaw's release timing or retention policy. Official tags: `main`, `latest`, `<version>` (e.g. `2026.2.26`), and beta tags such as `2026.2.26-beta.1` (betas never move `latest`/`main`). The default `main`/`latest`/`<version>` image bundles the `codex` and `diagnostics-otel` plugins. A `-browser` variant (e.g. `latest-browser`) also ships with Chromium baked in, useful for the [sandboxed browser](/gateway/sandboxing#sandboxed-browser) tool without a first-run Playwright install.
|
||||
Use `ghcr.io/openclaw/openclaw` or `openclaw/openclaw` and avoid unofficial mirrors, which don't share OpenClaw's release timing or retention policy. Immutable tags include exact versions such as `2026.2.26` and prereleases such as `2026.2.26-beta.1`. Regular stable releases move `latest` and `main`; trailing-month releases move `extended-stable` without changing the regular stable aliases. Matching slim and browser channels are `slim`, `main-slim`, `extended-stable-slim`, `latest-browser`, `main-browser`, and `extended-stable-browser`. The default images bundle the `codex` and `diagnostics-otel` plugins. A `-browser` variant also ships with Chromium baked in, useful for the [sandboxed browser](/gateway/sandboxing#sandboxed-browser) tool without a first-run Playwright install.
|
||||
|
||||
</Step>
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ OpenClaw currently exposes three user-facing update channels:
|
||||
- dev: the moving head of `main`
|
||||
|
||||
Separately, release operators can publish the trailing completed month's core
|
||||
package to npm `extended-stable`, beginning at patch `33`. The current-month
|
||||
regular final line continues on npm `latest`; this operator-side publication
|
||||
package to npm `extended-stable` and its official Docker images to dedicated
|
||||
extended-stable aliases, beginning at patch `33`. The current-month regular
|
||||
final line continues on npm `latest` and Docker `latest`/`main`; the publication
|
||||
split does not by itself change CLI update-channel resolution.
|
||||
|
||||
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).
|
||||
@@ -34,7 +35,7 @@ Tideclaw alpha builds are a separate internal prerelease track (npm dist-tag `al
|
||||
- `latest` continues to follow the current regular/daily npm line; `beta` is the current beta install target
|
||||
- `extended-stable` means the supported trailing-month npm package, beginning at patch `33`; patch `34` and later are maintenance releases on that monthly line
|
||||
- Regular final and regular correction releases publish to npm `beta` by default; release operators can target `latest` explicitly, or promote a vetted beta build later
|
||||
- The dedicated monthly extended-stable path publishes the core npm package and every npm-publishable official plugin at the same exact version. It does not publish plugins to ClawHub or publish macOS or Windows artifacts, a GitHub Release, private-repository dist-tags, Docker images, mobile artifacts, or website downloads.
|
||||
- The dedicated monthly extended-stable path publishes the core npm package and every npm-publishable official plugin at the same exact version. Its release tag also publishes exact Docker images to GHCR and Docker Hub, then advances only `extended-stable`, `extended-stable-slim`, and `extended-stable-browser`. It does not publish plugins to ClawHub or publish macOS or Windows artifacts, a GitHub Release, private-repository dist-tags, mobile artifacts, or website downloads.
|
||||
- Every regular final release ships the npm package, macOS app, signed standalone Android APK, and signed Windows Hub installers together. Beta releases normally validate and publish the npm/package path first, with native app build/sign/notarize/promote reserved for regular final unless explicitly requested.
|
||||
|
||||
## Release cadence
|
||||
@@ -44,7 +45,7 @@ Tideclaw alpha builds are a separate internal prerelease track (npm dist-tag `al
|
||||
- If a beta tag has been pushed or published and needs a fix, maintainers cut the next `-beta.N` tag instead of deleting or recreating the old one
|
||||
- Detailed release procedure, approvals, credentials, and recovery notes are maintainer-only
|
||||
|
||||
## Monthly npm-only extended-stable publication
|
||||
## Monthly extended-stable publication
|
||||
|
||||
This is a dedicated exception to the regular release procedure below. For a
|
||||
completed month `YYYY.M`, create `extended-stable/YYYY.M.33`; publish
|
||||
@@ -91,6 +92,12 @@ and push immutable `vYYYY.M.P` at that SHA. A post-tag source change requires a
|
||||
new patch version and new candidate; final extended-stable tags are never moved
|
||||
or deleted.
|
||||
|
||||
Pushing the tag starts `Docker Release`. For a final patch `33` or later, that
|
||||
workflow publishes immutable default, slim, browser, and architecture tags to
|
||||
GHCR and Docker Hub. It advances only the three extended-stable Docker aliases;
|
||||
it must not move `latest`, `main`, or their variants. Require the Docker run and
|
||||
its attestation verification to succeed before declaring the release complete.
|
||||
|
||||
After both runs succeed, publish every npm-publishable official plugin from the
|
||||
same exact branch tip. Patch `P` must be `33` or greater. Pass the full release
|
||||
SHA as `ref`, wait for the complete matrix and registry readback, then save the
|
||||
@@ -162,7 +169,7 @@ same exact-version publication path.
|
||||
|
||||
The regular checklist below continues to own beta, `latest`, GitHub Release,
|
||||
plugins, macOS, Windows, and other platform publication. Do not run those
|
||||
steps for this npm-only extended-stable path.
|
||||
steps for this extended-stable path.
|
||||
|
||||
## Regular release operator checklist
|
||||
|
||||
@@ -472,7 +479,7 @@ 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+` npm-only extended-stable path does not use this orchestrator. The
|
||||
`.33+` extended-stable path does not use this orchestrator. The
|
||||
regular workflow orchestrates the trusted-publisher workflows in the order the
|
||||
release needs:
|
||||
|
||||
@@ -631,7 +638,7 @@ Rules:
|
||||
|
||||
## Regular beta/latest stable release sequence
|
||||
|
||||
This legacy sequence is for the regular orchestrated release that also owns plugins, GitHub Release, Windows, and other platform work. It is not the monthly `.33+` npm-only extended-stable path documented at the top of this page.
|
||||
This legacy sequence is for the regular orchestrated release that also owns plugins, GitHub Release, Windows, and other platform work. It is not the monthly `.33+` extended-stable path documented at the top of this page.
|
||||
|
||||
When cutting a regular orchestrated stable release:
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parseReleaseVersion } from "./npm-publish-plan.mjs";
|
||||
|
||||
const STABLE_ALIASES = Object.freeze({
|
||||
default: Object.freeze(["latest", "main"]),
|
||||
slim: Object.freeze(["slim", "main-slim"]),
|
||||
browser: Object.freeze(["latest-browser", "main-browser"]),
|
||||
});
|
||||
|
||||
const EXTENDED_STABLE_ALIASES = Object.freeze({
|
||||
default: Object.freeze(["extended-stable"]),
|
||||
slim: Object.freeze(["extended-stable-slim"]),
|
||||
browser: Object.freeze(["extended-stable-browser"]),
|
||||
});
|
||||
|
||||
const NO_MOVING_ALIASES = Object.freeze({
|
||||
default: Object.freeze([]),
|
||||
slim: Object.freeze([]),
|
||||
browser: Object.freeze([]),
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {object} DockerReleasePolicy
|
||||
* @property {string} version
|
||||
* @property {"stable" | "extended-stable" | "beta"} channel
|
||||
* @property {{default: readonly string[], slim: readonly string[], browser: readonly string[]}} movingAliases
|
||||
*/
|
||||
|
||||
/**
|
||||
* Keep Docker's moving channels aligned with the release-version contract.
|
||||
* Patch 33+ finals belong to the trailing-month extended-stable line; they
|
||||
* must never move the regular latest/main aliases.
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {DockerReleasePolicy}
|
||||
*/
|
||||
export function resolveDockerReleasePolicy(version) {
|
||||
const parsed = parseReleaseVersion(version);
|
||||
if (parsed === null) {
|
||||
throw new Error(`Unsupported Docker release version "${version}".`);
|
||||
}
|
||||
if (parsed.channel === "alpha") {
|
||||
throw new Error("Docker alpha image publishing is disabled.");
|
||||
}
|
||||
if (parsed.channel === "beta") {
|
||||
return { version: parsed.version, channel: "beta", movingAliases: NO_MOVING_ALIASES };
|
||||
}
|
||||
if (parsed.patch >= 33) {
|
||||
if (parsed.correctionNumber !== undefined) {
|
||||
throw new Error(
|
||||
`Extended-stable Docker publication requires a final YYYY.M.PATCH version; found "${version}".`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
version: parsed.version,
|
||||
channel: "extended-stable",
|
||||
movingAliases: EXTENDED_STABLE_ALIASES,
|
||||
};
|
||||
}
|
||||
return { version: parsed.version, channel: "stable", movingAliases: STABLE_ALIASES };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const version = process.argv[2]?.trim();
|
||||
if (!version) {
|
||||
throw new Error("Usage: node scripts/lib/docker-release-policy.mjs <version>");
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(resolveDockerReleasePolicy(version))}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`docker-release-policy: ${message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
+23
-9
@@ -461,10 +461,11 @@ describe("Dockerfile", () => {
|
||||
expect(workflow).toContain("OPENCLAW_INSTALL_BROWSER=1");
|
||||
expect(workflow).toContain('${GHCR_IMAGE}:${version}-browser"');
|
||||
expect(workflow).toContain('${DOCKERHUB_IMAGE}:${version}-browser"');
|
||||
expect(workflow).toContain('${GHCR_IMAGE}:latest-browser"');
|
||||
expect(workflow).toContain('${DOCKERHUB_IMAGE}:latest-browser"');
|
||||
expect(workflow).toContain('${GHCR_IMAGE}:main-browser"');
|
||||
expect(workflow).toContain('${DOCKERHUB_IMAGE}:main-browser"');
|
||||
expect(workflow).toContain(
|
||||
"BROWSER_ALIASES: ${{ needs.resolve_release_policy.outputs.browser_aliases }}",
|
||||
);
|
||||
expect(workflow).toContain('browser_tags+=("${GHCR_IMAGE}:${alias}")');
|
||||
expect(workflow).toContain('dockerhub_browser_tags+=("${DOCKERHUB_IMAGE}:${alias}")');
|
||||
expect(workflow).not.toContain("main-browser-amd64");
|
||||
expect(workflow).not.toContain("main-browser-arm64");
|
||||
expect(workflow).toContain("Smoke test amd64 browser image");
|
||||
@@ -493,17 +494,30 @@ describe("Dockerfile", () => {
|
||||
expect(workflow).toContain("DOCKERHUB_MULTI_REFS: ${{ steps.refs.outputs.dockerhub_multi }}");
|
||||
});
|
||||
|
||||
it("publishes beta Docker tags without advancing latest aliases", async () => {
|
||||
it("keeps moving Docker aliases behind the release-version policy", async () => {
|
||||
const workflow = await readFile(dockerReleaseWorkflowPath, "utf8");
|
||||
|
||||
expect(workflow).toContain("Existing stable or beta release tag to backfill");
|
||||
expect(workflow).toContain("Existing stable, extended-stable, or beta release tag");
|
||||
expect(workflow).toContain('! "${RELEASE_TAG}" =~ ^v[0-9]{4}');
|
||||
expect(workflow).toContain("(-beta\\.[1-9][0-9]*)?");
|
||||
expect(workflow).toContain("(-(beta\\.)?[1-9][0-9]*)?");
|
||||
expect(workflow).toContain("${DOCKERHUB_IMAGE}:${version}");
|
||||
expect(workflow).toContain("${DOCKERHUB_IMAGE}:${version}-slim");
|
||||
expect(workflow).toContain("${DOCKERHUB_IMAGE}:${version}-browser");
|
||||
expect(workflow.split("do not advance latest/main aliases from those flows")).toHaveLength(3);
|
||||
expect(workflow.split('"$version" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9]+)?$')).toHaveLength(3);
|
||||
expect(workflow).toContain("node workflow-source/scripts/lib/docker-release-policy.mjs");
|
||||
expect(workflow.split("needs.resolve_release_policy.outputs.default_aliases")).toHaveLength(4);
|
||||
expect(workflow.split("needs.resolve_release_policy.outputs.slim_aliases")).toHaveLength(4);
|
||||
expect(workflow.split("needs.resolve_release_policy.outputs.browser_aliases")).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("promotes existing immutable Docker images without rebuilding them", async () => {
|
||||
const workflow = await readFile(dockerReleaseWorkflowPath, "utf8");
|
||||
|
||||
expect(workflow).toContain("promote-channel-aliases:");
|
||||
expect(workflow).toContain("inputs.operation == 'promote-channel'");
|
||||
expect(workflow).toContain("docker buildx imagetools create --prefer-index=false");
|
||||
expect(workflow).toContain('"${image}@${source_digest}"');
|
||||
expect(workflow).toContain('if [[ "${target_digest}" != "${source_digest}" ]]; then');
|
||||
expect(workflow).toContain('require_group_source "${image}" "-slim" "${SLIM_ALIASES}"');
|
||||
});
|
||||
|
||||
it("smokes runtime workspace templates before Docker release manifests publish", async () => {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveDockerReleasePolicy } from "../../scripts/lib/docker-release-policy.mjs";
|
||||
|
||||
describe("Docker release policy", () => {
|
||||
it("advances regular stable aliases only for final and correction patches below 33", () => {
|
||||
for (const version of ["2026.7.1", "2026.7.1-2"]) {
|
||||
expect(resolveDockerReleasePolicy(version)).toEqual({
|
||||
version,
|
||||
channel: "stable",
|
||||
movingAliases: {
|
||||
default: ["latest", "main"],
|
||||
slim: ["slim", "main-slim"],
|
||||
browser: ["latest-browser", "main-browser"],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps extended-stable releases on dedicated moving aliases", () => {
|
||||
for (const version of ["2026.6.33", "2026.6.34", "2026.6.99"]) {
|
||||
expect(resolveDockerReleasePolicy(version)).toEqual({
|
||||
version,
|
||||
channel: "extended-stable",
|
||||
movingAliases: {
|
||||
default: ["extended-stable"],
|
||||
slim: ["extended-stable-slim"],
|
||||
browser: ["extended-stable-browser"],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("publishes beta versions without moving a channel alias", () => {
|
||||
expect(resolveDockerReleasePolicy("2026.7.2-beta.3")).toEqual({
|
||||
version: "2026.7.2-beta.3",
|
||||
channel: "beta",
|
||||
movingAliases: { default: [], slim: [], browser: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["2026.6.33-1", "2026.6.33-alpha.1", "2026.0.33", "not-a-version"])(
|
||||
"rejects unsupported release version %s",
|
||||
(version) => {
|
||||
expect(() => resolveDockerReleasePolicy(version)).toThrow();
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user