Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
127 changes: 127 additions & 0 deletions .cursor/BUGBOT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
# Bugbot guide — tracebloc/cli

## Context

Public Go CLI (Apache-2.0), shipped as **signed 8-platform releases** (cosign keyless,
verified by `scripts/install.sh`). Customers run it on their own machines against their own
Kubernetes to operate a self-hosted secure environment. It talks to a public HTTPS backend
(`internal/api`) and to an in-cluster jobs-manager (`internal/submit`), and shells out to
`kubectl`/`helm`/`docker`.

Two things make this repo unusual and should shape every finding:

1. **Its exit codes are a scripting contract** — customers branch on them
(`internal/cli/exitcodes.go`: "the numeric values are FROZEN").
2. **`make ci` mirrors CI exactly.** The `Makefile` header states it outright: "divergence
between local and CI is the bug this file exists to prevent." Tool versions are pinned in
lockstep with `.github/workflows/build.yml`.

## Always flag

- **A command that reports success it hasn't earned.** Exiting 0 is not the same as
succeeding. The reference pattern is `classifyPushOutcome`
(`internal/cli/data_ingest_output.go:25`): a Job that exits 0 but whose summary reports row
failures returns `"completed_with_failures"` + `exitIngestFailed`, *not* `"succeeded"` — its
comment cites this class explicitly. `internal/doctor` carries the same idea in
`StatusUnknown`: a check that ran but cannot back a green prints a neutral line rather than a
false ✔. Flag any new multi-step command where a partial failure collapses into success, and
any path where the `--output-json` status and the process exit code can disagree.

- **An exit code that isn't a named constant from `internal/cli/exitcodes.go`**, a repurposed
numeric value, or a new failure path returning generic `1` when a specific code already
exists. Every non-test `&exitError{}` names its code.

- **A prompt whose cancellation produces no visible feedback.** `errInteractiveCancelled`
(`internal/cli/interactive.go:26`) must print a `Cancelled — …` line via the Printer and then
return cleanly — see `resources_set.go:219`, `data_delete.go:233`,
`data_ingest_local.go:103`, `data_ingest_cluster.go:339`. Watch for
**`mapClientErr` (`internal/cli/client.go:858`), which maps it straight to `nil` with no
output at all** — a Ctrl-C routed through it exits 0 in total silence, right next to a
declined-answer branch that *does* print (`client.go:334`, `delete.go:196`). Check this at
every new or changed prompt site. Signals are wired centrally via `signal.NotifyContext`
(`cmd/tracebloc/main.go:58`) so deferred cleanup runs — a bare handler skips it and breaks
`push.Stage`'s cleanup contract. Interrupted-but-clean paths exit 130.

- **HTTP 426 treated as anything other than a hard stop.** It is detected centrally
(`internal/api/client.go`, `parseUpgradeRequired` → `*UpgradeRequiredError`) so every caller
degrades to the same actionable "run `tracebloc upgrade`" message — see `auth.go:336`,
`doctor.go:119`, `client_status.go:129`, `delete.go:151,218`, `client.go:253`. Flag a new API
consumer that retries through it, frames it as a transient outage, or folds it into a generic
error. A too-old CLI never recovers by waiting, so `--wait` loops must fail fast on it.

- **Verification that degrades to a warning.** In `scripts/install.sh` the SHA256 compare
aborts when no hashing tool is present, and `verify_cosign_signature()` bootstraps a pinned,
checksum-verified cosign (`COSIGN_VERSION=v2.4.1`) rather than skipping; the only bypass is an
explicit `TRACEBLOC_ALLOW_UNVERIFIED=1` with a loud warning. A previous "warn + continue +
still print ✓ matches" branch was caught as *both* a security regression and a dishonest log.
Also flag any `--version` / `RELEASE_VERSION` use that skips `validate_version_tag` before URL
interpolation. `tracebloc upgrade` and host prep must keep delegating to this verified script
instead of reimplementing verification in Go.

- **An external call with no ceiling.** Backend HTTP: `defaultTimeout = 30 * time.Second`
(`internal/api/client.go:31`). In-cluster submit: `SubmitTimeout`
(`internal/submit/client.go:21`). Doctor probes: `httpProbeTimeout = 8s`. Every shell-out uses
`exec.CommandContext`. Flag a bare `exec.Command` in non-test code, an `http.Client{}` with no
`Timeout`, or a watch/poll loop with no deadline.

- **A missing empty / nil / zero guard on anything crossing a boundary** (user input, API
response, cluster state). There is no shared validator — the convention is a colocated
`validate*` func: `internal/push/spec.go:100` (`ValidateTableName`),
`internal/cli/interactive.go:537-568`. Two specifics: a bare Enter yields `""` and must not be
treated as a real path (`validateDatasetPath` documents exactly this); and pagination must
fail loudly on an unparseable `next` link rather than silently truncating the list
(`internal/api/client.go`, `nextPath`). Where "empty" and "unknown" are different answers,
prefer a three-valued return (`internal/cluster/discover.go:302`).

- **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
(`internal/schema/*.json`, `internal/api/testdata/*.json`,
`internal/push/testdata/parity/goldens.json`, `internal/cli/testdata/golden/*.golden`) that
doesn't also bump and re-sync its pin, and any change to a chart assumption (discovery labels,
jobs-manager port, PVC mount path) that doesn't update `scripts/chart-invariants` — a chart
rename otherwise ships green in both repos and breaks discovery in the field.

- **Output that breaks the style contract** (`STYLE.md`): all colour goes through
`internal/ui`'s Printer — never inline an escape or brand hex outside `internal/ui`
(`scripts/check-style.sh` greps for it). Colour is never load-bearing: headings carry bold,
alerts carry a glyph, so the output still reads under `NO_COLOR`, in a pipe, and for a
colour-blind reader. User-facing copy follows the terminology table ("secure environment",
"ingest", "delete", "Online/Offline", "collaborators", "task"); only the workspace → secure
environment swap is grep-enforced, the rest is review judgement. A new user-facing string
almost always needs its golden regenerated:
`TB_UPDATE_GOLDEN=1 go test ./internal/cli/ -run TestCopyCatalog`.

- **Errors that lose their type.** `%w` wrapping is the house convention (~325 sites), with
typed errors for the cases callers branch on: `APIError`, `UpgradeRequiredError`,
`SubmitError`, `WatchError`, `exitError`, `noParentReleaseError`. Flag string-matching on an
error message where `errors.Is`/`errors.As` applies.

## Known non-issues — do not flag

- **`.golangci.yml` does not gate CI.** `golangci-lint` is never invoked in a workflow (its
`staticcheck`/`unused` are disabled there for runner OOM reasons); the blocking Lint job runs
pinned standalone binaries — `errcheck`, `gofmt -s`, `goimports`, `ineffassign`, `misspell`,
`staticcheck`, plus `deadcode-check.sh`, `file-budget.sh`, `check-style.sh`. Don't infer
coverage from that file.
- **`staticcheck` runs `-checks all,-ST1005` deliberately** — do not flag error-string
capitalisation or punctuation. It is a tracked, intentional exclusion (cli#279).
- `internal/submit/client.go:78` — `InsecureSkipVerify` is intentional for cluster-internal
traffic with no recognisable CA, documented in place and marked `//nolint:gosec`. It is the
only `nolint` in the repo.
- `scripts/deadcode-allowlist.txt` entries are verified false positives (Stringers reached only
through `fmt` reflection; test-only parity harnesses that must live in production source).
- `test/integration/*` uses 30s–5min timeouts because it drives a real cluster — not the
production timeout convention.
- `mutation.yml` and `head-drift-canary.yml` are advisory and never gate a merge.
- No `vendor/` directory — the module cache is used on purpose.
- `// style-guard: allow` is a defined escape hatch but is currently used nowhere; if one
appears, it is a novel exception worth scrutiny rather than an accepted pattern.

## Tone

Direct. Name the file and line. Give a concrete fix, not "consider". State the customer-visible
consequence — what they see, and which exit code they get — not just the code smell.

This repo is **public**: never put a customer name, internal hostname, or internal-only ticket
detail in a finding. A bare `tracebloc/backend#NNNN` reference is fine.
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@
<!-- 1–3 sentences. What does this PR do and why? -->

## Related
<!-- Closes #123 / Ref tracebloc/other-repo#456 -->
<!-- Same repo: Closes #123 · Cross-repo: Fixes tracebloc/backend#456 (owner-qualified — a bare backend#456 closes nothing). PRs land on develop, not the default branch, so confirm the issue actually closed. -->

## Type of change
- [ ] Feature
Expand All@@ -19,3 +19,4 @@
- [ ] `go build ./...`, `go vet`, and the Lint job's checks pass locally
- [ ] Terminal output follows [STYLE.md](../STYLE.md) — Printer tones (no hardcoded colour/emoji), "secure environment" not "workspace"; `bash scripts/check-style.sh` passes
- [ ] No secrets / credentials in the diff
- [ ] Cross-repo issues use `Fixes tracebloc/<repo>#N` — a bare `repo#N` closes nothing
38 changes: 31 additions & 7 deletions .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
#
# cosign verify-blob \
# --certificate-identity-regexp \
# 'https://github.com/tracebloc/cli/.github/workflows/release.yml@.*' \
# 'https://github.com/tracebloc/cli/.github/workflows/release.yml@refs/tags/v.*' \
# --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
# --certificate <binary>.cert \
# --signature <binary>.sig \
Expand DownExpand Up@@ -100,8 +100,23 @@ jobs:
REF="${{ inputs.ref || github.ref_name }}"
# Strip the leading v for use in -X main.version
VERSION="${REF#v}"
echo "tag=$REF" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
# The VERSION file declares the next release (read by the release
# train to cut rc/final tags). Any tag -- train-cut OR manual --
# must agree with it, so the file can never go silently stale
# after an out-of-train release. Refs from before the file existed
# (rebuilds of old tags) are grandfathered with a warning.
BASE=$(printf '%s' "$VERSION" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+')
if [ -f VERSION ]; then
FILE_VERSION=$(tr -d '[:space:]' < VERSION)
if [ "$BASE" != "$FILE_VERSION" ]; then
echo "::error::tag $REF (base $BASE) does not match VERSION ($FILE_VERSION) - bump VERSION on develop first."
exit 1
fi
else
echo "::warning::no VERSION file at this ref (pre-train tag) - skipping the consistency check."
fi
echo "tag=$REF" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Building $REF (version=$VERSION)"

- name: Build binary
Expand DownExpand Up@@ -202,17 +217,26 @@ jobs:
id: tag
run: |
REF="${{ inputs.ref || github.ref_name }}"
echo "tag=$REF" >> $GITHUB_OUTPUT
# STRICT stability rule: only a plain vX.Y.Z tag is a stable release.
# Anything else (v1.2.3-rc.1, and typos like v1.2.3rc1) is marked
# prerelease, so it can never become 'latest' -- which is what the
# installer bootstrap (releases/latest/download/...) resolves.
if printf '%s' "$REF" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi
echo "tag=$REF" >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
generate_release_notes: true
# Mark as prerelease for v*.*.* tags containing - (e.g.
# v0.1.0-rc1). Plain semver releases are stable.
prerelease: ${{ contains(steps.tag.outputs.tag, '-') }}
# Strict: only plain vX.Y.Z is stable (computed above); rc tags and
# malformed variants are prereleases and never become 'latest'.
prerelease: ${{ steps.tag.outputs.prerelease == 'true' }}
files: |
dist/tracebloc-*
dist/SHA256SUMS
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@ This repo follows the same conventions as the rest of the tracebloc org:
fix(#150): handle empty kubeconfig gracefully
```

- **PR body** should include a `Closes #N` line (on its own line) for any ticket the PR fully resolves. GitHub auto-closes the issue on merge. The `feat(#N):` convention in the title is for kanban tracking; `Closes #N` in the body is what triggers auto-close.
- **PR body** should include a `Closes #N` line (on its own line) for any ticket the PR fully resolves. The `feat(#N):` convention in the title is for kanban tracking; the body line is what links the issue. Owner-qualify anything in another repo — `Fixes tracebloc/backend#123` — because a bare `backend#123` only cross-references and closes nothing. And since GitHub fires closing keywords only on merges into the default branch (`main`), a PR merged to `develop` won't auto-close on its own: confirm the ticket, and close it by hand if needed.

- **One PR per ticket** when practical. Roll-up sync PRs (`Sync develop → main for vX.Y.Z release`) are an exception.

Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
0.10.1
6 changes: 6 additions & 0 deletions docs/rfcs/0001-cli-auth-and-client-provisioning.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
# RFC 0001 — Browser-based auth & one-command client provisioning

> **Qualified ID: `RFC-CLI-0001`.** Cite this document by its qualified ID, never as
> a bare "RFC 0001" — `backend` and `client` each have their own 0001. Note that
> most existing bare `RFC-0001` references in the `backend`, `cli` and `client`
> codebases mean **this** document. Org-wide index: `docs/rfcs/README.md` in
> `tracebloc/backend` (private).
>
> **Status: ACCEPTED — implemented.** The design in this RFC shipped in
> **CLI v0.4.0** ([cli#107](https://github.com/tracebloc/cli/pull/107)); the
> tracking epic ([cli#54](https://github.com/tracebloc/cli/issues/54)) is closed.
Expand Down
4 changes: 4 additions & 0 deletions docs/rfcs/0002-data-ingest-flow.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
# RFC 0002 — `tracebloc data ingest`: flow, terminology & task taxonomy

> **Qualified ID: `RFC-CLI-0002`.** Cite this document by its qualified ID, never as
> a bare "RFC 0002" — `backend` also has a 0002 (platform cost & autoscaling).
> Org-wide index: `docs/rfcs/README.md` in `tracebloc/backend` (private).
>
> **Status: DRAFT — for discussion.** Owner: @LukasWodka. Last updated: 2026-07-07.
>
> This RFC captures the redesign of the `tracebloc data ingest` user
Expand Down
6 changes: 6 additions & 0 deletions docs/rfcs/0003-storage-and-offboard-hygiene.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
# RFC 0003 — The secure environment: dataset storage, offboard hygiene & the boundary

> **Qualified ID: `RFC-CLI-0003`.** Cite this document by its qualified ID, never as
> a bare "RFC 0003" — `backend` also has a 0003 (configurable preprocessing &
> imputation). Note that the existing bare `RFC-0003` references in the `client`
> chart templates and `docs/SEAL-CHECK.md` mean **this** document. Org-wide index:
> `docs/rfcs/README.md` in `tracebloc/backend` (private).
>
> **Status: DECIDED — v2.3; decisions D1–D20 locked (D1–D15 2026-07-22;
> D16–D20 2026-07-23). Execution tickets filed and cross-linked in §10/§12;
> D16–D20 (per-dataset isolation) are decided but not yet ticketed, with
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,7 +115,7 @@ produces that code.

| Code | Meaning | Produced by | Constant |
|------|---------|-------------|----------|
| `0` | Success — includes `--dry-run` completing, a guided run you cancelled cleanly, and `doctor` passing with warnings only | all commands | `exitOK` |
| `0` | Success — includes `--dry-run` completing, any prompt you declined or cancelled with Ctrl-C (nothing was started, and the CLI prints a `Cancelled — …` line saying so), and `doctor` passing with warnings only | all commands | `exitOK` |
| `1` | Generic failure with no more specific bucket (also any error without an explicit code) | `login`, `client …`, `delete`, mistyped commands | `exitFailure` |
| `2` | Your input didn't validate: schema validation failed (spec synthesized from flags, or your YAML), an unsupported/unknown `--task`, a task-scoped flag applied to the wrong task, an invalid dataset name, or a resource size that doesn't fit the machine | `data ingest`, `data validate`, `data delete`, `resources set` | `exitBadInput` |
| `2` | One or more checks failed — for `client status --seal`: the environment is unsealed (a conformance check failed), or unknown (the chart ships no conformance checks, so the seal couldn't be verified) | `doctor`, `client status --seal` | `exitChecksFailed` |
Expand All@@ -129,7 +129,7 @@ produces that code.
| `7` | The cluster couldn't be queried for its datasets | `data list` | `exitQueryFailed` |
| `8` | jobs-manager rejected the submitted run (a non-auth 4xx/5xx), or the port-forward to it couldn't be set up | `data ingest` | `exitSubmitFailed` |
| `9` | The ingestion Job exited non-zero, completed with row-level failures the summary panel reports, or its outcome couldn't be determined / followed | `data ingest` | `exitIngestFailed` |
| `130` | You hit Ctrl-C at an interactive prompt (128+SIGINT) | interactive prompts | `exitInterrupted` |
| `130` | You hit Ctrl-C while something was already running — the sign-in wait, `client status --wait`, the seal check, or an installer re-run (128+SIGINT). Ctrl-C at a *question* is `0` instead: nothing had started | `login`, `client status --wait`, `client status --seal`, `upgrade`, `prepare-host` | `exitInterrupted` |

## Still stuck?

Expand Down
Loading
Loading