From da1d05726891ccdc53e7eb4e242e21394e5b22f0 Mon Sep 17 00:00:00 2001 From: Matsu Date: Tue, 11 Aug 2026 09:36:28 +0300 Subject: [PATCH] ci: Publish a weekly release candidate from the 3.x branch (#35741) Co-authored-by: Claude Fable 5 --- .github/DEVELOPING_V3.md | 16 +++ .github/WORKFLOWS.md | 8 +- .github/workflows/build-v3-nightly.yml | 155 +++++++++++++++++++++++- .github/workflows/docker-build-push.yml | 22 ++++ 4 files changed, 199 insertions(+), 2 deletions(-) diff --git a/.github/DEVELOPING_V3.md b/.github/DEVELOPING_V3.md index 7ac7f289cda..34a72190329 100644 --- a/.github/DEVELOPING_V3.md +++ b/.github/DEVELOPING_V3.md @@ -206,8 +206,24 @@ Those authors are **requested as reviewers** on the conflict PR and listed in th ```bash docker pull n8nio/n8n:v3-nightly # latest v3 nightly docker pull n8nio/n8n:v3-nightly-20260625 # a specific build date +docker pull n8nio/n8n:v3-rc # latest release candidate +docker pull n8nio/n8n:v3-rc-20260625 # latest RC of that day +docker pull n8nio/n8n:v3-rc-20260625.2 # one exact RC, never overwritten ``` +Every Monday's nightly is also retagged as a release candidate, and a maintainer can +publish extra RCs any day (`force_rc` on a manual run). Each publish claims the next +rolling number for the day — `v3-rc-.1`, `.2`, … — and moves `v3-rc` and +`v3-rc-` onto it, so: + +- **`v3-rc-.N`** — pin this to hold a build still. Immutable. +- **`v3-rc` / `v3-rc-`** — track the newest RC overall / of that day. These move. + +The retag covers the whole set — `n8nio/n8n`, `n8nio/runners` and +`n8nio/runners:v3-rc[-.N]-distroless` — so pinning one RC across a stack gives +images built from one `3.x` commit, unlike `v3-nightly`, which moves daily and can be +mid-build when you pull. The same tags exist on GHCR (`ghcr.io/n8n-io/…`). + Use these to trial v3 in docker/kubernetes before release. Do **not** use them in production. diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md index 54c26bbcc37..a46fb3b4b84 100644 --- a/.github/WORKFLOWS.md +++ b/.github/WORKFLOWS.md @@ -434,7 +434,13 @@ mechanical files pre-resolved) is opened on `sync/master-to-3x`, requesting the breaking-commit authors as reviewers via `sync-conflict-owners.mjs`, posting to `#alerts-v3-sync` and pausing further syncs until it is resolved and merged normally. `build-v3-nightly.yml` publishes `n8nio/n8n:v3-nightly[-]` images from `3.x` -by calling `docker-build-push.yml` with `ref: 3.x` + `date_tag`. +by calling `docker-build-push.yml` with `ref: 3.x` + `date_tag`. On Mondays it also +retags that run's n8n + runners manifests as a release candidate (by digest on GHCR, so +the RC is exactly what was built), giving a self-consistent set to trial. Any manual run +can promote too via the `force_rc` dispatch input, several times a day: each publish +claims the next free `v3-rc-.N` as its immutable tag and moves the floating `v3-rc` +and `v3-rc-` onto it. The counter is derived by probing the registry, and the job +is serialized on a `v3-rc-tagging` concurrency group so two runs can't claim one number. See **[`DEVELOPING_V3.md`](./DEVELOPING_V3.md)** for the full model. diff --git a/.github/workflows/build-v3-nightly.yml b/.github/workflows/build-v3-nightly.yml index 9889579707a..6d548e006d0 100644 --- a/.github/workflows/build-v3-nightly.yml +++ b/.github/workflows/build-v3-nightly.yml @@ -5,6 +5,9 @@ # before release. Reuses the shared docker-build-push.yml pipeline, pointing it # at the 3.x branch via the `ref` input. # +# On Mondays the same manifests are additionally retagged v3-rc / v3-rc-, so +# there is a weekly, self-consistent set to run a stack against. +# # Lives on master so the schedule fires (scheduled runs only trigger on the # default branch), but builds the 3.x branch's code. @@ -15,6 +18,12 @@ on: # 08:00 UTC — after the master→3.x sync (06:00) so the image reflects the latest sync - cron: '0 8 * * *' workflow_dispatch: + inputs: + force_rc: + description: 'Also tag this build as a release candidate (normally Mondays only)' + required: false + type: boolean + default: false jobs: prepare: @@ -24,10 +33,14 @@ jobs: permissions: {} outputs: date_tag: ${{ steps.date.outputs.date }} + day_of_week: ${{ steps.date.outputs.day_of_week }} steps: - name: Compute date tag id: date - run: echo "date=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" + # Both values read the same UTC clock, so they can't disagree across a day boundary. + run: | + echo "date=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" + echo "day_of_week=$(date -u +%u)" >> "$GITHUB_OUTPUT" build: name: Build and push v3-nightly images @@ -44,3 +57,143 @@ jobs: date_tag: ${{ needs.prepare.outputs.date_tag }} push_enabled: true secrets: inherit + + # On Mondays (or on demand via force_rc), promote the whole set (n8n + both runners + # images) to v3-rc so a stack pulling `v3-rc` gets images that were built from the same + # 3.x commit. Pure retag of manifests the build already pushed — nothing is rebuilt. + # + # RCs can be published several times a day: each publish claims the next free rolling + # number for today (v3-rc-.1, .2, …) as its immutable reference, and also moves + # the floating v3-rc and v3-rc- tags onto it. + tag_release_candidate: + name: Tag release candidate + needs: [prepare, build] + if: ${{ needs.prepare.outputs.day_of_week == '1' || inputs.force_rc }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # Serialize promotion only (not the build) so two runs can't claim the same number. + concurrency: + group: v3-rc-tagging + cancel-in-progress: false + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Login to Docker registries + uses: ./.github/actions/docker-registry-login + with: + login-ghcr: true + login-dockerhub: true + dockerhub-username: ${{ secrets.DOCKER_USERNAME }} + dockerhub-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Claim rolling RC number for today + id: rc + env: + RC_VERSION: v3-rc + DATE_TAG: ${{ needs.prepare.outputs.date_tag }} + N8N_IMAGE: ${{ needs.build.outputs.n8n_image }} + MAX_PER_DAY: '50' + run: | + set -euo pipefail + + # The n8n GHCR repo is the single source of truth for the counter; all images + # then share the number, so one RC is always one consistent set. + tag_exists() { + local ref=$1 out + if out=$(docker buildx imagetools inspect "$ref" --raw 2>&1); then + return 0 + fi + # Only a genuine absence frees the number. Auth/network/rate-limit failures + # must not be read as "free" — that would overwrite an immutable tag. + if grep -qiE 'not found|manifest unknown|no such manifest|MANIFEST_UNKNOWN|404' <<<"$out"; then + return 1 + fi + echo "::error::Cannot determine whether ${ref} exists: ${out}" + exit 1 + } + + RC_NUMBER='' + for n in $(seq 1 "$MAX_PER_DAY"); do + if ! tag_exists "${N8N_IMAGE}:${RC_VERSION}-${DATE_TAG}.${n}"; then + RC_NUMBER=$n + break + fi + echo "${RC_VERSION}-${DATE_TAG}.${n} already published" + done + + if [[ -z "$RC_NUMBER" ]]; then + echo "::error::Already published ${MAX_PER_DAY} release candidates for ${DATE_TAG}" + exit 1 + fi + + echo "Claiming ${RC_VERSION}-${DATE_TAG}.${RC_NUMBER}" + echo "number=${RC_NUMBER}" >> "$GITHUB_OUTPUT" + + - name: Promote nightly manifests to release candidate + env: + NIGHTLY_VERSION: v3-nightly + RC_VERSION: v3-rc + DATE_TAG: ${{ needs.prepare.outputs.date_tag }} + RC_NUMBER: ${{ steps.rc.outputs.number }} + DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }} + N8N_IMAGE: ${{ needs.build.outputs.n8n_image }} + N8N_DIGEST: ${{ needs.build.outputs.n8n_digest }} + RUNNERS_IMAGE: ${{ needs.build.outputs.runners_image }} + RUNNERS_DIGEST: ${{ needs.build.outputs.runners_digest }} + DISTROLESS_IMAGE: ${{ needs.build.outputs.runners_distroless_image }} + DISTROLESS_DIGEST: ${{ needs.build.outputs.runners_distroless_digest }} + run: | + set -euo pipefail + + # Each image gets three tags: the numbered one is the immutable reference for this + # publish; v3-rc and v3-rc- float onto the newest RC overall / of the day. + RC_ROLLING="${RC_VERSION}-${DATE_TAG}.${RC_NUMBER}" + + # Retag one image as RC on both registries. GHCR is sourced by digest (immutable, + # can't drift); Docker Hub is sourced by its dated tag because `imagetools create` + # only retags within a repository — it can't pull GHCR blobs into Docker Hub. + promote() { + local ghcr_repo=$1 digest=$2 docker_repo=$3 suffix=$4 + + if [[ -z "$ghcr_repo" || -z "$digest" ]]; then + echo "::error::Missing image/digest for ${docker_repo}${suffix} - nightly manifest not published" + return 1 + fi + + echo "Promoting ${ghcr_repo}@${digest} -> ${RC_ROLLING}${suffix}" + docker buildx imagetools create \ + --tag "${ghcr_repo}:${RC_ROLLING}${suffix}" \ + --tag "${ghcr_repo}:${RC_VERSION}${suffix}" \ + --tag "${ghcr_repo}:${RC_VERSION}-${DATE_TAG}${suffix}" \ + "${ghcr_repo}@${digest}" + + docker buildx imagetools create \ + --tag "${docker_repo}:${RC_ROLLING}${suffix}" \ + --tag "${docker_repo}:${RC_VERSION}${suffix}" \ + --tag "${docker_repo}:${RC_VERSION}-${DATE_TAG}${suffix}" \ + "${docker_repo}:${NIGHTLY_VERSION}-${DATE_TAG}${suffix}" + + { + echo "- \`${ghcr_repo}:${RC_ROLLING}${suffix}\`" + echo "- \`${docker_repo}:${RC_ROLLING}${suffix}\`" + } >> "$GITHUB_STEP_SUMMARY" + } + + { + echo "## v3 release candidate \`${RC_ROLLING}\`" + echo "" + echo "Immutable references (\`${RC_VERSION}\` and \`${RC_VERSION}-${DATE_TAG}\` now point here too):" + } >> "$GITHUB_STEP_SUMMARY" + + promote "$N8N_IMAGE" "$N8N_DIGEST" "${DOCKER_BASE}/n8n" "" + promote "$RUNNERS_IMAGE" "$RUNNERS_DIGEST" "${DOCKER_BASE}/runners" "" + promote "$DISTROLESS_IMAGE" "$DISTROLESS_DIGEST" "${DOCKER_BASE}/runners" "-distroless" + diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index c54e8c58496..7e893f8dfc2 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -40,6 +40,28 @@ on: required: false type: string default: '' + outputs: + # Repo name + multi-arch manifest digest per image, so callers can promote what + # was actually pushed (retag by digest) instead of re-deriving tag strings. + # Empty when push_enabled is false — the manifest job doesn't run. + n8n_image: + description: 'GHCR repo for the n8n image (no tag)' + value: ${{ jobs.create_multi_arch_manifest.outputs.n8n_image }} + n8n_digest: + description: 'Multi-arch manifest digest of the n8n image' + value: ${{ jobs.create_multi_arch_manifest.outputs.n8n_digest }} + runners_image: + description: 'GHCR repo for the runners image (no tag)' + value: ${{ jobs.create_multi_arch_manifest.outputs.runners_image }} + runners_digest: + description: 'Multi-arch manifest digest of the runners image' + value: ${{ jobs.create_multi_arch_manifest.outputs.runners_digest }} + runners_distroless_image: + description: 'GHCR repo for the distroless runners image (no tag; same repo as runners)' + value: ${{ jobs.create_multi_arch_manifest.outputs.runners_distroless_image }} + runners_distroless_digest: + description: 'Multi-arch manifest digest of the distroless runners image' + value: ${{ jobs.create_multi_arch_manifest.outputs.runners_distroless_digest }} workflow_dispatch: inputs: