Skip to content

release-train: staging -> main - #441

Merged
tracebloc-release-train[bot] merged 9 commits into
mainfrom
release-train/to-main
Aug 3, 2026
Merged

release-train: staging -> main#441
tracebloc-release-train[bot] merged 9 commits into
mainfrom
release-train/to-main

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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


Note

Medium Risk
Installer edits user shell rc files on every curl | sh install (high blast radius if wrong), offset by extensive harness tests; release and secret-scan workflow changes affect publishing and CI gates.

Overview
Promotes 0.10.2 and ships the largest functional change in install.sh: shell rc PATH handling is rewritten so repeated installs or prefix changes replace a single tagged block instead of stacking entries (#433), with conservative ownership checks so dangling markers cannot delete user PATH lines (#434), clearer outcomes when tidy-up fails, and fish fish_add_path parsing fixes (spaces, multi-arg, inline comments — cli#439).

Release workflow drops the free-form dispatch tag input: rebuilds must run at the v* tag ref (new Ref guard job), and tag names are passed via env in shell steps to avoid injection (R8). Code quality turns soft-fail: false, adds .gitleaks-baseline.json, and supports manual whole-repo scans via workflow_dispatch.

Also updates the PR template (breaking change, deployment notes, cross-repo checklist) and extends install-verify.sh with sandbox $HOME and broad PATH regression tests.

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

LukasWodka and others added 8 commits July 29, 2026 19:29
…d#1303) (#435)

Backlog at zero fleet-wide; the quality contexts are already required on
develop. Also adds a workflow_dispatch(all-files) trigger for whole-tree
scans (gitleaks baseline).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… second (#433) (#434)

* fix(install): own ONE PATH block in the shell profile, never append a second (#433)

`scripts/install.sh` guarded its profile append with "does the rc mention
$PREFIX", so an install with a DIFFERENT prefix appended another block every
time and the profile grew without bound. The v0.10.1 validation box collected
TEN blocks, each naming a temp dir that no longer existed.

The polluter was our own test harness: scripts/tests/install-verify.sh mints a
fresh mktemp --prefix per case and never sandboxed $HOME, so it wrote 3 blocks
into the developer's real ~/.bash_profile per invocation.

- Tag our block with a stable $PATH_MARKER and REPLACE it on a re-run instead of
  stacking a second one. A profile already polluted by an older installer
  collapses to one block on the next install.
- Decide "is $PREFIX already handled?" from the rc with our block stripped out,
  so the answer comes from the user's own lines only — that is what makes the
  write idempotent.
- Compare whole path COMPONENTS, not substrings. `grep -F "$PREFIX"` matched
  --prefix /opt/tb against an existing /opt/tb2 line and then claimed "already in
  your PATH config" for a directory that was on nobody's PATH.
- Append when there is nothing of ours to clean up; only rewrite the file when a
  stale block must go. The rewrite truncates in place, so the inode, mode and
  owner survive and an rc symlinked into a dotfiles repo is written THROUGH
  rather than replaced by a regular file.
- Removal only takes the line under the marker when it is shaped like a PATH op
  we wrote, so a dangling marker can never eat unrelated user content.
- Quote the fish line (`fish_add_path "$PREFIX"`), matching the client
  installer's hint, so a prefix containing a space survives.
- Sandbox $HOME in install-verify.sh and add 7 assertions: same prefix x3 → one
  block; three different prefixes → one block naming the newest; prefix already
  on PATH → no rc written at all; unrelated lines preserved byte-for-byte;
  dangling marker harmless; zsh and fish route to the right rc.

A prefix already on $PATH still writes nothing (unchanged), and a $HOME prefix
is still persisted even when it looks on-PATH — that hit can be session-only
(Bugbot #392 r2), and it now costs at most one line rather than one per run.

Fixes #433

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

* fix(install): a same-prefix re-run must not rewrite the rc at all

The first pass consolidated correctly but reached the replace path even when our
one block already said exactly what this run would write — so a re-install, and
every `tracebloc upgrade` (which re-execs this installer), rewrote the user's
profile byte-for-byte and reported "Updated the tracebloc PATH entry".

Recognise that case and leave the file completely alone: count our blocks, and
when there is exactly one whose PATH line already matches, report `present`
("already in your PATH config — nothing to add") without touching the rc.

Asserted: re-install with the same prefix leaves the rc byte-identical.

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

* fix(install): restore the rc if the rewrite dies partway

The redirection in replace_rc truncates before cat writes, so a write that died
halfway (no space left, a vanishing mount) would leave the user's profile in
pieces. Put the original contents back on failure — best effort, but far better
than a half-written file we don't own. The caller already reports `failed` and
prints the line to add by hand.

Verified with a read-only rc: the file keeps its user content and its previous
block, and the manual-add advice is printed.

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

* fix(install): require positive proof a PATH line is ours before removing it (Bugbot)

Bugbot on #434: the marker alone was treated as proof of ownership, so a marker
left dangling by a hand-edit directly above the USER's own PATH export would take
that export with it on the next install — silently deleting a PATH entry we never
added, while still reporting success.

Removal now demands positive evidence about the directory the following line
names (_tb_owns_dir):

  * a '$' anywhere      -> never ours. We always write a literal, expanded path;
                           "$HOME/mytools" is the user's own idiom. This kills the
                           most realistic form of the bug.
  * == the prefix we are installing to now          -> ours
  * a directory that no longer exists               -> ours (the #433 cruft)
  * a directory still holding a tracebloc binary    -> ours (a prior install)

Anything else is the user's line: we drop only the orphaned marker comment and
leave their PATH op alone. The bias is deliberate — for an installer editing a
file it does not own, failing to clean one line is much cheaper than deleting a
PATH entry someone depends on. The residue is bounded: at most one unmarked line
per pathological cycle, never renewed growth.

The pair-then-verdict pass goes through a temp file rather than a pipe (so the
verdicts land in this shell) and rather than a heredoc (so the awk program needs
no nested-expansion escaping).

3 new assertions, 23/23 in install-verify.sh: a dangling marker above the user's
own PATH export keeps it, likewise with an unexpanded $HOME, and a block naming a
vanished directory is still cleaned up.

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

* style(install): state the empty-prefix exit status outright

Bugbot read the bare `exit` in the BEGIN rule as exiting 0, i.e. 'prefix
already listed', which would skip persistence. It does not: a bare exit in
BEGIN still runs END, so the status is END's exit(found ? 0 : 1) = 1, the
fail-safe direction. Verified 1 on BSD awk and by the POSIX rule.

Making it `exit 1` costs nothing and means the intent cannot be misread by
a later reader or a stricter awk, rather than resting on the fallthrough.

* fix(install): prove ownership from the marker, and stop mislabelling a failed tidy-up (Bugbot)

Two Medium findings, both in the ownership/reporting logic.

1. _tb_owns_dir claimed ANY non-directory as ours, so a dangling marker above the
   user's own literal-path line for a directory they hadn't created yet was
   stripped as an owned block — deleting a PATH entry we never wrote, which is not
   recoverable.

   The marker we write now records the directory:

     # Added by the tracebloc CLI installer (prefix: /opt/tracebloc)
     export PATH="/opt/tracebloc:$PATH"

   When the recorded prefix and the directory on the line below agree, we provably
   wrote both halves, so the block is reclaimable even if that directory has since
   been deleted — which is what keeps the #433 cleanup working, ten blocks and all.
   $PATH_MARKER stays the stable BEGINNING of the line (matched literally via
   index(), never as a whole line) so older blocks are still recognised.

   A legacy marker that recorded nothing can now only be claimed when the
   directory still holds a tracebloc binary, or is the prefix being installed to.
   A legacy marker over a vanished directory is indistinguishable from the user's
   own entry for a directory they plan to create, so we leave it alone. That gives
   up auto-healing pre-#433 cruft in exactly one case; Lukas's profile was already
   cleaned by hand, so that is a nice-to-have, whereas deleting someone's PATH line
   is not.

   Blocks we cannot claim are now left ENTIRELY intact, comment included, rather
   than losing their marker: a dead line still labelled "Added by the tracebloc CLI
   installer" tells the user what it is, a bare one doesn't.

2. A failed replace_rc reported `failed` even when the user's own line already
   persisted the prefix and all we'd failed to do was drop a redundant block — so
   the installer told them to hand-edit their profile while their PATH was in fact
   correct. Split into `tidied`/`tidy_failed` (PATH is right; at most a cosmetic
   note) versus `failed` (prefix genuinely not persisted; manual instruction
   warranted), and the write decision now hinges on content equality (rc_same), so
   a run that changes nothing writes nothing.

   rc_same deliberately avoids cmp(1) — it lives in diffutils, which a minimal
   container image can lack, and a missing tool must not silently turn "leave the
   file alone" into "rewrite it every run".

Also restores the `mkdir -p "$(dirname "$rc")"` that the state-machine rewrite
dropped; without it fish's ~/.config/fish/config.fish could never be created. The
matrix caught it.

install-verify.sh: 29/29, with six new cases — legacy marker can't claim a missing
dir, nor one without our binary; a recorded-prefix block naming a vanished dir is
still cleaned; ten of them collapse to one; an unclaimable legacy block is kept
without churning the rc; a failed tidy-up never asks for a manual PATH line, while
a real failure still does.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…egrity (#429)

* fix(release): rebuilds must run at the tag ref (Bugbot on promotion #428)

Cosign's keyless identity embeds the RUN's ref. workflow_dispatch took an
inputs.ref and could build a tag from a branch run, publishing signatures
(@refs/heads/...) that the tag-anchored installers reject on every customer
machine. inputs.ref removed; dispatch runs now hard-fail unless started at
a v* tag ref; rebuild paths = rerun the tag run or dispatch at the tag.

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

* review: pre-flight guard job + refs via env (Asad's nits on #429)

Guard moved out of the 8-way matrix into a tiny job that release needs:
a branch-misdispatch now fails once in seconds instead of burning eight
runners' setup. Refs passed via env, never interpolated -- git permits
$/backticks in tag names, so a crafted v* tag would otherwise execute on
the runner (R8, same rule as the client installer workflows).

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

* fix(release): pass the tag ref via env in Determine-release-tag (R8)

The one step this PR's hardening missed: it still did REF="${{ github.ref_name }}",
interpolating an attacker-controllable tag name straight into the shell, so a
crafted v* tag with backticks or $() would execute on the publish runner
before the release is created. Now passed as env REF_NAME and read as $REF_NAME,
matching the guard and version steps.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(install): match a quoted fish path, spaces included

rc_lists_dir field-split fish_add_path arguments on whitespace. Since #434
started writing fish_add_path "$PREFIX", a prefix containing spaces became
two fields, matched neither, and the installer appended a SECOND block
directly beneath an existing line naming the same directory -- while
reporting that it had added a PATH entry that was already there.

The read path now parses quotes like the ownership reader further up the
file already does: a quoted argument is one path, spaces included, while an
unquoted line still splits so several bare paths on one line keep working.
Single quotes are handled too, which the old code also missed.

Verified in both directions: the two new harness cases fail against the
current installer (29 passed / 2 failed) and pass with this change
(31 passed / 0 failed).

Found by Bugbot on the develop->staging promotion (cli#438).

* fix(install): take the FIRST fish_add_path, not the last

A greedy /^.*fish_add_path/ strips through the LAST occurrence on the
line, so an inline comment mentioning fish_add_path left the comment text
as the path and missed the real argument -- reintroducing the exact bug
this branch fixes. index() takes the first occurrence instead.

Also restores the leading-whitespace strip that the greedy regex used to
consume: without it substr() left ' "/path"', so the quote check saw a
space and fell back to field-splitting, breaking the spaced-path fix.
Caught by re-running the whole case set rather than only the new one.

Bugbot, cli#439.

* fix(install): take the FIRST fish_add_path, not the last

A greedy /^.*fish_add_path/ strips through the LAST occurrence on the
line, so an inline comment mentioning fish_add_path left the comment text
as the path and missed the real argument -- reintroducing the exact bug
this branch fixes. index() takes the first occurrence instead.

Also restores the leading-whitespace strip that the greedy regex used to
consume: without it substr() left ' "/path"', so the quote check saw a
space and fell back to field-splitting, breaking the spaced-path fix.
Caught by re-running the whole case set rather than only the new one.

Bugbot, cli#439.

* fix(install): tokenise the fish argument list properly

Replaces three rounds of patching with one quote-aware tokenizer, because
each patch fixed its own case and broke or missed another:

  * greedy .* strip lost the real argument to an inline comment
  * index() alone dropped the leading space, so the quote check failed
    and spaced paths regressed
  * taking only the first quoted argument missed the prefix when
    fish_add_path lists several directories

The loop now walks the argument list: quoted tokens are one path (spaces
included), bare tokens split on whitespace, flags are skipped, and a
trailing # comment ends parsing. 18 direct variants and 33 harness cases
green.

Bugbot, cli#439.

* fix(install): tokenise the fish argument list properly

Replaces three rounds of patching with one quote-aware tokenizer, because
each patch fixed its own case and broke or missed another:

  * greedy .* strip lost the real argument to an inline comment
  * index() alone dropped the leading space, so the quote check failed
    and spaced paths regressed
  * taking only the first quoted argument missed the prefix when
    fish_add_path lists several directories

The loop now walks the argument list: quoted tokens are one path (spaces
included), bare tokens split on whitespace, flags are skipped, and a
trailing # comment ends parsing. 18 direct variants and 33 harness cases
green.

Bugbot, cli#439.
Records a redacted gitleaks baseline at the repo root so the full-history
dispatch scan runs clean, and wires the code-quality caller to consume it
via the gitleaks-baseline input. Part of tracebloc/backend#1303.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread scripts/install.sh Outdated
@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 Jul 31, 2026
@LukasWodka LukasWodka self-assigned this Aug 1, 2026
@LukasWodka

Copy link
Copy Markdown
Contributor Author

This finding is already fixed on staging — the mirror is stale

The Bugbot finding on this PR is real for the content this PR shows, but the fix is already on the source branch. Do not fix it again here.

release-train/to-main is frozen at 5682bc8 and is 4 commits behind staging (61323b3). One of the four missing commits is the fix itself:

61323b3 Merge pull request #442 from tracebloc/release-train/to-staging
be16f6f chore: re-baseline the PR template on the org one (#440)
45b2a29 chore: add redacted gitleaks history baseline (#436)
23c12fd fix(install): match a quoted fish path, spaces included (#439)   ← the fix

Verified by content, not by commit message — scripts/install.sh on origin/staging and origin/develop both contain the quote-parsing implementation (Parse quotes instead of field-splitting, citing cli#439); origin/release-train/to-main and origin/main do not. develop and staging are level (0 commits apart).

Correct action: re-run stage=prepare for cli. That force-updates the mirror to staging's head, the PR then carries #439, and Bugbot re-reviews clean. Writing another fix on this branch would duplicate #439 and produce a conflict at the next hop.

The reusable finding here

Nothing on this PR says the mirror is behind its source. A reviewer sees old content, Bugbot correctly flags a bug against it, and the natural response is to fix an already-fixed bug — on the promotion branch, which is precisely where the train says fixes must not go.

This is the second instance today. tracebloc-py-package#352 is the same shape: its mirror sits at 6d99025 while staging has moved to 2a8b8cf, and it has been stuck since 2026-07-29.

Worth a guard rather than a habit: settle could report how far $src has advanced past the frozen mirror, and refuse (or at least warn loudly) when the delta is non-empty — "you are about to promote content that is not what staging contains". Filing that against backend#1414's lane, since it is the same "the train knew and did not say" class.

Posted by an automated review sweep over the PRs in Code review.

@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@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 Aug 1, 2026
@LukasWodka

Copy link
Copy Markdown
Contributor Author

Correction to my earlier comment — a prepare re-run alone will not unblock this

gate / gate has now gone red, and it is not a defect. It is the FR gate doing exactly its job:

❌ #436 — Status='FR on staging', required 'Ready for prod' or later
❌ #439 — Status='FR on staging', required 'Ready for prod' or later
❌ #440 — Status='FR on staging', required 'Ready for prod' or later

Those are precisely three of the four commits the mirror is missing — and #439 is the fish-path fix whose absence produced the Bugbot finding above. So the whole picture is one coherent state, not three separate problems:

staging carries #436, #439, #440 · their cards are still in FR on staging · so the gate correctly refuses to promote content nobody has functionally reviewed · and the mirror, frozen before they landed, shows a reviewer the old code and earns a Bugbot finding for a bug #439 already fixed.

I said earlier that the fix was to re-run stage=prepare. That was incomplete — it refreshes the mirror and clears the Bugbot finding, but the gate would still refuse. Correct order:

  1. /fr-pass on chore: add redacted gitleaks history baseline #436, fix(install): match a quoted fish path, spaces included #439 and chore: re-baseline the PR template on the org one #440 (or drag each card to Ready for prod). This is a human FR decision and is the actual blocker.
  2. Re-run stage=prepare so the mirror picks up all four commits.
  3. Then settlerelease.

One thing worth doing before step 1

Merge tracebloc/.github#124 first. The /fr-pass exit-code fix (backend#1413) is merged to .github's develop but not yet on main, and every caller pins @main — verified: the self-recognition marker is present on develop and absent on main.

So right now, if /fr-pass refuses any of those three for any reason — not on the board, wrong column, unauthorised — it exits 0 and reports success. That is the exact failure that caused three cards to be advanced by hand on 2026-08-01, overriding a control that had correctly refused. Promoting #124 first makes the three sign-offs you are about to do honest.

@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 61323b3. 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 Aug 2, 2026
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@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 61323b3. 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 Aug 2, 2026
@tracebloc-release-train
tracebloc-release-train Bot merged commit 1f20a94 into main Aug 3, 2026
110 of 113 checks passed
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-main branch August 3, 2026 05:18
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.

1 participant