mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
ci: Publish a weekly release candidate from the 3.x branch (#35741)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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-<date>.1`, `.2`, … — and moves `v3-rc` and
|
||||
`v3-rc-<date>` onto it, so:
|
||||
|
||||
- **`v3-rc-<date>.N`** — pin this to hold a build still. Immutable.
|
||||
- **`v3-rc` / `v3-rc-<date>`** — 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[-<date>.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.
|
||||
|
||||
|
||||
@@ -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[-<date>]` 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-<date>.N` as its immutable tag and moves the floating `v3-rc`
|
||||
and `v3-rc-<date>` 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.
|
||||
|
||||
|
||||
@@ -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-<date>, 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-<date>.1, .2, …) as its immutable reference, and also moves
|
||||
# the floating v3-rc and v3-rc-<date> 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-<date> 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"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user