Skip to content

test(e2e): add chainsaw-native release upgrade testing lane - #3276

Open
myasnikovdaniil wants to merge 10 commits into
mainfrom
feat/upgrade-e2e-chainsaw
Open

test(e2e): add chainsaw-native release upgrade testing lane#3276
myasnikovdaniil wants to merge 10 commits into
mainfrom
feat/upgrade-e2e-chainsaw

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Adds automated release upgrade testing — the "previous latest minor stable release → current version" path — rebuilt for the Chainsaw-based E2E suite (#2826). It installs the previous Cozystack, seeds real workloads with canary data, upgrades the platform to the build under test, then verifies workloads survived, data is intact, every HelmRelease reconciled, PVs stayed Bound, and the migration stamp advanced.

Supersedes #2401, which predates the BATS→Chainsaw migration and is now a single conflicting 895-line BATS file. Its useful pieces (previous-version resolution, the isp-full platform Package, the canary SQL, the CrashLoop/unbound-PV baseline diff) are re-expressed here.

Architecture

Chainsaw cannot pause mid-test for an external helm upgrade, so the flow is two Chainsaw phases bracketing the upgrade, driven by make upgrade-cozystack inside the e2e sandbox:

install-previous (bats, previous OCI chart)
  → seed   (chainsaw, cleanup.skipDelete=true — workloads + canary survive the phase boundary)
    → helm upgrade to the in-tree chart (bats) + all-HR-Ready gate + migration-stamp assert
      → verify (chainsaw, assert-only — survival + data integrity + platform health)

DRY over existing files

  • 4 composite actions (.github/actions/e2e-{download-assets,prepare,collect,teardown}) for the sandbox lifecycle, shared by both the e2e job and the new upgrade-e2e job. The existing e2e job is refactored onto them (behavior-preserving).
  • hack/e2e-wait-hr-ready.sh — the all-HelmReleases-Ready gate, extracted from e2e-install-cozystack.bats so install and upgrade share one legible, fail-fast gate.
  • hack/upgrade-prev-version.sh (+ upgrade-prev-version_test.bats, 10 cases) — resolves the baseline version from git tags.
  • Seed suites apply the existing app manifests by relative path — no re-authored CRs.

Trigger

A new upgrade-e2e job in pull-requests.yaml, a sibling of e2e that runs in parallel (no added critical-path wall-clock). It runs on the release-promotion PR (release label) or any PR a maintainer opts in with the new upgrade-e2e label. Advisory / non-blocking — its own check, gates nothing.

Coverage

Always-on: PostgreSQL + MariaDB canary rows, Redis/VM volume survival, migration stamp advance, all-HRs-Ready + no-new-unbound-PV, aggregated API available. The tenant-Kubernetes survival suites are env-gated OFF (UPGRADE_E2E_TENANT_K8S) as the flakiest surface (nested KVM + DRBD + tenant-worker image import), pending validation on a dev cluster.

Status

Ready. actionlint, zizmor and the full unit suite pass, and hack/promote-gate-contract.bats now pins both halves of the upgrade-e2e label opt-in — the allow-list that admits the labeled event and the job's own condition — so the gate cannot drift from the test that guards it again. The canary access paths were fixed against a live dev cluster (f598c253); the tenant-Kubernetes verify suites remain env-gated off behind UPGRADE_E2E_TENANT_K8S as the flakiest surface, pending their own validation run.

The upgrade-e2e job runs only on a PR carrying the upgrade-e2e or release label. Its check is Upgrade E2E Test, which is not a required context — branch protection requires only pre-commit and E2E Tests — so the lane is advisory and gates nothing.

The lane is currently red, and that is the lane working: it found upgrade-only defects that no other suite reaches. Two are confirmed platform bugs with a terminal mechanism — the seaweedfs naming guard refusing a 1.5.x→1.6 tenant upgrade, and opensearch-operator's spec.strategy.rollingUpdate surviving into a type: Recreate deployment under server-side apply. A third, vm-instance timing out at InProgress, is a real signal whose root cause the log does not establish, so it is not in the same category. All three belong in their own PRs.

Costs this accepts, stated rather than left implicit: a second full platform install per run on a 32-vCPU runner with a 180-minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change.

Added automated release upgrade testing (previous minor stable → current) as a parallel, opt-in E2E lane: it installs the previous release, seeds workloads with canary data, upgrades the platform, and verifies workload survival, data integrity, HelmRelease reconciliation, and migration completion.

Summary by CodeRabbit

  • New Features
    • Added an “upgrade E2E” lane (CI label) with baseline install, seeded data, upgrade apply, and post-upgrade verification, including optional tenant Kubernetes coverage.
    • Introduced shared composite actions to stage E2E assets, prepare the sandbox with retries, always collect debug artifacts, and teardown consistently.
    • Added automatic selection of the previous stable baseline for upgrade runs.
    • Added an opt-in etcdAdoptSkipBackup migration flag to bypass the pre-adoption safety snapshot.
  • Bug Fixes
    • Improved HelmRelease readiness gates with clearer failure diagnostics.
  • Documentation
    • Documented the new upgrade lane phases and CI placement.

@github-actions github-actions Bot added area/testing Issues or PRs related to testing (e2e, bats, unit tests) size/XXL This PR changes 1000+ lines, ignoring generated files labels Jul 13, 2026
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds reusable GitHub Actions for E2E sandbox lifecycle management and introduces a release upgrade E2E lane covering baseline installation, resource seeding, Cozystack upgrade, post-upgrade verification, and diagnostics.

Changes

Release upgrade E2E

Layer / File(s) Summary
Shared sandbox actions and CI wiring
.github/actions/e2e-*, .github/workflows/pull-requests.yaml, .github/labels.yml
Centralizes asset downloads, sandbox preparation, diagnostics collection, teardown, and the upgrade-e2e workflow lane.
Upgrade orchestration and version gates
hack/e2e-upgrade-*.bats, hack/e2e-wait-hr-ready.sh, hack/upgrade-prev-version*, packages/core/testing/Makefile, packages/core/platform/{values.yaml,templates/migration-hook.yaml}
Adds previous-version selection, baseline installation, upgrade execution, readiness and migration gates, Make targets, etcd adoption configuration, and resolver tests.
Upgrade seed suite
hack/e2e-chainsaw-upgrade/.chainsaw.yaml, hack/e2e-chainsaw-upgrade/seed/*
Seeds MariaDB, PostgreSQL, Redis, tenant Kubernetes, and VM resources, and records baseline storage and cluster snapshots.
Upgrade verification suite and documentation
hack/e2e-chainsaw-upgrade/verify/*, docs/agents/e2e-testing.md
Verifies platform readiness, application data, PVC-to-PV bindings, VM state, tenant Kubernetes state, and documents the release-lane flow.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Workflow
  participant SandboxActions
  participant Sandbox
  participant Chainsaw
  participant Kubernetes
  Workflow->>SandboxActions: download assets and prepare sandbox
  SandboxActions->>Sandbox: run prepare-env
  Workflow->>Sandbox: run upgrade-cozystack
  Sandbox->>Chainsaw: execute seed and verify suites
  Chainsaw->>Kubernetes: apply resources and run checks
  Kubernetes-->>Chainsaw: readiness, data, and storage results
  Workflow->>SandboxActions: collect reports and teardown
Loading

Possibly related PRs

Suggested labels: area/uncategorized

Suggested reviewers: androndo, ivanhunters, kvaps, lexfrei

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: a Chainsaw-based release upgrade E2E testing lane.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/upgrade-e2e-chainsaw

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@myasnikovdaniil myasnikovdaniil added the upgrade-e2e Run the release upgrade E2E test (previous minor stable -> this build) on this PR label Jul 13, 2026
@myasnikovdaniil
myasnikovdaniil marked this pull request as ready for review July 13, 2026 11:38
@dosubot dosubot Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation kind/feature Categorizes issue or PR as related to a new feature labels Jul 13, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces an automated release upgrade testing lane to ensure platform stability during version transitions. By installing the previous stable release, seeding workloads with canary data, and verifying their integrity after an upgrade to the current build, the new suite provides critical validation for migration paths. The implementation uses a modular architecture with shared composite actions and reusable health checks, ensuring that the upgrade lane remains maintainable and efficient without impacting the existing E2E pipeline's critical path.

Highlights

  • Automated Release Upgrade Testing: Added a new E2E lane that validates the upgrade path from the previous stable minor release to the current build.
  • Chainsaw Integration: Leveraged Chainsaw for seeding canary data and verifying post-upgrade state, while using BATS for orchestration.
  • Shared Infrastructure: Refactored sandbox lifecycle management into four reusable composite GitHub Actions to improve maintainability.
  • New CI Job: Introduced an upgrade-e2e job that runs in parallel to the standard E2E suite, triggered by specific labels.
  • Robust Health Checks: Extracted the HelmRelease readiness gate into a shared script to ensure consistent and fail-fast validation across both install and upgrade paths.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/pull-requests.yaml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a release upgrade E2E testing lane to verify platform upgrades from the previous latest minor stable release to the current version. It adds composite GitHub Actions for sandbox lifecycle management, BATS orchestration scripts, and Kyverno Chainsaw suites for seeding and verifying workloads (MariaDB, PostgreSQL, Redis, VMs, and tenant Kubernetes clusters). The review feedback highlights a missing seeding file for PostgreSQL (upgrade-seed-postgres), recommends adding the --fail (-f) flag to curl when downloading assets to ensure proper error handling, suggests quoting the SANDBOX_NAME variable in shell commands to prevent word splitting, and proposes simplifying heavily escaped inline scripts in the upgrade BATS tests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

(conditions[?type == 'Ready']):
- status: "True"

- name: canary-intact

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.

high

It appears that the corresponding seeding file hack/e2e-chainsaw-upgrade/seed/postgres/chainsaw-test.yaml is missing from this pull request. Without a seeding phase to initialize the upgrade_canary table and insert the test rows, the upgrade-verify-postgres verification step will fail during the upgrade test run.

Comment on lines +50 to +52
curl -sSL -H "Authorization: token ${APP_TOKEN}" -H "Accept: application/octet-stream" \
-o _out/assets/nocloud-amd64.raw.xz \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${DISK_ID}"

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.

medium

The curl command is invoked with -sSL but without -f (or --fail). If the asset download fails (e.g., due to an expired token, invalid disk ID, or network issue), curl will exit with code 0 and write the error response (often HTML or JSON) to _out/assets/nocloud-amd64.raw.xz. This causes silent failures that are harder to debug in subsequent steps. Adding -f ensures curl fails fast with a non-zero exit code on HTTP errors.

        curl -fsSL -H "Authorization: token ${APP_TOKEN}" -H "Accept: application/octet-stream" \
          -o _out/assets/nocloud-amd64.raw.xz \
          "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${DISK_ID}"

run: |
cd "/tmp/$SANDBOX_NAME"
attempt=0
until make SANDBOX_NAME=$SANDBOX_NAME prepare-env; do

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.

low

The SANDBOX_NAME variable is unquoted when passed to make. While the sandbox name generated in this workflow is unlikely to contain spaces, it is a best practice to quote variable expansions in shell scripts to prevent word splitting and globbing issues.

        until make SANDBOX_NAME="$SANDBOX_NAME" prepare-env; do

fi

# The stamp is set by a Job, so poll briefly rather than reading once.
timeout 120 sh -ec "until [ \"\$(kubectl get configmap cozystack-version -n cozy-system -o jsonpath='{.data.version}' 2>/dev/null)\" = \"${expected}\" ]; do sleep 3; done" || {

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.

low

The inline script uses heavily escaped double quotes and variable expansions (\"\$(...) and ${expected}). This can be hard to read and maintain. You can simplify this by passing the expected version as an environment variable and using single quotes for the sh -ec script to avoid escaping. Additionally, the -o jsonpath argument does not require quotes if it contains no spaces.

  EXPECTED_VERSION="$expected" timeout 120 sh -ec 'until [ "$(kubectl get configmap cozystack-version -n cozy-system -o jsonpath={.data.version} 2>/dev/null)" = "$EXPECTED_VERSION" ]; do sleep 3; done' || {

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
hack/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yaml (1)

20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add scoped diagnostics for the tenant-cluster resources.

Unlike the sibling seed suites (mariadb/postgres/redis/vm), which each attach a describe + podLogs for their specific resource in catch:, this test only has events: {}. Given this is explicitly called out as the flakiest/heaviest suite (nested KVM + DRBD + tenant Talos image import), a failure here would benefit most from a describe of the KamajiControlPlane/TenantControlPlane/MachineDeployment on error.

♻️ Suggested catch block addition
   catch:
   - events: {}
+  - describe:
+      apiVersion: kamaji.clastix.io/v1alpha1
+      kind: KamajiControlPlane
+      name: kubernetes-upgrade-tk8s
+  - describe:
+      apiVersion: cluster.x-k8s.io/v1beta1
+      kind: MachineDeployment
+      name: kubernetes-upgrade-tk8s-md0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yaml` around lines 20
- 22, Update the catch block in the tenant-k8s Chainsaw test to retain the
existing events diagnostics and add scoped describe diagnostics for the
KamajiControlPlane, TenantControlPlane, and MachineDeployment resources,
matching the diagnostic structure used by the sibling seed suites.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/e2e-prepare/action.yaml:
- Around line 28-35: Update the sandbox-name step around the shell variable key
to receive sandbox-suffix through the step’s env configuration, then reference
that environment variable in the shell condition and key extension instead of
directly interpolating inputs.sandbox-suffix. Preserve the existing default
behavior when the suffix is empty and the current SANDBOX_NAME and sandbox-name
outputs.

In `@hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml`:
- Around line 41-55: Validate that the baseline files exist before running
either comm comparison in the PersistentVolume and CrashLoopBackOff checks. Add
fail-fast guards for $dir/unbound-pv.txt and $dir/crashloop.txt, exiting nonzero
with an appropriate error when either is missing, while preserving the existing
PV failure gate and warning-only CrashLoopBackOff behavior.

---

Nitpick comments:
In `@hack/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yaml`:
- Around line 20-22: Update the catch block in the tenant-k8s Chainsaw test to
retain the existing events diagnostics and add scoped describe diagnostics for
the KamajiControlPlane, TenantControlPlane, and MachineDeployment resources,
matching the diagnostic structure used by the sibling seed suites.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6c3df8d5-fd85-4ca9-8ad1-b3245fd9eee3

📥 Commits

Reviewing files that changed from the base of the PR and between eea8b73 and 6f987df.

📒 Files selected for processing (27)
  • .github/actions/e2e-collect/action.yaml
  • .github/actions/e2e-download-assets/action.yaml
  • .github/actions/e2e-prepare/action.yaml
  • .github/actions/e2e-teardown/action.yaml
  • .github/labels.yml
  • .github/workflows/pull-requests.yaml
  • docs/agents/e2e-testing.md
  • hack/e2e-chainsaw-upgrade/.chainsaw.yaml
  • hack/e2e-chainsaw-upgrade/seed/mariadb/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/seed/postgres/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/seed/redis/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/seed/vm/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/seed/zz-baseline/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/verify/mariadb/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/verify/postgres/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/verify/redis/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/verify/tenant-k8s/chainsaw-test.yaml
  • hack/e2e-chainsaw-upgrade/verify/vm/chainsaw-test.yaml
  • hack/e2e-install-cozystack.bats
  • hack/e2e-upgrade-apply.bats
  • hack/e2e-upgrade-install-previous.bats
  • hack/e2e-wait-hr-ready.sh
  • hack/upgrade-prev-version.sh
  • hack/upgrade-prev-version_test.bats
  • packages/core/testing/Makefile

Comment thread .github/actions/e2e-prepare/action.yaml Outdated
Comment on lines +41 to +55
dir=/workspace/_out/upgrade-baseline
echo "=== gate: no new unbound PersistentVolumes vs baseline ==="
kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt
new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true)
if [ -n "$new_pv" ]; then
echo "FAIL: PersistentVolumes newly unbound after upgrade:" >&2
echo "$new_pv" >&2
kubectl get pv 2>&1 | awk 'NR==1 || $5 != "Bound"' >&2 || true
exit 1
fi
echo "no new unbound PVs"

echo "=== warning-only: new CrashLoopBackOff pods vs baseline ==="
kubectl get pods -A --no-headers 2>/dev/null | awk '/CrashLoopBackOff/ {print $1"/"$2}' | sort > /tmp/post-crashloop.txt
new_cl=$(comm -13 "$dir/crashloop.txt" /tmp/post-crashloop.txt || true)

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add baseline file existence checks before comm diffs.

The fatal PV check and warning crashloop check both use comm -13 "$dir/..." without first verifying the baseline files exist. If the seed phase didn't run (CI orchestration failure), comm fails, || true catches it, new_pv is empty, and the fatal "no new unbound PVs" gate silently passes — a false negative on the authoritative health signal. The Redis and VM verify suites both guard with [ -f "$before" ] || exit 1; this suite should be consistent.

🛡️ Proposed fix: add baseline file guards
           dir=/workspace/_out/upgrade-baseline
+          [ -f "$dir/unbound-pv.txt" ] || { echo "missing baseline unbound-pv.txt (seed did not run?)" >&2; exit 1; }
+          [ -f "$dir/crashloop.txt" ] || { echo "missing baseline crashloop.txt (seed did not run?)" >&2; exit 1; }
           echo "=== gate: no new unbound PersistentVolumes vs baseline ==="
           kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt
           new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dir=/workspace/_out/upgrade-baseline
echo "=== gate: no new unbound PersistentVolumes vs baseline ==="
kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt
new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true)
if [ -n "$new_pv" ]; then
echo "FAIL: PersistentVolumes newly unbound after upgrade:" >&2
echo "$new_pv" >&2
kubectl get pv 2>&1 | awk 'NR==1 || $5 != "Bound"' >&2 || true
exit 1
fi
echo "no new unbound PVs"
echo "=== warning-only: new CrashLoopBackOff pods vs baseline ==="
kubectl get pods -A --no-headers 2>/dev/null | awk '/CrashLoopBackOff/ {print $1"/"$2}' | sort > /tmp/post-crashloop.txt
new_cl=$(comm -13 "$dir/crashloop.txt" /tmp/post-crashloop.txt || true)
dir=/workspace/_out/upgrade-baseline
[ -f "$dir/unbound-pv.txt" ] || { echo "missing baseline unbound-pv.txt (seed did not run?)" >&2; exit 1; }
[ -f "$dir/crashloop.txt" ] || { echo "missing baseline crashloop.txt (seed did not run?)" >&2; exit 1; }
echo "=== gate: no new unbound PersistentVolumes vs baseline ==="
kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt
new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true)
if [ -n "$new_pv" ]; then
echo "FAIL: PersistentVolumes newly unbound after upgrade:" >&2
echo "$new_pv" >&2
kubectl get pv 2>&1 | awk 'NR==1 || $5 != "Bound"' >&2 || true
exit 1
fi
echo "no new unbound PVs"
echo "=== warning-only: new CrashLoopBackOff pods vs baseline ==="
kubectl get pods -A --no-headers 2>/dev/null | awk '/CrashLoopBackOff/ {print $1"/"$2}' | sort > /tmp/post-crashloop.txt
new_cl=$(comm -13 "$dir/crashloop.txt" /tmp/post-crashloop.txt || true)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml` around lines 41
- 55, Validate that the baseline files exist before running either comm
comparison in the PersistentVolume and CrashLoopBackOff checks. Add fail-fast
guards for $dir/unbound-pv.txt and $dir/crashloop.txt, exiting nonzero with an
appropriate error when either is missing, while preserving the existing PV
failure gate and warning-only CrashLoopBackOff behavior.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/e2e-prepare/action.yaml:
- Around line 3-8: Clarify the retry description in the composite action
documentation around the Talos e2e sandbox provisioning: state that the command
permits up to 3 total attempts, meaning 2 retries, and update the referenced
e2e-testing guidance citation or wording so both describe the same retry budget.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f384a97-7f00-44c4-a126-1a327aed0c6c

📥 Commits

Reviewing files that changed from the base of the PR and between 6f987df and 2cb1845.

📒 Files selected for processing (2)
  • .github/actions/e2e-prepare/action.yaml
  • .github/workflows/pull-requests.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/pull-requests.yaml

Comment thread .github/actions/e2e-prepare/action.yaml Outdated

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hack/e2e-upgrade-install-previous.bats (1)

69-73: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add existence backstops before kubectl wait and avoid ad-hoc retries.

The kubectl wait on line 69 is missing a prior existence backstop, making it vulnerable to race conditions if the Deployment is not yet created. Furthermore, lines 72-73 use an ad-hoc until kubectl wait retry loop instead of the established backstop pattern.

As per coding guidelines for E2E scripts and learnings, every kubectl wait must be guarded by an existence backstop (until kubectl get), and ad-hoc retry variations should be avoided in favor of the repository's established pattern.

🛠️ Proposed fix to apply the established backstop pattern
-  kubectl wait deployment/cozystack-operator -n cozy-system --timeout=2m --for=condition=Available
-
-  # Operator installs the CRDs at startup, then creates the platform PackageSource.
-  timeout 120 sh -ec 'until kubectl wait crd/packages.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
-  timeout 120 sh -ec 'until kubectl wait crd/packagesources.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
+  timeout 60 sh -ec 'until kubectl get deployment/cozystack-operator -n cozy-system >/dev/null 2>&1; do sleep 2; done'
+  kubectl wait deployment/cozystack-operator -n cozy-system --timeout=2m --for=condition=Available
+
+  # Operator installs the CRDs at startup, then creates the platform PackageSource.
+  timeout 120 sh -ec 'until kubectl get crd/packages.cozystack.io crd/packagesources.cozystack.io >/dev/null 2>&1; do sleep 2; done'
+  kubectl wait crd/packages.cozystack.io crd/packagesources.cozystack.io --for=condition=Established --timeout=2m
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-upgrade-install-previous.bats` around lines 69 - 73, Guard the
deployment wait and both CRD waits with the repository’s established existence
backstop: first poll using “until kubectl get” with the appropriate resource and
namespace, then run “kubectl wait” once. Replace the ad-hoc “until kubectl wait”
loops for packages.cozystack.io and packagesources.cozystack.io while preserving
their Established condition checks and timeouts.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@hack/e2e-upgrade-install-previous.bats`:
- Around line 69-73: Guard the deployment wait and both CRD waits with the
repository’s established existence backstop: first poll using “until kubectl
get” with the appropriate resource and namespace, then run “kubectl wait” once.
Replace the ad-hoc “until kubectl wait” loops for packages.cozystack.io and
packagesources.cozystack.io while preserving their Established condition checks
and timeouts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3befbf2-52d8-4344-9c1d-0a3182ef6251

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb1845 and a14cff5.

📒 Files selected for processing (1)
  • hack/e2e-upgrade-install-previous.bats

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
hack/e2e-upgrade-install-previous.bats (1)

72-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align with the established existence backstop pattern.

The learning explicitly advises using until kubectl get to poll for resource existence prior to a single kubectl wait call, rather than wrapping kubectl wait inside the polling loop itself.

As per retrieved learnings: "guard kubectl wait by first polling for the target resource to exist... Prefer the repository’s established backstop pattern rather than ad-hoc one-off variations."

♻️ Proposed refactor
-  timeout 120 sh -ec 'until kubectl wait crd/packages.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
-  timeout 120 sh -ec 'until kubectl wait crd/packagesources.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
+  timeout 120 sh -ec 'until kubectl get crd/packages.cozystack.io crd/packagesources.cozystack.io >/dev/null 2>&1; do sleep 2; done'
+  kubectl wait crd/packages.cozystack.io crd/packagesources.cozystack.io --for=condition=Established --timeout=2m
   timeout 120 sh -ec 'until kubectl get packagesource cozystack.cozystack-platform >/dev/null 2>&1; do sleep 2; done'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-upgrade-install-previous.bats` around lines 72 - 74, Update the
polling commands in the upgrade-install flow so each CRD first uses an until
kubectl get existence loop, then performs a single kubectl wait for Established.
Preserve the existing timeouts and resource names, and apply the repository’s
established existence-backstop pattern consistently to packages.cozystack.io and
packagesources.cozystack.io.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@hack/e2e-upgrade-install-previous.bats`:
- Around line 72-74: Update the polling commands in the upgrade-install flow so
each CRD first uses an until kubectl get existence loop, then performs a single
kubectl wait for Established. Preserve the existing timeouts and resource names,
and apply the repository’s established existence-backstop pattern consistently
to packages.cozystack.io and packagesources.cozystack.io.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d89a1314-c216-4c79-8bc4-4fc3afe09e47

📥 Commits

Reviewing files that changed from the base of the PR and between d8f3ad6 and 7ee079e.

📒 Files selected for processing (4)
  • docs/agents/e2e-testing.md
  • hack/e2e-upgrade-install-previous.bats
  • packages/core/platform/templates/migration-hook.yaml
  • packages/core/platform/values.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/agents/e2e-testing.md

IvanHunters
IvanHunters previously approved these changes Jul 20, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict

LGTM with non-blocking notes

test(e2e): add chainsaw-native release upgrade testing lane (+1692/-90, 29 files). No CRITICAL/MAJOR defects after executing the available offline reproductions (helm template vs merge-base, dash/shellcheck on both new scripts, mutation-testing the new bats suite, source-level verification of the GitHub Actions / Chainsaw assumptions).

Findings

[MINOR] hack/upgrade-prev-version_test.bats:87-95 (guards hack/upgrade-prev-version.sh:41) — the test named "line match is dot-anchored (1.5 does not match v155.x)" is theatre. Removing the dot-escaping (sed 's/\./\\./g') still leaves all 10 tests green, because stable_desc's upstream filter already normalizes tag shapes so escaped vs unescaped patterns are equivalent (confirmed via a ~30k-combination differential check). A second mutation on the walk-down loop did correctly turn a different test red, so the rest of the suite is non-vacuous — only this one named test overclaims what it verifies.

Claim mismatches (non-blocking)

  • [PARTIAL] PR body "Status" section is stale: commit 2c6b9140 already fixes the canary DB access path the body says "still needs a dev-cluster run".
  • [UNVERIFIABLE] actionlint / live CI green — outside the hermetic toolset; the unit-tests portion was independently confirmed by re-running the bats suite.

Caveats (verification limits, not defects)

  • Phase 5c corner-render for the new migrations.etcdAdoptSkipBackup toggle could not be executed: helm template packages/core/platform fails offline on a pre-existing unconditional lookup+fail in repository.yaml (reproduces identically on merge-base, not touched by this PR), and migration-hook.yaml is itself lookup-gated. Residual risk judged low — the consumed ETCD_ADOPT_SKIP_BACKUP env var is pre-existing, already unit-tested, and the wiring is a single unconditional-else block with no cross-reference or dependsOn.
  • The chart_lint render_error + 2 missing_refs flagged by tooling are all confirmed pre-existing and unrelated (reproduced on merge-base; referencing templates untouched).
  • Both new #!/bin/sh scripts pass shell-portability checks (shellcheck --shell=sh, dash -n, no bashisms, invoked only via shebang).
  • Tenant-Kubernetes seed/verify suites are correctly gated off by default (UPGRADE_E2E_TENANT_K8S); zero risk to default CI.

@myasnikovdaniil
myasnikovdaniil force-pushed the feat/upgrade-e2e-chainsaw branch from 6e0c374 to c47f709 Compare July 20, 2026 12:56
myasnikovdaniil added a commit that referenced this pull request Jul 21, 2026
…reate (#3319)

## What this PR does

Fixes a Helm-upgrade failure in the `opensearch-operator` chart that
blocks the new release-upgrade E2E lane (added in #3276).

Upgrading from a chart that shipped no explicit strategy (v1.5.x) to a
v1.6.0-rc build fails validation immediately, helm-controller retries
it, and the HelmRelease never becomes Ready:

```
Helm upgrade failed for release cozy-opensearch-operator/opensearch-operator:
Deployment.apps "opensearch-operator-controller-manager" is invalid:
spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is Recreate
```

Observed in the ["Upgrade E2E Test" job of run
29480724982](https://github.com/cozystack/cozystack/actions/runs/29480724982)
— `UpgradeFailed ... (x19 over 11m)`, with the HelmRelease never
reaching Ready.

### Root cause

#3040 (commit db797b1) set the single-replica operator Deployment to
`strategy.type: Recreate` — a reasonable goal: with leader election
gated off on one replica, a default `RollingUpdate` briefly runs two
uncoordinated managers (`maxSurge`) during a rollout, and `Recreate`
closes that window.

The problem surfaces only on **upgrade from an older chart, under
server-side apply**:

- A v1.5.x release shipped no strategy at all, so the apiserver
**defaulted** `spec.strategy.rollingUpdate` to `25%/25%`, and the
`helm-controller` field manager never owned that block. Its
`managedFields` entry covers `f:replicas`, `f:selector` and
`f:template`, but not `f:strategy`.
- helm-controller applies **server-side** by default
(`Install.ServerSideApply` defaults to true, `Upgrade.ServerSideApply`
to `auto`; `internal/operator/package_reconciler.go` sets neither), and
SSA does not remove a field the applier never owned merely because the
new intent omits it.
- So the v1.6.0-rc manifest's `type: Recreate` merges onto a live object
that still carries the defaulted `rollingUpdate`, and the apiserver
rejects the result with `FieldValueForbidden`.

The **client-side** path is unaffected, which is why this surfaces
through helm-controller's SSA but not a local `helm upgrade`:
`DeploymentSpec.Strategy` carries `patchStrategy:"retainKeys"`
(`k8s.io/api@v0.34.1/apps/v1/types.go:395`), so a 3-way merge emits
`{"spec":{"strategy":{"$retainKeys":["type"],"type":"Recreate"}}}`,
which merges to a valid `{"strategy":{"type":"Recreate"}}` and does
clear the block.

This is a genuine product upgrade bug, not a test-harness issue — #3276
only adds the upgrade lane that exposes it; it does not touch
`opensearch-operator`.

> An earlier revision of this PR attributed the failure to Helm's
client-side 3-way merge. That was wrong, and the correction is
@lexfrei's — see [his
review](#3319 (review)).
The mechanism above is now reproduced end to end (below).

### The fix

Keep `type: RollingUpdate` on the single-replica path and set
`rollingUpdate.maxSurge: 0 / maxUnavailable: 1` instead of switching to
`Recreate`:

- `maxSurge: 0` caps total pods at `replicas`, so the deployment
controller must scale the old manager down before it can scale the new
one up — unlike the default 25% surge, which starts a second manager
while the first is still fully live.
- This is **not** `Recreate`'s exclusivity, and the PR no longer claims
it is. `rolloutRecreate` gates scale-up on `oldPodsRunning()`, which
counts non-terminal `Status.Phase`, so `Recreate` waits for the old pod
to be gone. `rolloutRolling` has no such gate: `NewRSNewReplicas`
derives `currentPodCount` from `GetReplicaCountForReplicaSets`, which
sums `rs.Spec.Replicas` (desired, not live). The ordering that is
guaranteed is over ReplicaSet desired counts, not pod lifecycles, so a
draining old manager can still overlap the new pod within
`terminationGracePeriodSeconds` (10s here).
- Because `strategy.type` never becomes `Recreate`, the forbidden merge
cannot occur on any apply path. It also recovers anyone already on an rc
build with `Recreate`, since `Recreate -> RollingUpdate` is permitted.
- This matches the idiom already vendored in this repo:
`packages/system/kubeovn/charts/kube-ovn/templates/central-deploy.yaml:11-15`
uses `RollingUpdate` with `maxSurge: 0 / maxUnavailable: 1`.

Multi-replica installs (`manager.replicaCount > 1`) are unchanged: they
keep the default `RollingUpdate` with leader election enabled.

The change is carried in `patches/leaderElection.diff` so it survives a
chart re-vendor via `make update`; the patch applies to pristine
upstream 2.8.0 with no fuzz and reproduces the committed strategy block
exactly.

### Alternatives considered

| Option | Why not |
| --- | --- |
| Keep `Recreate`, but make the chart own `rollingUpdate` first (ship an
explicit block, then switch `type` in a later release) | This is the
only option that preserves real exclusivity, but it needs two releases
and only works once every install has reconciled the intermediate one.
Disproportionate for a single-replica operator whose residual overlap is
a draining manager. |
| Pre-upgrade hook / Job that patches or deletes the Deployment strategy
before the upgrade | Heavy: needs a kubectl image, a ServiceAccount and
RBAC to `patch`/`delete` Deployments, and hook ordering; runs on every
upgrade even when unneeded; adds standing attack surface —
disproportionate for a scalar field transition. |
| `helm.sh/resource-policy` / force-replace | `resource-policy: keep`
only governs deletion on uninstall (irrelevant). Helm has no
per-resource "force recreate" annotation; `--force` / HelmRelease
`spec.upgrade.force` is global and disruptive and not controllable from
within the chart. |
| Plain revert to default `RollingUpdate` (25%/25%) | Fixes the
rejection but starts a surge pod while the old manager is still fully
live — the window #3040 set out to close. `maxSurge: 0` narrows it to a
draining old pod without leaving `RollingUpdate`. |

### Verification

The mechanism and the fix were reproduced on kind v1.33.1 (a bare
apiserver is enough — the failure is defaulting + SSA + validation):

| Path | Result |
| --- | --- |
| SSA install of the v1.5.x manifest (no strategy) | apiserver defaults
`rollingUpdate: 25%/25%`; `managedFields` shows `helm-controller` does
not own `f:strategy` |
| SSA upgrade to `type: Recreate` | fails with the exact production
error above |
| Client-side apply of the same pair | succeeds; `rollingUpdate` is
cleared, live strategy becomes `{"type":"Recreate"}` |
| SSA upgrade to `maxSurge: 0` (this PR) | succeeds |
| SSA of `maxSurge: 0` onto a live `Recreate` Deployment (rc recovery) |
succeeds |
| Rollout under `maxSurge: 0` vs `Recreate` | `maxSurge: 0`: two pods
coexisted within 1s (old Terminating, new ContainerCreating).
`Recreate`: one pod, waited the full drain. This is the evidence for the
"not exclusivity" wording above. |

### Note on regression coverage

`tests/leader_election_test.yaml` was never executed:
`hack/helm-unit-tests.sh` runs a package's suite only when the package
Makefile defines a `test:` target, and this one did not — so the
coverage the previous revision of this PR claimed did not exist. This PR
adds the target (matching `packages/system/etcd-operator/Makefile`),
which revives the suite; the repo-wide runner now picks the package up
and a revert to `Recreate` fails it. Thanks to @lexfrei for catching
that.

The apiserver-side merge that produces the invalid object still needs a
live cluster, so the behavioural regression test remains the
release-upgrade E2E lane in #3276 (which this fix unblocks).

### Follow-ups filed

- #3324 — `packages/system/ouroboros` has the same latent shape
(`Recreate` gated on `controller.mode == "external-dns"`, while
`controller.mode` defaults to `coredns`). Not fixed here, and it likely
needs a different fix, since that mode genuinely depends on exclusivity.
- #3325 — `make update` does not reproduce this package's vendored
template: three hand-edits are not captured in `patches/`, and one of
them breaks rendering when `manager.extraEnv` is set. Pre-existing on
`main`; an earlier revision of this body claimed the regeneration was
exact, which was wrong.

### Downstream repositories

- [x] No downstream repository is affected by this change

<!-- Walked the trigger map in docs/agents/contributing.md against the
diff: the change touches only a vendored packages/system operator chart
template + its patch + its helm-unittest suite + the package Makefile
test target. No packages/apps or packages/extra add/rename/remove, no
values.schema.json / enum / default change, no node contract, no
annotations/labels, no hack/ or ApplicationDefinition change.
opensearch-k8s-operator is a third-party upstream (not a cozystack
downstream), and the fix lives in patches/leaderElection.diff so it
survives make update. -->

### Release note

```release-note
fix(opensearch-operator): fix a Helm upgrade failure on single-replica installs. Upgrading a release that predates an explicit Deployment strategy failed under server-side apply, because switching strategy.type to Recreate does not clear the apiserver-defaulted spec.strategy.rollingUpdate block that the chart never owned, and the merged object is rejected. The operator now uses RollingUpdate with maxSurge=0/maxUnavailable=1, which keeps the transition legal on every apply path and recovers releases already stuck on Recreate.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Updated OpenSearch Operator controller rollout behavior for
single-replica installs to use a safer rolling update configuration.
* Leader election is now applied only when multiple replicas are
configured, reducing the chance of conflicting controller activity.
* **Tests**
* Expanded Helm unittest assertions for the controller deployment
rollout strategy and leader-election behavior for single- and
multi-replica scenarios.
* **Chores**
  * Added a `make test` target to run Helm unit tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@coderabbitai coderabbitai Bot mentioned this pull request Jul 22, 2026
11 tasks
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/upgrade-e2e-chainsaw branch from c47f709 to f6c0ed4 Compare July 29, 2026 13:40

@lexfrei Aleksei Sviridkin (lexfrei) 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.

NOT LGTM. The head commit rewrites two workflow gate expressions that an existing contract test pins verbatim, without updating that test. The unit-test job goes red, which skips finalize, which skips both the required E2E check and this PR's own new lane, so at this head nothing in the verification pipeline actually ran.

Business context: upgrade coverage is entirely absent today (there is no upgrade job anywhere in pull-requests.yaml on main), so a defect that only appears when an existing cluster moves to a new release ships unnoticed. This adds a lane that installs the previous stable release, seeds workloads with canary data, upgrades to the build under test, and asserts survival.

Blockers

B1: broadening the label gate breaks the contract test that guards it, which takes down the whole pipeline

File: .github/workflows/pull-requests.yaml:32 and :454

Issue: hack/promote-gate-contract.bats:122 and :126 pin these two gate expressions as exact string literals. This PR broadens both from github.event.label.name == 'full-e2e' to contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name) and does not update the test, so both grep -cF counts fall to zero.

Evidence: reproduced locally at f6c0ed4, running that contract file on its own prints Test failed: release PR E2E is a working manual full-e2e label opt-in (exit 1). The CI job log ends with the same sequence, + count=0, + [ 0 -eq 1 ], then make: *** [Makefile:165: bats-unit-tests] Error 1. In the run at this head, Unit & controller tests is failure and Finalize, E2E Tests and Upgrade E2E Test are all skipped.

Impact: the required E2E Tests check does not run on merit, it reports skipped. The new lane does not run either, even though the upgrade-e2e label is applied, so the commit titled "make the upgrade-e2e label opt-in actually start a run" currently stops every run instead. It is also why the lane's results are only visible in the run from nine days ago rather than at this head.

Fix: update both assertions to match the broadened expressions and add one asserting upgrade-e2e is in the allow-list, so the contract keeps guarding the gate rather than being routed around it.

On the lane's own red result

I checked this rather than taking it on trust, because a brand new upgrade lane failing on its first run is equally consistent with the lane being wrong. Two of the three failures carry a terminal mechanism and are platform defects, not lane defects.

The seaweedfs pair fails during template rendering: execution error at (seaweedfs/templates/seaweedfs.yaml:191:6) and (cozy-seaweedfs/templates/naming-guard.yaml:151:6), the naming guard refusing the release because volume names are derived from the release name. That is only reachable by upgrading an existing install, which is exactly the gap this lane closes. tenant-root/tenant-root then fails waiting on it, so that is a consequence rather than a fourth defect.

opensearch-operator fails with Deployment.apps "opensearch-operator-controller-manager" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is Recreate. That is the server-side-apply residue case, where the field set by the previous version survives into the new object. Upgrade-only again, and opensearch-rd is a downstream dependency of it, not a separate defect.

The third, vm-instance-test timing out on VirtualMachine/tenant-test/vm-instance-test status: 'InProgress', is a real observed regression signal, but the log does not establish a root cause, so I would not put it in the same category as the other two yet. Worth saying that explicitly rather than counting three equally proven findings.

All three belong in their own changes, so none of them block this one. I also confirmed the lane is genuinely advisory rather than taking the description's word for it: branch protection on main requires only pre-commit and E2E Tests, and this job's check name is Upgrade E2E Test, which is neither.

Non-blocking follow-ups

  1. A permanently red advisory check trains reviewers to ignore it, and then it stops working on the day it catches something new. Nothing here records which failures are currently expected, so once this merges a fourth, unrelated failure is indistinguishable from the three known ones. Landing the three fixes first, or recording the known-failing set somewhere the lane prints, would keep it honest.

  2. hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml:44 and :55: comm -13 "$dir/unbound-pv.txt" ... || true turns a missing baseline file into an empty diff and therefore a pass, so a verify phase run without its seed phase reports healthy having compared nothing. The sibling suite already handles this correctly at verify/vm/chainsaw-test.yaml:47, which fails fast with [ -f "$before" ] || { ...; exit 1; }. Applying the same guard here would make the two consistent. This was raised in the automated review and is still open.

  3. hack/upgrade-prev-version_test.bats:87: the case named "line match is dot-anchored (1.5 does not match v155.x)" does not test that. I removed the sed 's/\./\\./g' escaping at hack/upgrade-prev-version.sh:41 and all ten cases stayed green, because stable_desc already constrains tags to vX.Y.Z and that makes the escaped and unescaped patterns equivalent over the resulting set. The shipped code is correct, only the test overclaims. An earlier review reached the same conclusion. The rest of the suite is not vacuous: unanchoring the stable-tag regex, and starting the minor walk at min instead of min - 1, each turned a case red.

  4. .github/actions/e2e-download-assets/action.yaml:50: curl -sSL ... -o _out/assets/nocloud-amd64.raw.xz has no --fail, so an auth failure or 404 gets written into the disk image while curl exits 0, surfacing much later as a confusing decompression error. The behaviour is unchanged from the code this was extracted from, but it now serves two jobs, and the release PR path is precisely the one this lane exists for. Also raised in the automated review and still open.

  5. The description does not name the costs it accepts: a second full platform install per run on a 32 vCPU runner with a 180 minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change. I think the trade is worth making, but it should be stated rather than left implicit. The "Status" section also still says "Draft" and "unit-tests green locally", neither of which holds now.

  6. hack/e2e-install-cozystack.bats is also being changed by another open PR, in the worker image catalog region rather than the readiness gate this one extracts. They should not conflict; I am flagging it only so whoever merges second knows to look.

What I checked and found clean

No EXIT or RETURN traps in either new pipeline BATS file or in any of the twelve new Chainsaw suites. hack/e2e-wait-hr-ready.sh uses if kubectl wait ...; then rather than capturing output into a variable, so it avoids the failure mode where an assignment with command substitution under set -e kills the shell before the captured output is ever printed. I checked every command-substitution assignment this diff adds and none has that shape. hack/upgrade-prev-version_test.bats is correctly picked up by the unit target rather than filtered out as an e2e file, and it declares ten cases and executes ten. The tenant Kubernetes suites gate off through an early exit 0 in a single script step and print a visible skip line. The lane runs fixed seed and verify paths with no selector, so unlike the app suite it cannot silently select nothing and report success; the one way it can report a non-failure having run nothing is the skip path in B1. The seed suites really do reuse the existing app manifests, all five relative paths resolve. Documentation is in sync, this diff adds the matching section to the E2E conventions doc.

name: Plan build
runs-on: ubuntu-latest
if: github.event.action != 'labeled' || github.event.label.name == 'full-e2e'
if: github.event.action != 'labeled' || contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name)

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.

This literal is pinned by hack/promote-gate-contract.bats:122, which is not updated in this PR, so grep -cF returns 0 and the assertion on the next line fails. That takes bats-unit-tests red, which skips finalize, which skips both E2E Tests and Upgrade E2E Test. Line 454 breaks the companion assertion at :126 the same way. Updating the contract test to the broadened expressions (and asserting upgrade-e2e is in the allow-list) keeps the guard meaningful.

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.

Second round on the same head, posted separately so the additions actually reach you. Two things are new since my earlier review: a second blocker on the stale file header in the mariadb seed manifest, and a corrected statement of the version-resolver defect (the failing input is a single-component target like v1, not a two-component one). Everything else below stands as reported.

NOT LGTM. The head commit rewrites two workflow gate expressions that an existing contract test pins verbatim, without updating that test. The unit-test job goes red, which skips finalize, which skips both the required E2E check and this PR's own new lane, so at this head nothing in the verification pipeline actually ran.

Business context: upgrade coverage is entirely absent today (there is no upgrade job anywhere in pull-requests.yaml on main), so a defect that only appears when an existing cluster moves to a new release ships unnoticed. This adds a lane that installs the previous stable release, seeds workloads with canary data, upgrades to the build under test, and asserts survival.

Blockers

B1: broadening the label gate breaks the contract test that guards it, which takes down the whole pipeline

File: .github/workflows/pull-requests.yaml:32 and :454

Issue: hack/promote-gate-contract.bats:122 and :126 pin these two gate expressions as exact string literals. This PR broadens both from github.event.label.name == 'full-e2e' to contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name) and does not update the test, so both grep -cF counts fall to zero.

Evidence: reproduced locally at f6c0ed4, running that contract file on its own prints Test failed: release PR E2E is a working manual full-e2e label opt-in (exit 1). The CI job log ends with the same sequence, + count=0, + [ 0 -eq 1 ], then make: *** [Makefile:165: bats-unit-tests] Error 1. In the run at this head, Unit & controller tests is failure and Finalize, E2E Tests and Upgrade E2E Test are all skipped.

Impact: the required E2E Tests check does not run on merit, it reports skipped. The new lane does not run either, even though the upgrade-e2e label is applied, so the commit titled "make the upgrade-e2e label opt-in actually start a run" currently stops every run instead. It is also why the lane's results are only visible in the run from nine days ago rather than at this head.

Fix: update both assertions to match the broadened expressions and add one asserting upgrade-e2e is in the allow-list, so the contract keeps guarding the gate rather than being routed around it.

On the lane's own red result

I checked this rather than taking it on trust, because a brand new upgrade lane failing on its first run is equally consistent with the lane being wrong. Two of the three failures carry a terminal mechanism and are platform defects, not lane defects.

The seaweedfs pair fails during template rendering: execution error at (seaweedfs/templates/seaweedfs.yaml:191:6) and (cozy-seaweedfs/templates/naming-guard.yaml:151:6), the naming guard refusing the release because volume names are derived from the release name. That is only reachable by upgrading an existing install, which is exactly the gap this lane closes. tenant-root/tenant-root then fails waiting on it, so that is a consequence rather than a fourth defect.

opensearch-operator fails with Deployment.apps "opensearch-operator-controller-manager" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is Recreate. That is the server-side-apply residue case, where the field set by the previous version survives into the new object. Upgrade-only again, and opensearch-rd is a downstream dependency of it, not a separate defect.

The third, vm-instance-test timing out on VirtualMachine/tenant-test/vm-instance-test status: 'InProgress', is a real observed regression signal, but the log does not establish a root cause, so I would not put it in the same category as the other two yet. Worth saying that explicitly rather than counting three equally proven findings.

All three belong in their own changes, so none of them block this one. I also confirmed the lane is genuinely advisory rather than taking the description's word for it: branch protection on main requires only pre-commit and E2E Tests, and this job's check name is Upgrade E2E Test, which is neither.

Non-blocking follow-ups

  1. A permanently red advisory check trains reviewers to ignore it, and then it stops working on the day it catches something new. Nothing here records which failures are currently expected, so once this merges a fourth, unrelated failure is indistinguishable from the three known ones. Landing the three fixes first, or recording the known-failing set somewhere the lane prints, would keep it honest.

  2. hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml:44 and :55: comm -13 "$dir/unbound-pv.txt" ... || true turns a missing baseline file into an empty diff and therefore a pass, so a verify phase run without its seed phase reports healthy having compared nothing. The sibling suite already handles this correctly at verify/vm/chainsaw-test.yaml:47, which fails fast with [ -f "$before" ] || { ...; exit 1; }. Applying the same guard here would make the two consistent. This was raised in the automated review and is still open.

  3. hack/upgrade-prev-version_test.bats:87: the case named "line match is dot-anchored (1.5 does not match v155.x)" does not test that. I removed the sed 's/\./\\./g' escaping at hack/upgrade-prev-version.sh:41 and all ten cases stayed green, because stable_desc already constrains tags to vX.Y.Z and that makes the escaped and unescaped patterns equivalent over the resulting set. The shipped code is correct, only the test overclaims. An earlier review reached the same conclusion. The rest of the suite is not vacuous: unanchoring the stable-tag regex, and starting the minor walk at min instead of min - 1, each turned a case red.

  4. .github/actions/e2e-download-assets/action.yaml:50: curl -sSL ... -o _out/assets/nocloud-amd64.raw.xz has no --fail, so an auth failure or 404 gets written into the disk image while curl exits 0, surfacing much later as a confusing decompression error. The behaviour is unchanged from the code this was extracted from, but it now serves two jobs, and the release PR path is precisely the one this lane exists for. Also raised in the automated review and still open.

  5. The description does not name the costs it accepts: a second full platform install per run on a 32 vCPU runner with a 180 minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change. I think the trade is worth making, but it should be stated rather than left implicit. The "Status" section also still says "Draft" and "unit-tests green locally", neither of which holds now.

  6. hack/e2e-install-cozystack.bats is also being changed by another open PR, in the worker image catalog region rather than the readiness gate this one extracts. They should not conflict; I am flagging it only so whoever merges second knows to look.

What I checked and found clean

No EXIT or RETURN traps in either new pipeline BATS file or in any of the twelve new Chainsaw suites. hack/e2e-wait-hr-ready.sh uses if kubectl wait ...; then rather than capturing output into a variable, so it avoids the failure mode where an assignment with command substitution under set -e kills the shell before the captured output is ever printed. I checked every command-substitution assignment this diff adds and none has that shape. hack/upgrade-prev-version_test.bats is correctly picked up by the unit target rather than filtered out as an e2e file, and it declares ten cases and executes ten. The tenant Kubernetes suites gate off through an early exit 0 in a single script step and print a visible skip line. The lane runs fixed seed and verify paths with no selector, so unlike the app suite it cannot silently select nothing and report success; the one way it can report a non-failure having run nothing is the skip path in B1. The seed suites really do reuse the existing app manifests, all five relative paths resolve. Documentation is in sync, this diff adds the matching section to the E2E conventions doc.

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.

NOT LGTM. Second round on the same head. Two things are new since my previous review, and the rest of that review stands unchanged: a second blocker, the file header in the MariaDB seed suite prescribing the connection method the same file documents as fatal, and a corrected version of the resolver follow-up, where the defect is real but my earlier framing of it was wrong. I also tightened the evidence on the original blocker.

Blockers

B1: broadening the label gate breaks the contract test that guards it, which takes down the whole pipeline

Unchanged from the previous round, restated because it still blocks. .github/workflows/pull-requests.yaml:32 and :454 broaden the gate from github.event.label.name == 'full-e2e' to contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name), while hack/promote-gate-contract.bats:122 and :126 pin the old literals. Both grep -cF counts fall to zero, bats-unit-tests fails, Unit & controller tests goes red, Finalize is skipped, and with it both E2E Tests and Upgrade E2E Test.

New evidence this round. This is not an artifact of the branch trailing main. The branch is zero commits behind main, and hack/promote-gate-contract.bats is byte-identical between this head and origin/main, where it still pairs with the narrow expressions at lines 31 and 453. The test and the workflow agree on main, and this PR is what puts them out of step. Attribution is unambiguous.

Fix: update both assertions to the broadened expressions and add one asserting upgrade-e2e is in the allow-list, so the contract keeps guarding the gate instead of being routed around.

B2: the MariaDB seed header prescribes the connection method the same file documents as fatal

hack/e2e-chainsaw-upgrade/seed/mariadb/chainsaw-test.yaml:5 says the canary is written "over 127.0.0.1 from inside the primary pod". Lines 23 and 24 of the same file say the opposite, with a mechanism attached: "connecting to 127.0.0.1 fails (server does not bind loopback) and its retry OOM-kills the nano server container; the primary Service is clean". The code at lines 65 and 70 uses -h mariadb-test-primary, matching the second comment and contradicting the header.

Commit f598c253 ("fix upgrade canary DB access, verified on a live cluster") records this same finding from a dev cluster and changed the code and the step comment, but left the file header on the pre-fix approach. So the header describes the method that commit replaced.

The header is the first thing anyone reads here. Someone simplifying the suite back to what it claims to do reproduces an OOM-kill of the server container, and that surfaces as a timeout that looks unrelated rather than as a connection error, which is the expensive kind to debug. Rewriting the header to match -h mariadb-test-primary is the whole fix. One line of prose, no test needed.

Non-blocking follow-ups

Items 4 and 8 are new this round. The rest are unchanged from the previous review.

  1. A permanently red advisory check trains reviewers to ignore it, and then it stops working on the day it catches something new. Nothing here records which failures are currently expected, so once this merges a fourth, unrelated failure is indistinguishable from the three known ones. Landing the three fixes first, or recording the known-failing set somewhere the lane prints, would keep it honest.

  2. hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml:44 and :55: comm -13 "$dir/unbound-pv.txt" ... || true turns a missing baseline file into an empty diff and so into a pass, and a verify phase run without its seed phase reports healthy having compared nothing. The sibling suites handle this correctly already, verify/vm/chainsaw-test.yaml:47 and verify/redis both fail fast with [ -f "$before" ] || { ...; exit 1; }. The same guard here would make the three consistent. Raised in the automated review and still open.

  3. hack/upgrade-prev-version_test.bats:87: the case named "line match is dot-anchored (1.5 does not match v155.x)" does not test that. I removed the sed 's/\./\\./g' escaping at hack/upgrade-prev-version.sh:41 and all ten cases stayed green, because stable_desc already constrains tags to vX.Y.Z, which makes the escaped and unescaped patterns equivalent over that set. The shipped code is correct, only the test overclaims. An earlier review reached the same conclusion. The rest of the suite holds up: unanchoring the stable-tag regex, and starting the minor walk at min instead of min - 1, each turned a case red.

  4. hack/upgrade-prev-version.sh:56: a single-component target is silently misparsed instead of rejected. min=$(printf '%s' "$ver" | cut -d. -f2) returns the whole string when there is no second field, so v1 gives maj=1 min=1, the numeric case guard accepts 1:1 as well-formed, and the walk resolves the 1.0 line. Confirmed against a synthetic tag set: v1 returns v1.0.9 with exit 0. Two- and three-component targets are fine, cut finds a second field in both. Unreachable from CI, since the workflow requires three components before it sets a target at all, but this is a standalone tool with a ten-case suite and one more fixture would pin it.

  5. .github/actions/e2e-download-assets/action.yaml:50: curl -sSL ... -o _out/assets/nocloud-amd64.raw.xz has no --fail, so an auth failure or 404 gets written into the disk image while curl exits 0, surfacing later as a confusing decompression error. Behaviour is unchanged from the code this was extracted from, but it now serves two jobs, and the release PR path is the one this lane exists for. Raised in the automated review and still open.

  6. The description does not name the costs it accepts: a second full platform install per run on a 32 vCPU runner with a 180 minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change. I think the trade is worth making, but it should be stated. The "Status" section also still says "Draft" and "unit-tests green locally", neither of which holds now.

  7. hack/e2e-install-cozystack.bats is also being changed by another open PR, in the worker image catalog region rather than the readiness gate this one extracts. They should not conflict. Flagging it so whoever merges second knows to look.

  8. Both phases write chainsaw-report.xml into the same directory and the workflow always uploads it as upgrade-chainsaw-report, so when the lane fails during seed the artifact is the seed report published under the verify name. The step comment assumes verify ran last, true only on the passing path. Copying per phase, or naming the artifact by the phase that produced it, removes the ambiguity in the case where someone is actually reading it.

Pre-existing, not introduced here

template-injection is disabled tree-wide in .github/zizmor.yml, which is why the new composites' ${{ inputs.sandbox-name }} interpolations into run: pass the gate. The values are sha256 hex so there is no exploitable path in this diff, but the new composite actions enlarge the surface that audit will cover once it is re-enabled. Worth attaching to the existing hardening item. Separately, hack/e2e-chainsaw/mariadb/mariadb.yaml carries hardcoded fixture credentials that predate this PR, and the new suites re-type the password literal in two more places, which argues for sourcing it from the manifest.

The lane's own red result, and what I verified clean

Unchanged from the previous round. Short form: of the three failures the lane surfaced, the seaweedfs naming-guard render failure and the opensearch spec.strategy.rollingUpdate rejection are platform defects reachable only by upgrading an existing install, which is the gap this lane exists to close. The vm-instance-test timeout is a real signal whose root cause the log does not establish, so I do not rank it with the other two. None of the three blocks this PR. The lane is advisory for real: branch protection requires only pre-commit and E2E Tests, and this job's check is Upgrade E2E Test.

Also unchanged. No EXIT or RETURN traps anywhere in the new suites or pipeline BATS files. No command-substitution assignment in this diff has the shape where set -e kills the shell before the captured output is printed. The new unit BATS declares ten cases, executes ten, and is picked up by the unit target. The tenant Kubernetes suites gate off through an early exit 0 and print a visible skip line. The lane has no suite selector, so it cannot silently select nothing, and the only way it reports a non-failure having run nothing is the skip path in B1. All five reused app manifest paths resolve. The migration-stamp assertion is reachable under the isp-full variant the lane selects. The E2E conventions doc gained the matching section in this diff.

#
# Reuses the app suite's MariaDB manifest verbatim (DRY). The canary lives in
# the manifest's `testdb`, written as `testuser` (password from the same
# manifest) over 127.0.0.1 from inside the primary pod — TCP so the user@%%

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.

This header says the canary is written over 127.0.0.1 from inside the primary pod, but lines 23 and 24 of this same file say connecting to 127.0.0.1 fails because the server does not bind loopback, and that the retry OOM-kills the nano server container. The code at lines 65 and 70 uses -h mariadb-test-primary, matching the second comment. Commit f598c253 fixed the code and the step comment after finding this on a live cluster, but left this header on the pre-fix approach, so it now describes the one method known to be fatal here.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) both blockers are fixed, plus the corrected resolver finding from your third round — db0b26b66, 4c818b2a8, 6c787399f.

B1, contract test. hack/promote-gate-contract.bats now matches the broadened expressions in plan and resolve_assets, and a new case pins the upgrade-e2e opt-in end to end: admitted by the labeled allow-list in both jobs, honoured by the job's own condition, and named Upgrade E2E Test so it cannot collide with the required E2E Tests context and stop being advisory. Verified non-vacuous — narrowing the plan gate back to the full-e2e-only literal turns exactly one case red. Unit & controller tests is green again and the lane executed at this head instead of skipping.

This one was mine to own: I ran the unit suite before writing that commit rather than after, and pre-commit does not cover bats-unit-tests, so it shipped behind a "green locally" claim that did not cover the change that broke it.

B2, MariaDB header. Rewritten to -h mariadb-test-primary, matching the code at 65/70 and the step comment, so the header no longer prescribes the loopback path the same file documents as OOM-killing the server container.

Resolver, per your correction. Reproduced — v1 returns v1.0.9 with rc=0 while v1.6 is fine. Root cause is cut -f2 printing the whole field when the line carries no delimiter, so min came back as 1 and v1 was read as v1.1. Fixed with -s. Worth flagging that my first test for it was vacuous: _fixture ships no v1.0.x tag, so the buggy path errored for the wrong reason and the case passed either way — reverting -s left all eleven green. It now carries its own tag set containing v1.0.9, and reverting -s fails it and only it.

Body. ## Status rewritten — "Draft" and "unit-tests green locally" are gone, the advisory-lane reasoning and required-context check are explicit, the findings are split two-confirmed/one-unproven per your framing, and the costs you listed are named.

The red e2e is not this PR. kubernetes-latest and kubernetes-previous fail identically on #3276, #3262, #3456 and #3294, and today's nightly on main is 43/43 green including both. Same mechanism everywhere — node-join failed: fewer than 2 tenant nodes Ready, tenant cilium never installing, dependents cascading. That points at the PR e2e lane rather than any PR's content; the structural difference from nightly is that PR runs reconstruct their image set via pr.patch plus hack/overlay-main-images.sh. #3471 happens to rework exactly that file and has a fresh run in flight, which should be informative. Flagging rather than asserting — I have not confirmed the cause.

Still open from your non-blocking list: curl --fail in e2e-download-assets, the comm -13 ... || true baseline guard in verify/platform, and the dot-anchored case name that still overclaims. Happy to take those here or separately.

myasnikovdaniil and others added 10 commits August 15, 2026 13:16
Reimplements the intent of #2401 (previous-minor-stable -> current upgrade
test) over the post-Chainsaw-migration codebase. The old #2401 was a single
895-line BATS file, now obsolete; this builds a two-phase Chainsaw lane
bracketing an external helm upgrade, reusing existing files throughout.

Flow (make upgrade-cozystack, in the e2e sandbox):
  install-previous (bats, prev OCI chart) -> seed (chainsaw, skipDelete) ->
  helm upgrade to in-tree chart (bats) -> verify (chainsaw, assert-only).

DRY over existing files:
- 4 composite actions (.github/actions/e2e-{download-assets,prepare,collect,
  teardown}) shared by both the e2e and new upgrade-e2e jobs; the e2e job is
  refactored onto them (behavior-preserving).
- hack/e2e-wait-hr-ready.sh: the all-HR-Ready gate extracted from
  e2e-install-cozystack.bats so install and upgrade share one legible gate.
- hack/upgrade-prev-version.sh (+ unit test) resolves the baseline version.
- seed suites apply the existing app manifests by relative path (no new CRs).

Trigger: an upgrade-e2e job in pull-requests.yaml, parallel to e2e, gated on
the release label or a maintainer upgrade-e2e label. Advisory / non-blocking.

Coverage: postgres + mariadb canary rows, redis/VM volume survival, migration
stamp advance, all-HRs-Ready + no-new-unbound-PV. The tenant Kubernetes
survival suites (#2931 path) are env-gated OFF (UPGRADE_E2E_TENANT_K8S) as the
flakiest surface, pending dev-stand validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Dev-cluster validation of the seed/verify canary commands surfaced two real
bugs, now fixed and re-verified (postgres=3, mariadb=3 rows):

- postgres: the seed writes SQL to psql over a heredoc on stdin, but
  `kubectl exec` without `-i` does not forward stdin, so CREATE/INSERT silently
  never ran. Add `-i`.
- mariadb: the nano server container ships the `mariadb` client (MariaDB 11
  dropped `mysql`) and does not bind 127.0.0.1 — connecting there fails and its
  retry OOM-kills the container. Use the `mariadb` client against the
  mariadb-test-primary Service (writable primary) from mariadb-test-0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Pre-commit's zizmor audit flags any `>> $GITHUB_ENV` write inside a composite
action as a cross-boundary env write (github-env, "may allow code execution") —
even the identical line passes in a workflow file. Move the SANDBOX_NAME write
back into each job as a one-liner (the pre-composite, zizmor-clean form) and
have the e2e-prepare composite only copy the workspace + run prepare-env.
GITHUB_JOB keeps the e2e and upgrade-e2e sandboxes distinct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…dfs baseline

The etcd v1alpha2 adoption snapshot cannot succeed in an e2e sandbox: it needs a
trusted-cert (ACME) external S3 endpoint, which a sandbox (example.org, no
DNS/ACME, self-signed in-cluster cert) cannot provide — matching the dev10
findings. Set migrations.etcdAdoptSkipBackup=true in the platform Package so
migration 50 runs the real adoption without the infeasible snapshot. Keep
seaweedfs on the root tenant (the historical default install-cozystack.bats also
uses) for a realistic baseline, but drop the fragile cozy-backups-creds wait —
the adoption no longer depends on the seaweedfs S3/bucket/creds chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The composites extracted here were written against the pre-78fc3f917 e2e
job, which copied the checkout to /tmp/$SANDBOX_NAME, ran every step from
there and removed it on teardown. main has since dropped that workspace:
e2e is a single job on an ephemeral runner, so there is no cross-job
rendezvous and the checkout is never reused.

Resolving the rebase in favour of the composites would have reintroduced
it, including the unguarded `rm -rf /tmp/$SANDBOX_NAME` that commit
deliberately deleted rather than guarded — when an upstream step fails the
unconditional `Set sandbox ID` is skipped, SANDBOX_NAME stays empty, and
the `if: always()` removal expands to `rm -rf /tmp/`.

Drop the Prepare/Remove workspace steps and the `cd /tmp/$SANDBOX_NAME`
prefixes from e2e-prepare, e2e-collect and e2e-teardown, and point the
collect and chainsaw-report upload paths at _out/. Same treatment for the
upgrade-e2e job's own two `cd` prefixes.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
main gained a `labeled` pull_request trigger whose plan/resolve guards
discard every label event except `full-e2e`. This lane's documented opt-in
("any PR a maintainer opts in with the upgrade-e2e label") therefore did
nothing after the rebase: adding the label fired a `labeled` event, `plan`
skipped, `finalize` skipped with it, and upgrade-e2e never satisfied its
needs. Before the rebase there was no `labeled` trigger at all, so the label
was only ever read on push — the gap appeared with the new base, not here.

Admit `upgrade-e2e` alongside `full-e2e` in both allowlists so labelling an
open PR starts a run, and use !cancelled() rather than always() in the job
gate, matching the sibling `e2e` job: a cancelled run must not go on to
occupy a 32-vCPU runner for up to 180 minutes.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The previous commit broadened the plan and resolve_assets label gates to admit
upgrade-e2e, but hack/promote-gate-contract.bats pins those two expressions as
exact grep -cF literals. Both counts fell to zero, bats-unit-tests failed, and
because "Unit & controller tests" gates finalize, the required E2E Tests check
and this PR's own upgrade lane were both skipped — the commit meant to make the
label start a run stopped every run instead.

I broke this by running the unit suite before writing that commit rather than
after; pre-commit does not cover bats-unit-tests, so it went unnoticed.

Update both assertions to the broadened expressions, and add a case pinning the
label end to end: admitted by the `labeled` allow-list in both plan and
resolve_assets, honoured by the job's own condition, and named "Upgrade E2E
Test" so it cannot collide with the required "E2E Tests" context and stop being
advisory. Note the label is double-quoted inside the JSON allow-list — matching
the single-quoted form counts zero and asserts nothing.

Verified non-vacuous: narrowing the plan gate back to the full-e2e-only literal
turns exactly one test red.

Also correct the MariaDB seed header, which still prescribed the 127.0.0.1
connection that commit f598c25 replaced — the same file documents that path as
OOM-killing the nano server container.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
`cut -f2` prints the whole line when that line carries no delimiter, so a
single-component target like "v1" came back with min=1 and was resolved as if
it were v1.1 — walking down to the v1.0 line and printing a real-looking
baseline with exit 0. The lane would then install that baseline and report on
an upgrade path nobody asked for, which is worse than failing.

Pass -s so the delimiter-less line is suppressed, min comes back empty, and the
existing parse guard rejects it. Two-component targets are unaffected: the
guard rejects a missing minor, not a missing patch, and v1.6 still resolves.

In practice the lane only ever passes "" or a target derived from a
release-X.Y.Z branch, so this was latent rather than live.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The case I just added was vacuous. _fixture ships no v1.0.x or v1.1.x tag, so
the buggy reading of "v1" as v1.1 walked down to an empty v1.0 line and exited
non-zero for the wrong reason — the assertion held whether or not the defect
was present, which the mutation run showed: reverting -s left all eleven green.

Give the case its own tag set containing v1.0.9, so the buggy path has a real
baseline to return with exit 0 and the case can only pass because the target is
refused. Reverting -s now fails this case and only this case.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The upgrade-e2e label opt-in was restored in "make the upgrade-e2e label
opt-in actually start a run" by naming the label in `plan`'s and
`resolve_assets`' allowlists. Both gates still exist and that commit
still applies, but main has since grown two more places that read the
same label, and the opt-in is not actually live until both agree.

The concurrency key routes a `labeled` event into a separate `-label`
group, documented as the exact complement of `plan`'s guard so that only
a run publishing nothing is moved aside. Once `plan` admits upgrade-e2e
the complement is no longer exact: the run publishes, but sat in its own
group, which is two live publishers of the `E2E Tests` context on one
head SHA — the case the comment there warns produces a later green that
erases a real failure.

`e2e-report` carries the same `labeled` guard for the opposite reason,
to stay in lockstep with `plan`. The hazard it documents is running
while `plan` skipped; the hazard here is the reverse. `plan` overwrites
`E2E Tests` with `pending` at the start of every run it executes and
only `e2e-report` concludes it, so admitting the label to one and not
the other opens a pending required status on an upgrade-e2e label and
never closes it, leaving the PR unmergeable until someone pushes again.

The lane stays advisory. `e2e-report` does not gain upgrade-e2e in
`needs`, so the lane's result never moves the verdict, and on an
upgrade-e2e-labelled run the sibling `e2e` job still supplies the real
one.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/upgrade-e2e-chainsaw branch from 6c78739 to 48ae5be Compare August 15, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files upgrade-e2e Run the release upgrade E2E test (previous minor stable -> this build) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants