Skip to content

test(core): Add a repeatable upgrade-cycle test for the encryption key rollout (no-changelog) - #37885

Draft
sychovSaveliy wants to merge 1 commit into
masterfrom
iam-1292-build-a-repeatable-upgrade-test-for-the-rollout
Draft

test(core): Add a repeatable upgrade-cycle test for the encryption key rollout (no-changelog)#37885
sychovSaveliy wants to merge 1 commit into
masterfrom
iam-1292-build-a-repeatable-upgrade-test-for-the-rollout

Conversation

@sychovSaveliy

@sychovSaveliy sychovSaveliy commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

A one-command, repeatable upgrade test for the encryption-key rollout (v1). It proves the three guarantees every release of the rollout must keep:

  1. Upgrading keeps existing data readable — data encrypted by an old release decrypts on this checkout.
  2. Rollback stays safe while the rotation flag is off — a value written by the newer instance decrypts on the older release (writes stay byte-compatible legacy format).
  3. Flipping the flag switches new writes to the new format without breaking anything — new writes become <keyId>:<ciphertext>, rotation moves the write key immediately, and all data generations keep decrypting side by side.
pnpm test:upgrade

What it runs

One sqlite data folder travels through four phases (FROM_IMAGE defaults to n8nio/n8n:latest, the "new" version is this built checkout):

Phase Instance Checks
P1 seed old release (docker) owner + credential created; decrypt round-trip
P2 upgrade this checkout, flag off old credential decrypts; new write is legacy format (U2FsdGVkX1…); key store seeded with exactly 2 deployment_key rows
P3 downgrade old release again, same folder credential written by the newer instance decrypts on the older one
P4 write-on this checkout, flag on mixed data decrypts; new write is <activeKeyId>:…; POST /rest/encryption/keys rotates and the very next write uses the new key id; all four generations decrypt

Decryption is proven through the REST API: the unique seeded secret lives in a non-password credential field, so GET /rest/credentials/:id?includeData=true returning it verbatim means the stored blob decrypted (the password-typed field coming back redacted is further evidence — redaction only runs on successfully decrypted data).

Every step is asserted against the raw credentials_entity.data column as well, so the stored format is checked, not just the round-trip.

Metrics collected

Each run prints per-phase duration, the number of decrypt round-trips, and their average/max end-to-end latency (raw samples land in <work root>/metrics.csv) — the seed for the decrypt-regression go/no-go metric from the rollout plan:

=== metrics ===
phase           duration  decrypt checks   avg ms   max ms
P1 seed              10s               1     13.5     13.5
P2 upgrade            7s               2      9.1     10.4
P3 downgrade          5s               2     14.4     15.2
P4 write-on           5s               7      7.6     11.2
total                27s              12      9.5     15.2

Console output from a full run (FROM = 2.37.10, TO = this branch stack)

[17:16:27] ================ P1 seed ================
[17:16:36] [P1 seed] FROM version: 2.37.10
[17:16:36] [P1 seed]   OK: credential A = 5G473j2Ue0cUTCae
[17:16:36] [P1 seed]   OK: credential 5G473j2Ue0cUTCae decrypts to the seeded secret (0.013536s)
[17:16:37] ================ P2 upgrade ================
[17:16:43] [P2 upgrade]   OK: credential 5G473j2Ue0cUTCae decrypts to the seeded secret (0.010411s)
[17:16:43] [P2 upgrade]   OK: credential B = xnzpKLLPj11SNjDW
[17:16:43] [P2 upgrade]   OK: credential B stored in legacy format (U2FsdGVkX1...)
[17:16:43] [P2 upgrade]   OK: credential xnzpKLLPj11SNjDW decrypts to the seeded secret (0.007824s)
[17:16:44] ================ P3 downgrade ================
[17:16:49] [P3 downgrade]   OK: credential xnzpKLLPj11SNjDW decrypts to the seeded secret (0.015205s)
[17:16:49] [P3 downgrade]   OK: credential 5G473j2Ue0cUTCae decrypts to the seeded secret (0.013639s)
[17:16:49] ================ P4 write-on ================
[17:16:53] [P4 write-on]   OK: credential 5G473j2Ue0cUTCae decrypts to the seeded secret (0.011191s)
[17:16:53] [P4 write-on]   OK: credential xnzpKLLPj11SNjDW decrypts to the seeded secret (0.009804s)
[17:16:53] [P4 write-on]   OK: credential C prefixed with the active key id (VTBMCs5mmNCCYwUq)
[17:16:54] [P4 write-on]   OK: rotated: VTBMCs5mmNCCYwUq -> QZGhQpmLinbo2efl
[17:16:54] [P4 write-on]   OK: credential D prefixed with the rotated key id (QZGhQpmLinbo2efl)
[17:16:54] [P4 write-on]   OK: credential 5G473j2Ue0cUTCae decrypts to the seeded secret (0.006458s)
[17:16:54] [P4 write-on]   OK: credential xnzpKLLPj11SNjDW decrypts to the seeded secret (0.005583s)
[17:16:54] [P4 write-on]   OK: credential Lda1AQiT9qSqn476 decrypts to the seeded secret (0.006573s)
[17:16:54] [P4 write-on]   OK: credential SVARGq1kw4bpEOH7 decrypts to the seeded secret (0.006676s)
[17:16:55] PASS: upgrade-cycle — seed(2.37.10) -> upgrade(read) -> downgrade-read -> write-on+rotate, all decrypts OK

A nice side-finding from P3: the old release boots cleanly on a database already migrated by this checkout (TypeORM ignores applied migrations it does not know), so the downgrade check runs against the real old binary rather than an offline decrypt.

Design notes

  • Deliberately v1-simple: plain bash, sqlite, credentials only. It reuses the FROM image straight from the registry and the local build as TO, so it needs no docker image build and no license. Follow-ups planned as the rollout progresses: Postgres parameter, queue-mode (flag must flip fleet-wide), variables/SSO seed data, and a nightly CI workflow using the PR-built docker image as TO.
  • Repeatable and clean: everything lives in a mktemp folder, the container is removed by a trap, unique ports; failure prints the failing check plus the instance log tail.

How to test

pnpm build   # once
pnpm test:upgrade

Optional knobs: FROM_IMAGE=n8nio/n8n:2.35.0 pnpm test:upgrade to start from an older release; N8N_PORT, WORK_ROOT as needed. Needs docker, sqlite3, python3 (exit 77 = skipped when docker is down).

Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/IAM-1292

Review / Merge checklist

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • PR title and summary are descriptive.
  • Docs updated or follow-up ticket created.
  • Tests included.

🤖 Generated with Claude Code

Review in cubic

…y rollout (no-changelog)

One command (pnpm test:upgrade) drives a sqlite data folder through
seed-on-old-release, upgrade with the rotation flag off, downgrade-read
on the old release, and write-on with rotation enabled, asserting the
stored format and decrypt round-trips at every step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@n8n-assistant

n8n-assistant Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR review overview

Based on ownership of the 3 changed files in this PR:

Ownership Files owned Share Source code Test files Misc
@n8n-io/qa-dx 2 67% +350 / -0 +0 / -0 +44 / -0
@n8n-io/catalysts 1 33% +1 / -0 +0 / -0 +0 / -0
Total 3 100% +351 / -0 +0 / -0 +44 / -0

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files

Confidence score: 3/5

  • scripts/upgrade-test/upgrade-cycle.sh uses a changing latest source binary by default, so repeatable upgrade results can vary over time; pin the default FROM image to a known release.
  • package.json only exposes pnpm test:upgrade locally, leaving the encryption-key rollout acceptance gate out of continuous integration; wire the suite into CI so every release is covered.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/upgrade-test/upgrade-cycle.sh">

<violation number="1" location="scripts/upgrade-test/upgrade-cycle.sh:24">
P2: According to linked Linear issue IAM-1292, this is the standing repeatable upgrade suite, but the default source binary is not repeatable because `latest` changes over time. Pin the default FROM image to a known release digest or tag, and update that baseline deliberately when the compatibility target changes.</violation>
</file>

