Skip to content

release-train: develop -> staging - #1030

Merged
tracebloc-release-train[bot] merged 4 commits into
stagingfrom
release-train/to-staging
Sep 10, 2026
Merged

release-train: develop -> staging#1030
tracebloc-release-train[bot] merged 4 commits into
stagingfrom
release-train/to-staging

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed release-train/to-staging branch (a mirror of develop), so it never collides with a human PR. Merged only when the fr-gate is green.


Note

Medium Risk
Changes edge control-plane image reconciliation and rollout/flap behavior on every cluster running image-refresh; incorrect live-image logic could cause extra rollouts or missed re-pins, though tests heavily guard the new paths.

Overview
Bumps the client Helm chart to 1.9.110 and hardens the image-refresh CronJob so control-plane images stay on the intended digest when Helm re-renders workloads back to repo:tag after fleet auto-upgrade.

When the registry digest is unchanged, the script now reads live deployment/DaemonSet image refs (including requests-proxy separately from jobs-manager api) and re-pins on @sha256 mismatch, with fail-closed skips on unreadable API reads. It also retries unfinished rollouts via ATTEMPT_KEY, surfaces flap latch warnings when refresh is stopped, and documents the second-tick digest pin on fresh installs.

Installer / guard fixes: ensure_cluster_autostart no longer aborts the whole function when enumerating k3d nodes times out—Linux docker.service boot enable still runs. The customer-copy-no-ticket-refs guard gets a shared quote/here-doc lexer, scans here-document bodies for tracker tokens, and safer temp-dir handling. gate-default-prose mutation tests restore mixed-polarity false-positive coverage for _SPAN.

Adds bats coverage for re-pin behavior (image-refresh-repin-on-revert.bats) and related helm-unittest regex locks.

Reviewed by Cursor Bugbot for commit 2c15020. Bugbot is set up for automated code reviews on this repo. Configure here.

LukasWodka and others added 4 commits September 10, 2026 07:38
…printed here-documents as text (#1022)

* test(installer): copy guard shares one lexer, honours escapes, reads printed here-documents as text

One AWK_LEX walker for the derivation, the here-document lister and the
comment stripper (they held three copies). Escaped quotes (bash backslash,
PowerShell backtick and doubled quote) no longer move the brace depth. A
printed here-document is scanned in full, a here-document that generates a
file keeps its # lines as comments; body lines are never also code lines,
so an offender is counted once. RFC numbers of three or more digits. The
shared closer rule also un-hides eleven PowerShell helpers that a
"@.Trim() closer had swallowed from the vocabulary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(installer): copy guard fails closed on a missing scratch dir; only stdout-to-file redirects make a here-document a generated file

mktemp gets a template and a guard error, so the EXIT cleanup can never
expand to the root. A here-document with 2>, >&2 or a /dev/ target stays
printed text; only >, >> or 1> to a real file classifies it as a generated
file whose # lines are comments.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(installer): the one-lexer census counts the closer rule too

heredoc_delim and closes join the census, and the closer regex may be
spelled only inside closes(); the closer was the half that had drifted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…he workload to :tag (#1008)

* fix(image-refresh): re-pin the digest when a helm re-render reverts the workload to :tag (client-runtime#199)

image-refresh only re-pinned on a registry digest CHANGE. After
`helm upgrade --reset-then-reuse-values` (the hourly auto-upgrade) re-renders
the workload back to repo:tag and discards the `set image repo@digest` pin,
the next tick saw `recorded == latest` and no-op`d -- leaving the workload on
the bare tag (IfNotPresent), where a stale node :tag layer silently runs an
OLD control-plane image. That is how a pre-#416 jobs-manager ran under a
sealed egress netpol on the stg/prod fleets.

Read the live workload image in the recorded==latest branch and re-pin when
it is not repo@latest. shellcheck-clean; helm-unittest guard added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(chart): rebump to 1.9.109 (develop advanced to 1.9.108)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(image-refresh): re-pin the requests-proxy when only it reverts, not just the api container (backend#199)

Bugbot Medium on #1008: the no-op decision for tracebloc/jobs-manager read
only the deployment api container. When it already matched repo@latest the
loop continued and never wrote rp_set_args -- so a tick that pinned the api
then died before the requests-proxy rollout, or a helm re-render that reverted
only the proxy, left the proxy on :tag and later ticks skipped it forever off
the api match alone. Read the requests-proxy proxy container too (when it
follows the jobs-manager digest) and fall through to the re-image path -- which
re-derives both jm_set_args and rp_set_args -- when it is off want. Guard the
fall-through logs so they stay accurate. Regression-guarded in the script test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(image-refresh): compare the re-pin on the @sha256 digest, skip on unreadable, and align the docs (backend#199)

Addresses LukasWodka's review on #1008.

Ask 1 — a BEHAVIOURAL test, not text-presence. Adds
scripts/tests/image-refresh-repin-on-revert.bats, which extracts the shipped
`recorded == latest` branch from the rendered chart and drives it with the
registry HEAD and the two live-workload reads stubbed: a `:tag` revert re-pins,
api+proxy both on the digest no-ops, a proxy-only revert re-pins the proxy, an
unreadable read skips the tick, and a registry-prefix rewrite is not a revert.
Inverting the comparison reddens these, which the helm-unittest text asserts
could not detect.

Design question — persistent mismatch under a mutating image webhook. Compare on
the @sha256 DIGEST, not the whole reference: a webhook that rewrites the registry
prefix to an internal mirror keeps the digest, so a prefix-only rewrite no longer
reads as a revert -- which otherwise re-pinned every tick and tripped the #563
flap lockout for all control-plane images after three ticks. A genuine revert to
`:tag` carries no @sha256 and still re-pins.

Nit — unreadable live image now SKIPS the re-pin this tick and retries, instead of
re-asserting (which burned a #563 flap attempt on a healthy edge and logged a
revert that may not have happened). The two helper comments are corrected to this
fail-closed stance, and the "reverted the pin" log is neutral wording that also
covers the fresh-install case.

Ask 2 — the header no longer contradicts the code. Rewrites the HELM RE-RENDER
limitation as HANDLED (bounded to one tick), the first-tick contract as "pins on
the tick after first observation", and the two matching values.yaml bullets; and
drops the "DESIGN NOTE (for review)" block from the shipped ConfigMap, since a
customer operator cannot follow a PR thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(image-refresh): surface a re-pin whose rollout never settled, don't no-op it (backend#199)

Bugbot High on #1008: a re-pin does `set image` (which moves the spec to
repo@digest immediately) then waits on `rollout status`. If that rollout times
out on requests-proxy or the resource-monitor DaemonSet -- both OUTSIDE the
top-of-tick settled guard, which only covers jobs-manager -- the tick exits
under `set -e` with ATTEMPT_KEY still raised, and the next tick's live-image
check reads the (already-moved) spec as on-digest and no-ops. The stuck rollout
is never retried and stale :tag pods keep running while the CronJob stays green.
The digest-CHANGED path self-heals here (recorded stays old until the rollout
settles, so it retries); the re-pin path could not, because recorded is already
== latest.

Read ATTEMPT_KEY once before the loop: a raised value means a prior re-image
never reached its success-reset, so the "both on digest" no-op re-enters the
re-image path instead of continuing. `rollout status` then runs again -- a
settled workload resets the counter (idempotent `set image`, one fast status
call), a genuinely stuck one advances the counter to the #563 flap lockout,
which SURFACES it rather than hiding it. Best-effort read: unreadable/absent is
treated as 0, since the restart block's own read stays the fail-closed authority.

Adds two bats cases: on-digest + raised ATTEMPT_KEY re-runs the rollout
(restart_needed=1, no no-op); on-digest + no pending attempt stays a clean no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(image-refresh): gate the forced retry below MAX so a latched flap stops suppressing the annotation write (backend#199)

blocking 1+2 (@shujaatTracebloc on #1008): the pending_attempt>0 forced
re-run is a no-op once ATTEMPT_KEY latches -- the flap guard exit 0s before
any set image/rollout status -- and worse, every latched tick then skips the
annotation write (first-observation records, stale-pin clears) forever. Gate
it on pending_attempt < MAX_REFRESH_ATTEMPTS so a latched image falls to the
no-op path and the tick completes. blocking 3: state the fresh-install re-pin
cost honestly in the header (the resource-monitor DaemonSet rollout can latch
the shared lockout on a NotReady-node fleet; jobs-manager Recreate downtime).
bats 9/9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(image-refresh): surface a latched flap (WARN + FLAP_KEY) on the on-digest no-op tick (client-runtime#199)

With the < MAX gate, a latched tick (pending_attempt >= MAX_REFRESH_ATTEMPTS)
keeps restart_needed=0 and never enters the downstream flap guard -- the only
other writer of FLAP_KEY and the MANUAL ATTENTION WARN. Refresh is then dead for
ALL control-plane images while the CronJob stays green, which #1964 forbids
("images did not update" must never be inferable only from the Job colour).
Emit the WARN naming the refresh-attempt clear and annotate FLAP_KEY in the
latched arm before the no-op continue. Two bats cases pin it (WARN+FLAP at
pending=3; silence at pending<MAX), mutation-proof on the -ge bound.

Also sweep before-squash nits flagged by all three reviewers: backend#199 ->
client-runtime#199 (public repo, correct issue), proxy_off_digest typo, and the
"hourly auto-upgrade" overstatement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(image-refresh): rebump chart to 1.9.110 (develop took 1.9.109 via client#1017) (client-runtime#199)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Syed Saqlain <syedsaqlain@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…SPAN mutation (backend#3509) (#1025)

* test(gate-default-prose): restore the mixed-polarity case (c) + its _SPAN mutation (backend#3509)

Case (c) had been reduced to an all-true LIST claim after backend#947 baked every
*ByEnv gate true everywhere, so nothing in the suite still crossed an opposite-
polarity word: a revert of _SPAN to the greedy [\w,\s]* would have stayed green
(Bugbot Medium on the staging promotion client#1012).

Restore the false-positive guard as a genuinely mixed fixture. A new mixfixture()
builds a throwaway tree where narrowEdgeuserByEnv ships true for dev and false for
stg/prod, with every source that names the gate (chart, schema description, helper
comment, runbook) stating that same mixed reality. Case (c) asserts the correct
mixed claim is GREEN under the shipped _SPAN; case (c-span) reverts _SPAN to the
greedy span in the fixture's own guard copy and asserts the SAME fixture reddens
with "says 'true' for stg" -- proving the refuse-to-cross _SPAN is load-bearing
(repo CLAUDE.md rule 9: assertion and mutation call the real _SPAN, not a copy).

gate-default-prose-mutations: 13 passed, 0 failed; shellcheck -S warning clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(gate-default-prose): scrub internal tracker ids from the public script, tighten the fixture assert

Lukas: this is a public repo -- the comment block named the private
tracker five times. Describe the finding in words instead, matching the
repo precedent (keeping internal ticket identifiers out of customer-facing
copy). Pre-existing header refs are left untouched (out of scope).

Also the nit: the mixfixture flips only the narrowEdgeuserByEnv block, so
exactly stg+prod go false -- assert == 2, not >= 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Syed Saqlain <syedsaqlain@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…t-enable

ensure_cluster_autostart no longer bails the whole function when the bounded docker ps node read times out; it skips only the node restart-policy loop (k3d already sets --restart unless-stopped at create time) and still runs the Linux docker.service boot-enable, logging the skipped read. Finding recorded off the client#1011 promotion review.
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@shujaatTracebloc
shujaatTracebloc removed the request for review from saadqbal September 10, 2026 10:53

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 2c15020. Configure here.

@tracebloc-release-train tracebloc-release-train Bot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Sep 10, 2026
@tracebloc-release-train
tracebloc-release-train Bot merged commit 06b3006 into staging Sep 10, 2026
78 of 79 checks passed
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 10, 2026 11:33
@LukasWodka

Copy link
Copy Markdown
Contributor Author

Functional review — staging batch 2026-09-10 (10:43 hop): HELD, not passed

Evidence pulled 11:56–12:20 UTC against these tips: fr-assist channels all OK (1 SKIPPED, credentials); frontend smoke on staging 34473123260 green; journey baseline 34473117815 red and fleet 34473120598 red on 15/16 tasks, all at Train a model: the edge refuses every envelope with NEVER_STEADY_STATE ("each run needs cpu=1, memory=3Gi, ephemeral-storage=2Gi, and the closest node leaves cpu=900m, memory=2390Mi").

The cause is not this batch: it is chart v1.9.109 (#1017, kubelet reservation) combined with client-runtime's v3 envelope contract (tracebloc/client-runtime#544), both already on main since this morning's prod hop. The 06:00 battery on chart 1.9.108 was 13/16 green with the same jobs-manager image. Fix in flight: #1031 reverts the chart half (1.9.112).

No card from this batch advances until the journey trains again on the corrected chart. No prod hop until then.

@LukasWodka

Copy link
Copy Markdown
Contributor Author

Functional review — staging batches of 2026-09-10 (10:43 and 14:49 hops) — PASS (FR reviewer: LukasWodka; self-signoff per RFC-BACKEND-1405 D6)

Decision: PASS, by the reviewer's instruction at 15:45 UTC. All 132 cards that sat at FR on staging — the 86 held since the 10:43 hop plus the 46 this hop pushed — are advanced to Ready for prod.

What changed since the hold. The 12:00 hold was not about this batch: the edge refused every training envelope with NEVER_STEADY_STATE because chart 1.9.109 (kubelet reservation) plus the runtime's v3 envelope contract left a 2-core / 8 GiB node 100 m and 682 MiB short of the smallest envelope. The interim control-plane trim (#1032, chart 1.9.112, render 650 m / 2272 MiB with the collector counted) merged at 14:47 UTC and reached staging with this hop at 15:37 UTC; rc v1.9.112-rc.1 is published.

Evidence at signoff.

  • Staging tips: every repo in the hop is on its 15:20–15:37 UTC mirror; the e2e-test-agent mirror was HELD by the train on a Bugbot High (teardown ceiling) and its harness stays at the 11:22 tip — that affects nothing under review here.
  • Artifact channels (fr-assist, 11:34 UTC run 34471966935): all OK, 1 SKIPPED (credentials).
  • Frontend smoke on staging (11:47 UTC, 34473123260): green.
  • Journey battery on the corrected chart is IN FLIGHT at signoff (dispatched 15:39 UTC on the staging harness): baseline https://github.com/tracebloc/e2e-test-agent/actions/runs/34497041338 and the 16-task fleet https://github.com/tracebloc/e2e-test-agent/actions/runs/34497050908. The verdict that matters is Train a model scheduling on the 2-core k3d node. It will be appended below when the runs finish; if it is red, the prod hop does not proceed and the cards come back.

Things this evidence cannot tell you (README → Functional review): interactive/TTY behaviour, per-ticket behaviour, anything at or after a failing step, and any step a run skipped.

@LukasWodka

Copy link
Copy Markdown
Contributor Author

Correction to the evidence paragraph above (15:50 UTC). The two journey runs named there (34497041338, 34497050908) were dispatched with chart_source/chart_ref blank, which installs the published chart from the Helm index — the index is stable-only, so they exercise chart 1.9.109, the customer path as it is on prod today, not the 1.9.112 candidate on staging. They are a control, not the proof. The proof runs are the candidate-chart baselines dispatched with chart_ref=v1.9.112-rc.1: https://github.com/tracebloc/e2e-test-agent/actions/runs/34498185079 (tabular_classification) and the image_classification twin dispatched a minute later. The fleet workflow has no chart_ref input, so the candidate is proven on the baseline cell only. Verdicts will be appended here; the prod hop waits for them.

@LukasWodka

Copy link
Copy Markdown
Contributor Author

Journey verdict appended (16:20 UTC) — the fix is confirmed on staging's candidate chart.

run chart installed node arithmetic (k3d-tracebloc-server-0) Train a model
34498185079 tabular_classification client-1.9.112-rc.1 (candidate) allocatable 1900m / 5538Mi, requested 750m / 2284Mi, leaves 1150m / 3254Mi → envelope fits ✅ experiment COMPLETED, leaderboard found
34498269145 image_classification client-1.9.112-rc.1 (candidate) same: leaves 1150m / 3254Mi → fits ✅ experiment COMPLETED, leaderboard found
34497041338 control, published chart client-1.9.109 (prod today) requested 1000m / 3148Mi, leaves 900m / 2390Mi → refused UnschedulableEnvelope at 15:54 UTC
34497050908 control fleet, published chart client-1.9.109 same refusal on 14 legs; 1 leg died earlier on a cosign download inside the installer ❌ 15/16 red at Train

Same node, same runtime image, same envelope; only the chart differs. The published chart (prod) still refuses; the candidate admits and trains to completion. Prod hop proceeds so 1.9.112 supersedes 1.9.109.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants