Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9d1ef08
feat(interactive): the guided `?` line carries a label, not just an a…
LukasWodka Aug 17, 2026
a360536
fix(sanitize): strip SS3 escapes and floor escape-only names (#516) (…
LukasWodka Aug 17, 2026
b81e5f9
fix(cli): diagnose before advising — a wrong active-client pointer ha…
LukasWodka Aug 17, 2026
f9a2521
fix(auth): a transient poll failure retries; the expiry copy names th…
LukasWodka Aug 17, 2026
465c880
fix(test): the copy catalog skipped every message written as a join (…
LukasWodka Aug 17, 2026
f9f1328
Merge pull request #524 from tracebloc/release-train/to-staging
tracebloc-release-train[bot] Aug 18, 2026
3b88a85
sec(install): Windows verified the signature only if you already had …
LukasWodka Aug 18, 2026
885c5c3
feat(telemetry): one outcome event per command, with no channel for a…
LukasWodka Aug 18, 2026
df1daa9
fix(install.ps1): verify-blob must not inherit a stale exit code (#529)
LukasWodka Aug 18, 2026
d18a520
feat(2036): add-to-kanban authenticates as the App (backend#2036) (#525)
LukasWodka Aug 18, 2026
7f1fc28
fix(telemetry): don't file a run signed into an unknown env under pro…
LukasWodka Aug 19, 2026
d443509
fix(install.ps1): SET the TLS 1.2 floor, don't OR it onto the default…
LukasWodka Aug 19, 2026
a1002cc
Merge pull request #528 from tracebloc/release-train/to-staging
tracebloc-release-train[bot] Aug 19, 2026
fcef630
sec(2157): scope the add-to-kanban App token to least privilege (#533)
LukasWodka Aug 20, 2026
c28c120
ci(2212): the fixtures drift check must fail when it cannot run (#536)
LukasWodka Aug 20, 2026
6f66e87
Merge pull request #535 from tracebloc/release-train/to-staging
tracebloc-release-train[bot] Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .cursor/BUGBOT.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,6 +73,27 @@ Two things make this repo unusual and should shape every finding:
(`internal/api/client.go`, `nextPath`). Where "empty" and "unknown" are different answers,
prefer a three-valued return (`internal/cluster/discover.go:302`).

- **"Couldn't confirm" used as "confirmed" — in a BRANCH, not just a return type.** The rule
above is about the value a function hands back; this is about the `if` that consumes it, and
it is the defect this repo produces most often. cli#515 shipped it three times in one PR,
each in a different file, each after the previous one was fixed: a failed cluster scan
reported as "no client is running here"; an empty `cluster_id` on a legacy client read as
"runs elsewhere" (`ProvisionedClient.ClusterID` is documented empty on not-yet-backfilled
records); and `reachStateOf(x) != ReachNoEnv` used to mean "an environment is here", when
`ReachState` also has `ReachUnreachable` and `ReachError`. Two concrete shapes to flag:
- **A negated comparison against ONE member of a multi-valued enum.** `!= ReachNoEnv`,
`!= StatusFail` and friends silently include every member added later. Compare against the
member you actually require (`== ReachOK`), and derive the test's input domain from the
enum's declared surface — mutation coverage cannot see a vocabulary gap.
- **A lenient "not found" default reused where the question is "may I believe this?"**
`reachStateOf` returns `ReachOK` for an ABSENT check, which is right for a verdict roll-up
and wrong for authorising a claim — hence the separate `reachConfirmedOK`
(`internal/cli/doctor.go`). The same default is rarely correct for both.

The customer-visible cost is never a wrong log line: on #515 each instance ended in advice to
run `client create` on a cluster nothing was confirmed on, where it MINTS rather than adopts —
i.e. the guidance manufactured the orphaned phantom of `backend#970`.

- **A cross-repo contract change that only lands on one side.** `scripts/.data-ingestors-ref`,
`scripts/.client-ref` and `scripts/.backend-ref` pin upstream refs deliberately so an
unrelated upstream commit can't red every open PR. Flag a hand-edit to a generated artifact
Expand Down
78 changes: 77 additions & 1 deletion .github/workflows/add-to-kanban.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,84 @@ on:
jobs:
add-to-project:
runs-on: ubuntu-latest
# NO GITHUB_TOKEN AT ALL (saadqbal, #2181). Every call in this job authenticates
# as the App, so the workflow token needs nothing -- and an empty grant is the
# only version of that claim a reader can check. Free, and it means the least-
# privilege story covers both credentials in the job rather than just the loud one.
permissions: {}
steps:
# Board writes authenticate as the tracebloc-release-train App (backend#2036),
# not a human's PAT. `owner:` yields an ORG-scoped installation token; a
# repo-scoped one cannot write the org project. No fallback to the PAT: a
# fallback would let a broken App path keep working silently.
#
# This workflow also fires on DEPENDABOT PRs, which GitHub gates on a separate
# secret scope -- both app secrets are set there too, or Dependabot PRs would
# stop reaching the board with `Input required and not supplied`.
- name: Mint an installation token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_TRAIN_APP_ID }}
private-key: ${{ secrets.RELEASE_TRAIN_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
# SCOPED TO THIS REPO, or the two reads below land org-wide (saadqbal,
# #2181). `owner:` alone does not narrow anything -- run 32239403796 says
# so in as many words: "Input 'repositories' is not set. Creating token for
# all repositories owned by tracebloc." A token calling itself
# least-privilege while carrying issue+PR read across all 19 installed
# repos is the claim this PR exists to stop making.
#
# `organization_projects` is an ORG-level permission and is not affected by
# repo scoping, so the board write should be unchanged -- but that is an
# assumption, and it is the same class of assumption that broke the first
# attempt, so the verification run is what settles it rather than this
# comment. If it is wrong the failure is LOUD (see below), which is what
# makes trying it cheap.
repositories: ${{ github.event.repository.name }}
# Least privilege (backend#2166): without any `permission-*` the token
# carries the App's FULL installation grant. actions/add-to-project needs
# THREE scopes, not one: it must RESOLVE the triggering issue/PR node
# before it can add it to the board, so it needs read on both content
# types (this workflow fires on `issues` and `pull_request`) in addition
# to the project write. Projects-write alone leaves the node unresolvable
# -- the add fails with "Could not resolve to a node with the global id".
#
# WHAT IS ACTUALLY DEMONSTRATED, and what is not. Stated narrowly because
# two earlier versions of this paragraph each overclaimed in a different
# direction, and this text is copied verbatim into 17 repos -- a wrong
# argument here is a wrong argument 17 times, in a byte-compared file
# nobody re-derives.
#
# DEMONSTRATED: a MISSING READ scope fails loudly. Run 32239403796 on this
# branch, at commit 218f0b13 (projects-write only), errored with
# `Could not resolve to a node with the global id` and the job went RED --
# `add-to-project` routes GraphQL errors through `setFailed`.
#
# NOT DEMONSTRATED: the case the FIRST version of this comment described --
# the token resolving the node fine and then 403ing the BOARD WRITE. No run
# has ever produced it. So "fails loudly" is proven for the read scopes and
# is an expectation, not a measurement, for the write.
#
# AND ONE RUN THAT LOOKED LIKE EVIDENCE IS NOT (aptracebloc). The previous
# wording cited run 32237283072 as a second scope failure. It is not one:
# it ran on `develop`, whose file at that sha passes NO `permission-*` at
# all (the App's full grant), and it failed on
# `Could not resolve to a node with the global id of I_kwDONNfQt88...` --
# a node a fully-privileged token also cannot see, i.e. an issue that no
# longer exists (this workflow fires on `issues: transferred`). Run
# 32237067262, the SAME develop sha, succeeded 2m34s earlier. A dead node,
# not a permission.
#
# The proof this is right is therefore still a LANDED CARD, not a passing
# mint: a mint can succeed with a scope the board write then needs and
# lacks, and that is the one path nothing here has exercised.
permission-issues: read
permission-pull-requests: read
permission-organization-projects: write

- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/tracebloc/projects/2
github-token: ${{ secrets.PROJECTS_KANBAN_TOKEN }}
github-token: ${{ steps.app-token.outputs.token }}

76 changes: 72 additions & 4 deletions .github/workflows/backend-fixtures-drift.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,24 @@ name: Backend fixtures drift
# org Actions secret. Until that secret exists the job SKIPS with a warning
# instead of failing: the gate is inert, not red (same activation model as
# the public PII gate's denylist secret).
#
# THAT ACTIVATION PHASE IS OVER. The secret has existed since 2026-08-05 and
# this check is being armed as a required context (backend#2212), so the
# skip-with-a-warning path is now a fail-open: a *required* check that exits 0
# when it cannot run is worse than an advisory one, because it also looks
# solved. It now FAILS instead -- see the step below for the three-way split
# and why a public repo needs an override label rather than a silent pass.

on:
push:
branches: [develop, main]
pull_request:
branches: [develop, main]
# labeled/unlabeled so the skip-fixtures-drift override actually re-runs the
# gate. The default types are opened/synchronize/reopened, so without these
# applying the label would change nothing until the next push -- exactly the
# bug Bugbot caught on version-bump-gate-caller.yml's skip-version-gate.
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
workflow_dispatch:

permissions:
Expand All@@ -42,13 +54,69 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Three-way, because this repo is PUBLIC and the two reasons the token can
# be missing are not the same thing:
#
# token present -> run the check (the normal path)
# absent, Dependabot PR -> PASS with a ::notice::. Dependabot runs get
# DEPENDABOT-scoped secrets, never Actions
# secrets, so the token is empty on them
# however correctly it is set -- and unlike a
# fork, head.repo.fork is FALSE, so they would
# otherwise land in the misconfiguration branch
# below and block every security bump (Bugbot,
# averaging-service#367). Safe to defer here:
# a dependency bump cannot alter
# internal/api/testdata/*.json, and if it did,
# internal/api/contracts_test.go replays every
# fixture through the real decode paths under
# the REQUIRED `Test` check, with no token.
# Divergence from the pinned backend ref is
# re-checked by the push run on develop/main,
# where Actions secrets are available. Set the
# token as a Dependabot secret too and this
# branch stops being reached.
# absent, fork PR -> FAIL. GitHub does not expose repo secrets to
# forks by design, so the check genuinely
# cannot run. A maintainer verifies the
# fixtures by hand and applies
# `skip-fixtures-drift`, which is a permanent
# artifact on the PR -- the same model as
# skip-fr-gate. Passing forks silently would
# fail open on exactly the contributions that
# warrant the most scrutiny.
# absent, not a fork -> FAIL. The secret was removed, rotated or
# expired. That is a misconfiguration, and it
# used to read as a clean run.
#
# Every ${{ }} goes through env:, never into the run: body.
- name: scripts/sync-backend-fixtures.sh --check
env:
BACKEND_CONTRACTS_TOKEN: ${{ secrets.BACKEND_CONTRACTS_TOKEN }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork || false }}
# The PR author, not github.actor: stable across re-runs of the same PR.
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
OVERRIDE: ${{ contains(github.event.pull_request.labels.*.name, 'skip-fixtures-drift') }}
run: |
if [ -z "$BACKEND_CONTRACTS_TOKEN" ]; then
echo "::warning::BACKEND_CONTRACTS_TOKEN is not set — skipping the backend fixtures drift check." \
"Add a read-only (Contents: read) token for tracebloc/backend as a repo/org Actions secret to activate this gate."
set -uo pipefail

if [ "$OVERRIDE" = "true" ]; then
echo "::warning title=Backend fixtures drift OVERRIDDEN::The skip-fixtures-drift label is applied, so this gate did NOT verify internal/api/testdata/*.json against tracebloc/backend. Whoever applied the label is asserting they checked the fixtures by hand. The label stays on the PR as the record."
exit 0
fi
./scripts/sync-backend-fixtures.sh --check

if [ -n "$BACKEND_CONTRACTS_TOKEN" ]; then
exec ./scripts/sync-backend-fixtures.sh --check
fi

if [ "$PR_AUTHOR" = "dependabot[bot]" ]; then
echo "::notice title=Backend fixtures drift deferred::Dependabot runs receive Dependabot-scoped secrets, not Actions secrets, so BACKEND_CONTRACTS_TOKEN is empty here by design. A dependency bump cannot alter internal/api/testdata/*.json, and internal/api/contracts_test.go replays every fixture through the real decode paths under the required Test check without needing a token. Drift against the pinned backend ref is re-checked by the push run on develop/main. Set BACKEND_CONTRACTS_TOKEN as a Dependabot secret too for full coverage here."
exit 0
fi

if [ "$IS_FORK" = "true" ]; then
echo "::error title=Backend fixtures drift could not run::This PR is from a fork, and GitHub does not expose BACKEND_CONTRACTS_TOKEN to forks, so the vendored fixtures could not be checked against tracebloc/backend. A maintainer must verify internal/api/testdata/*.json by hand and apply the 'skip-fixtures-drift' label. Refusing to report green on a check that did not run (backend#2212)."
else
echo "::error title=BACKEND_CONTRACTS_TOKEN is missing::The secret is not readable on this run, so the backend fixtures drift check did not execute. It is expected on develop/main PRs from this repo -- if it was rotated or removed, restore a read-only (Contents: read) token for tracebloc/backend as a repo/org Actions secret. This step used to exit 0 here, which reported a check that never ran as a passing one (backend#2212)."
fi
exit 1
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,8 +55,15 @@ jobs:
shellcheck --shell=bash --severity=error scripts/check-tool-pins.sh
dash -n scripts/install.sh
bash -n scripts/tests/install-verify.sh
shellcheck --shell=bash --severity=error scripts/tests/install-ps1-verify.sh
bash -n scripts/tests/install-ps1-verify.sh
- name: Verification harness (mandatory cosign / fail-closed)
run: bash scripts/tests/install-verify.sh
# Same property on Windows (backend#2078). pwsh is preinstalled on the
# ubuntu runner image; the harness FAILS rather than skips if it isn't,
# since "cannot tell" is not evidence that verification is mandatory.
- name: Verification harness — Windows (mandatory cosign / fail-closed)
run: bash scripts/tests/install-ps1-verify.sh

test:
timeout-minutes: 15
Expand Down
29 changes: 25 additions & 4 deletions STYLE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,10 +55,31 @@ flow) use one uniform rhythm so every question reads the same:
- **A result that belongs to an answer attaches to it with no blank** — e.g. the
`✔ Found a CSV table …` sniff echo sits directly under the path answer.

So: `header → blank → [supporting text → blank] → ? prompt`. The prompt line is
answer-only (`? train`); the question lives in the header (the prompter runs
`bare`), never repeated on the `?` line. Keep it uniform — don't hand-tune the
spacing of individual questions.
So: `header → blank → [supporting text → blank] → ? prompt`. Keep it uniform —
don't hand-tune the spacing of individual questions.

## Guided-prompt labels

The `?` line carries a **short noun label**, not the question: `? Path: ~/mydata`,
`? Task: tabular_classification`. The question lives in the header and is never
repeated on the `?` line; the label says what you are typing into.

- The label is the **shortest noun phrase that names the answer**, with a trailing
colon — `Split:`, `Name:`, `Path:`, `Task:`, `Data type:`, `Label:` / `Target:`,
`Keypoints:`, `Resolution:`, `Column types:`, `Label policy:`, `Time column:`.
- **A prompt never goes label-less.** The guided flow used to blank survey's
`Message` (a `bare` mode), which rendered a lone `?` — and once answers were
pre-filled, `? [~/mydata]`: a question mark, a bracket and a path, with no verb
(cli#504). `internal/cli/interactive_test.go` asserts the property — non-empty,
ends in `:`, no `?`, within a length budget — against whatever the real flow
asks, so a new question is covered without editing the test.
- **A confirm is the exception: it carries the whole question** (`? Proceed with
the ingest? (y/N)`). A y/N prompt has no header of its own, and the
overwrite-replace confirm fires with nothing printed before it — a noun there
would name the object and hide the stakes.
- Flows with no step headers of their own (`client create`, `delete`,
`resources set`) pass the whole question as the label. The register follows the
header, not the prompter.

## Terminology

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
0.10.8
0.10.9
17 changes: 15 additions & 2 deletions cmd/tracebloc/main.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import (
"os"
"os/signal"
"syscall"
"time"

"github.com/tracebloc/cli/internal/cli"
)
Expand DownExpand Up@@ -59,11 +60,23 @@ func main() {
syscall.SIGINT, syscall.SIGTERM)
defer stop()

executed, err := cli.NewRootCmd(cli.BuildInfo{
info := cli.BuildInfo{
Version: version,
GitSHA: gitSHA,
BuildDate: buildDate,
}).ExecuteContextC(ctx)
}
root := cli.NewRootCmd(info)

started := time.Now()
executed, err := root.ExecuteContextC(ctx)

// backend#1907: one command-outcome event per invocation, emitted from the
// single point every command path converges on — command name, duration,
// exit code, OS/arch, version, error class. No arguments, no paths (see
// internal/cli/telemetry.go for why that is structural rather than a rule).
// Opt-out via TRACEBLOC_NO_TELEMETRY / DO_NOT_TRACK; best-effort and silent,
// so nothing here can change what the customer sees or what we exit with.
cli.RecordCommandOutcome(root, executed, info, cli.ExitCodeFromError(err), time.Since(started))

// F1: after the command runs, a quiet once-a-day nudge if a newer release
// exists (best-effort; silent on dev builds, off a terminal, in CI, or with
Expand Down
4 changes: 2 additions & 2 deletions docs/cli-navigation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ flowchart TD
ACCT --> logout["logout"]
ACCT --> authst["auth status"]
ACCT --> clis["client status"]
ACCT -.-> clcreate["client create"]:::hidden
ACCT --> clcreate["client create — point this machine at its client"]
ACCT -.-> cllist["client list"]:::hidden

ENVC --> di["data ingest"]
Expand DownExpand Up@@ -172,7 +172,7 @@ flowchart TD
- **not signed in / token 401·403** → `login`
- **426 upgrade-required** → upgrade the CLI
- **kubeconfig (exit 3)** → fix `--kubeconfig`/`--context`, then `doctor`
- **no client / environment (exit 4)** → run the installer (or `--namespace`); triage with `doctor`
- **no client / environment (exit 4)** → the error now says what IS on the reached cluster before advising (cli#515): one client on a local cluster → `client create` repoints this machine (it adopts, no new credential); a client on a remote/shared cluster → `--namespace <ns>` only; nothing there → run the installer. Triage with `doctor`
- **no token (exit 5)** → grant RBAC; diagnose with `cluster info` / `doctor`
- **destination exists (exit 6)** → `--overwrite`, a different `--name`, or `data delete` first
- **staging partial (exit 7)** → `data delete` then re-ingest
Expand Down
Loading
Loading