<file name="package.json">

<violation number="1" location="package.json:81">
P2: Per linked Linear issue IAM-1292, this test suite must "run in continuous integration" — it is the acceptance gate for every release of the encryption-key rollout. This PR only wires up a local `pnpm test:upgrade` command; no CI workflow calls the script, so regressions will only be caught when a developer runs it manually. Add a workflow (for example a nightly job using the PR-built image as TO) to satisfy the AC rather than deferring it.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Fix all with cubic | Re-trigger cubic

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
N8N_REPO="${N8N_REPO:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
N8N_PORT="${N8N_PORT:-5714}"
FROM_IMAGE="${FROM_IMAGE:-n8nio/n8n:latest}"

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: According to linked Linear issue IAM-1292, this is the standing repeatable upgrade suite, but the default source binary is not repeatable because latest changes over time. Pin the default FROM image to a known release digest or tag, and update that baseline deliberately when the compatibility target changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/upgrade-test/upgrade-cycle.sh, line 24:

<comment>According to linked Linear issue IAM-1292, this is the standing repeatable upgrade suite, but the default source binary is not repeatable because `latest` changes over time. Pin the default FROM image to a known release digest or tag, and update that baseline deliberately when the compatibility target changes.</comment>

<file context>
@@ -0,0 +1,350 @@
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+N8N_REPO="${N8N_REPO:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
+N8N_PORT="${N8N_PORT:-5714}"
+FROM_IMAGE="${FROM_IMAGE:-n8nio/n8n:latest}"
+WORK_ROOT="${WORK_ROOT:-$(mktemp -d "${TMPDIR:-/tmp}/${SPEC_NAME}.XXXXXX")}"
+
</file context>
Fix with cubic

Comment thread package.json
"test:ci:backend:integration": "turbo run test:integration --continue --concurrency=1 --filter='!./packages/frontend/**' --filter='!./packages/modules/**'",
"test:ci:backend:integration:changed": "turbo run test:integration:changed --continue --concurrency=1 --filter='!./packages/frontend/**' --filter='!./packages/modules/**'",
"test:affected": "turbo run test --affected --concurrency=1",
"test:upgrade": "./scripts/upgrade-test/upgrade-cycle.sh",

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Per linked Linear issue IAM-1292, this test suite must "run in continuous integration" — it is the acceptance gate for every release of the encryption-key rollout. This PR only wires up a local pnpm test:upgrade command; no CI workflow calls the script, so regressions will only be caught when a developer runs it manually. Add a workflow (for example a nightly job using the PR-built image as TO) to satisfy the AC rather than deferring it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At package.json, line 81:

<comment>Per linked Linear issue IAM-1292, this test suite must "run in continuous integration" — it is the acceptance gate for every release of the encryption-key rollout. This PR only wires up a local `pnpm test:upgrade` command; no CI workflow calls the script, so regressions will only be caught when a developer runs it manually. Add a workflow (for example a nightly job using the PR-built image as TO) to satisfy the AC rather than deferring it.</comment>

<file context>
@@ -78,6 +78,7 @@
     "test:ci:backend:integration": "turbo run test:integration --continue --concurrency=1 --filter='!./packages/frontend/**' --filter='!./packages/modules/**'",
     "test:ci:backend:integration:changed": "turbo run test:integration:changed --continue --concurrency=1 --filter='!./packages/frontend/**' --filter='!./packages/modules/**'",
     "test:affected": "turbo run test --affected --concurrency=1",
+    "test:upgrade": "./scripts/upgrade-test/upgrade-cycle.sh",
     "db:schema:docs": "turbo run build --filter=@n8n/db && pnpm --filter=@n8n/db schema:docs",
     "db:schema:check": "turbo run build --filter=@n8n/db && pnpm --filter=@n8n/db schema:check",
</file context>
Fix with cubic

@n8n-assistant n8n-assistant Bot added the n8n team Authored by the n8n team label Sep 4, 2026
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant