@bensandee/tooling 0.68.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -146
- package/bin/bst.mjs +26 -0
- package/dist/bin.mjs +17477 -8444
- package/package.json +16 -18
- package/tooling.schema.json +193 -4
- package/dist/bin.d.mts +0 -1
- package/dist/index.d.mts +0 -146
- package/dist/index.mjs +0 -1
package/README.md
CHANGED
|
@@ -13,14 +13,14 @@ pnpm dlx @bensandee/tooling repo:sync
|
|
|
13
13
|
|
|
14
14
|
## Conventions
|
|
15
15
|
|
|
16
|
-
The tool auto-detects project structure, CI platform, project type, and Docker packages from the filesystem. `.tooling.json` stores **overrides only**
|
|
16
|
+
The tool auto-detects project structure, CI platform, project type, and Docker packages from the filesystem. `.tooling.json` stores **overrides only** -- omitted fields use detected defaults. Runtime commands (`docker:build`, `checks:run`, `release:changesets`) work without running `repo:sync` first.
|
|
17
17
|
|
|
18
18
|
| Convention | Detection | Default | Override via |
|
|
19
19
|
| ----------------- | ----------------------------------------------------- | ---------------------------------------- | ------------------------------------ |
|
|
20
20
|
| Project structure | `pnpm-workspace.yaml` present | `single` | `structure` in `.tooling.json` |
|
|
21
21
|
| CI platform | `.github/workflows/` or `.forgejo/workflows/` dir | `none` | `ci` in `.tooling.json` |
|
|
22
22
|
| Project type | Dependencies in `package.json` (react, node, library) | `default` | `projectType` in `.tooling.json` |
|
|
23
|
-
| Docker packages | `Dockerfile` or `docker/Dockerfile` in package dirs |
|
|
23
|
+
| Docker packages | `Dockerfile` or `docker/Dockerfile` in package dirs | -- | `docker` map in `.tooling.json` |
|
|
24
24
|
| Formatter | Existing prettier config detected | `oxfmt` | `formatter` in `.tooling.json` |
|
|
25
25
|
| Release strategy | Existing release config detected | monorepo: `changesets`, single: `simple` | `releaseStrategy` in `.tooling.json` |
|
|
26
26
|
|
|
@@ -28,13 +28,13 @@ The tool auto-detects project structure, CI platform, project type, and Docker p
|
|
|
28
28
|
|
|
29
29
|
### Project management
|
|
30
30
|
|
|
31
|
-
| Command | Description
|
|
32
|
-
| --------------------------------- |
|
|
33
|
-
| `tooling repo:sync [dir]` | Detect, generate, and sync project tooling (idempotent). First run prompts for release strategy, CI platform (if not detected), and formatter (if Prettier found). Subsequent runs are non-interactive.
|
|
34
|
-
| `tooling repo:sync --check [dir]` | Dry-run drift detection. Exits 1 if files would change. CI-friendly.
|
|
35
|
-
| `tooling checks:run` | Run project checks (build, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: `--skip`, `--add`, `--fail-fast`, `--verbose`, `--with-coverage`, `--no-coverage`.
|
|
31
|
+
| Command | Description |
|
|
32
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `tooling repo:sync [dir]` | Detect, generate, and sync project tooling (idempotent). First run prompts for release strategy, CI platform (if not detected), and formatter (if Prettier found). Subsequent runs are non-interactive. |
|
|
34
|
+
| `tooling repo:sync --check [dir]` | Dry-run drift detection. Exits 1 if files would change. CI-friendly. |
|
|
35
|
+
| `tooling checks:run` | Run project checks (build, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: `--skip`, `--add`, `--fail-fast`, `--verbose`, `--with-coverage`, `--coverage`, `--no-coverage`. |
|
|
36
36
|
|
|
37
|
-
**Flags:** `--yes` (accept all defaults), `--no-ci
|
|
37
|
+
**Flags:** `--yes` (accept all defaults), `--no-ci` (negates `--ci`), `--no-prompt` (negates `--prompt`), `--eslint-plugin`
|
|
38
38
|
|
|
39
39
|
#### `checks:run`
|
|
40
40
|
|
|
@@ -56,24 +56,25 @@ The `--add` flag appends extra checks (must be defined in `package.json`):
|
|
|
56
56
|
tooling checks:run --add e2e
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
**Coverage is branch-aware by default.** With neither coverage flag, the `test` step runs via `pnpm run test:coverage` (producing `coverage/coverage-summary.json` for the `coverage:*` commands) **only on the repo's default branch**
|
|
59
|
+
**Coverage is branch-aware by default.** With neither coverage flag, the `test` step runs via `pnpm run test:coverage` (producing `coverage/coverage-summary.json` for the `coverage:*` commands) **only on the repo's default branch** -- PR and feature-branch runs skip the v8 instrumentation cost automatically. The current branch is read from `GITHUB_REF` / `GITHUB_REF_NAME` (set by GitHub _and_ Forgejo Actions), falling back to `git rev-parse --abbrev-ref HEAD` for local runs; tag and pull-request refs count as non-default. The default branch is `.tooling.json#defaultBranch` if set, otherwise detected via `git symbolic-ref refs/remotes/origin/HEAD`, otherwise `main`.
|
|
60
60
|
|
|
61
61
|
Explicit flags always win, regardless of branch:
|
|
62
62
|
|
|
63
|
-
- `--with-coverage`
|
|
64
|
-
- `--
|
|
63
|
+
- `--with-coverage` -- force the `test` step to run with coverage.
|
|
64
|
+
- `--coverage` -- the same thing, spelled as the positive of `--no-coverage`.
|
|
65
|
+
- `--no-coverage` -- force it to run without coverage.
|
|
65
66
|
|
|
66
67
|
If both are passed, `--no-coverage` wins.
|
|
67
68
|
|
|
68
|
-
Because the decision is branch-aware, the generated CI workflow runs plain `pnpm ci:check` (no coverage flag) and lets `checks:run` decide
|
|
69
|
+
Because the decision is branch-aware, the generated CI workflow runs plain `pnpm ci:check` (no coverage flag) and lets `checks:run` decide -- except when coverage is disabled in `.tooling.json`, where it passes `--no-coverage` to force it off everywhere.
|
|
69
70
|
|
|
70
|
-
**Extend the `check` script with `--add`, never a shell chain.** `repo:sync` generates `check` as a single rooted command (`bst checks:run`) and `ci:check` as `pnpm check --skip 'docker:*'`. When CI runs `pnpm ci:check`, pnpm appends the resolved `--skip 'docker:*'` (and any flags you add, e.g. `--no-coverage`) to the _end_ of the resolved script
|
|
71
|
+
**Extend the `check` script with `--add`, never a shell chain.** `repo:sync` generates `check` as a single rooted command (`bst checks:run`) and `ci:check` as `pnpm check --skip 'docker:*'`. When CI runs `pnpm ci:check`, pnpm appends the resolved `--skip 'docker:*'` (and any flags you add, e.g. `--no-coverage`) to the _end_ of the resolved script -- so they reach `bst checks:run` only if that is the last command. Adding extra checks via an `&&` chain silently drops `--skip 'docker:*'` and any forwarded flag:
|
|
71
72
|
|
|
72
73
|
```jsonc
|
|
73
|
-
//
|
|
74
|
+
// [ok] forwarded flags reach bst checks:run
|
|
74
75
|
"check": "bst checks:run --add check:env-drift,check:custom"
|
|
75
76
|
|
|
76
|
-
//
|
|
77
|
+
// [x] --skip / --no-coverage land on check:custom instead
|
|
77
78
|
"check": "bst checks:run && pnpm check:env-drift && pnpm check:custom"
|
|
78
79
|
```
|
|
79
80
|
|
|
@@ -99,50 +100,50 @@ Generated workflow files are regenerated on each `repo:sync` run. To preserve cu
|
|
|
99
100
|
|
|
100
101
|
Custom blocks are anchored to the nearest preceding non-blank line. When `repo:sync` regenerates the workflow, it extracts custom blocks from the existing file, generates fresh content, then re-inserts each block after its anchor line. If the anchor line is no longer present (e.g. a step was renamed), the block is appended at the end of the file.
|
|
101
102
|
|
|
102
|
-
To skip a workflow file entirely, add `# @bensandee/tooling:ignore` as the first line
|
|
103
|
+
To skip a workflow file entirely, add `# @bensandee/tooling:ignore` as the first line -- `repo:sync` will leave the file untouched.
|
|
103
104
|
|
|
104
105
|
### pnpm-workspace.yaml
|
|
105
106
|
|
|
106
|
-
`repo:sync` manages `pnpm-workspace.yaml` with a narrow contract
|
|
107
|
+
`repo:sync` manages `pnpm-workspace.yaml` with a narrow contract -- mandated keys, a few seeded-on-creation keys, and everything else passed through verbatim.
|
|
107
108
|
|
|
108
109
|
**Mandated** (drift fails `repo:sync --check`):
|
|
109
110
|
|
|
110
111
|
- `blockExoticSubdeps: true`
|
|
111
|
-
- `minimumReleaseAge`
|
|
112
|
+
- `minimumReleaseAge` -- any non-negative number (`0` is valid)
|
|
112
113
|
|
|
113
114
|
**Seeded on creation only** (free to change or remove afterward):
|
|
114
115
|
|
|
115
116
|
- `minimumReleaseAgeExclude: ["@bensandee/*"]`
|
|
116
117
|
- `packages: ["packages/*"]` (monorepos only)
|
|
117
118
|
|
|
118
|
-
All other keys
|
|
119
|
+
All other keys -- `allowBuilds`, custom entries -- are preserved verbatim through `repo:sync`. The generator emits the file for single-package repos too, not just monorepos.
|
|
119
120
|
|
|
120
121
|
Opt out entirely with `"pnpmWorkspace": false` in `.tooling.json`; the generator then never inspects or writes the file.
|
|
121
122
|
|
|
122
123
|
### packageManager force-bump
|
|
123
124
|
|
|
124
|
-
`repo:sync` rewrites `package.json#packageManager` when an existing value is `pnpm@<N>.x` with `N < 11`, setting it to the tooling's pinned pnpm version (currently `11.1.1`). This is delivered through `repo:sync` rather than Renovate because the mandated `pnpm-workspace.yaml` keys (`blockExoticSubdeps`) require pnpm 11+. Within-major bumps (`pnpm@11.x
|
|
125
|
+
`repo:sync` rewrites `package.json#packageManager` when an existing value is `pnpm@<N>.x` with `N < 11`, setting it to the tooling's pinned pnpm version (currently `11.1.1`). This is delivered through `repo:sync` rather than Renovate because the mandated `pnpm-workspace.yaml` keys (`blockExoticSubdeps`) require pnpm 11+. Within-major bumps (`pnpm@11.x -> 11.y`) remain Renovate's job; non-pnpm package managers (yarn, npm) are out of scope.
|
|
125
126
|
|
|
126
127
|
### Repository setup
|
|
127
128
|
|
|
128
|
-
| Command | Description
|
|
129
|
-
| ------------------------- |
|
|
130
|
-
| `tooling setup:secrets` | Configure CI secrets (Forgejo or GitHub). Auto-detects platform from `package.json` repository URL. Flags: `--dry-run`, `--no-docker
|
|
131
|
-
| `tooling forgejo:secrets` | Manage Forgejo Actions secrets directly. Subcommands: `set`, `list`, `delete`.
|
|
129
|
+
| Command | Description |
|
|
130
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `tooling setup:secrets` | Configure CI secrets (Forgejo or GitHub). Auto-detects platform from `package.json` repository URL. Flags: `--dry-run`, `--no-docker` (negates `--docker`). |
|
|
132
|
+
| `tooling forgejo:secrets` | Manage Forgejo Actions secrets directly. Subcommands: `set`, `list`, `delete`. |
|
|
132
133
|
|
|
133
134
|
`setup:secrets` detects the hosting platform from the `repository` field in `package.json`. For Forgejo repositories, it prompts for username and password, creates an access token with appropriate scopes, then sets `RELEASE_TOKEN` (and Docker secrets if Docker packages are detected). For GitHub repositories, it prompts for an existing Personal Access Token and uses `gh secret set` to configure secrets.
|
|
134
135
|
|
|
135
136
|
### Release management
|
|
136
137
|
|
|
137
|
-
| Command | Description
|
|
138
|
-
| -------------------------------- |
|
|
139
|
-
| `tooling release:changesets` | Changesets version/publish for Forgejo CI. Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`.
|
|
140
|
-
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`. Env: `RELEASE_TOKEN` (required on every platform).
|
|
141
|
-
| `tooling release:trigger` | Trigger a release workflow.
|
|
142
|
-
| `tooling workflow:dispatch` | Dispatch a `workflow_dispatch` event for any workflow in any repo on the same git host. Flags: `--repo`, `--workflow` (required), `--ref`, `--input key=value` (repeatable).
|
|
143
|
-
| `tooling forgejo:create-release` | Create a Forgejo release from a tag.
|
|
144
|
-
| `tooling changesets:merge` | Merge a changesets version PR.
|
|
145
|
-
| `tooling webhook:send <url>` | POST a structured JSON release payload with HMAC-SHA256 auth. Flags: `--tag`, `--repository`, `--server-url`, `--actor`, `--prerelease`. Secret from `WEBHOOK_SECRET` env var.
|
|
138
|
+
| Command | Description |
|
|
139
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `tooling release:changesets` | Changesets version/publish for Forgejo CI. Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`. |
|
|
141
|
+
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`. Env: `RELEASE_TOKEN` (required on every platform). |
|
|
142
|
+
| `tooling release:trigger` | Trigger a release workflow. |
|
|
143
|
+
| `tooling workflow:dispatch` | Dispatch a `workflow_dispatch` event for any workflow in any repo on the same git host. Flags: `--repo`, `--workflow` (required), `--ref`, `--input key=value` (repeatable). |
|
|
144
|
+
| `tooling forgejo:create-release` | Create a Forgejo release from a tag. |
|
|
145
|
+
| `tooling changesets:merge` | Merge a changesets version PR. Gated on a live read of the PR; refuses an empty version PR. On Forgejo, a first `mergeable: false` re-reads once after a short delay before refusing, since the forge reports `false` both for a conflict and while still checking. |
|
|
146
|
+
| `tooling webhook:send <url>` | POST a structured JSON release payload with HMAC-SHA256 auth. Flags: `--tag`, `--repository`, `--server-url`, `--actor`, `--prerelease`. Secret from `WEBHOOK_SECRET` env var. |
|
|
146
147
|
|
|
147
148
|
#### `release:simple`
|
|
148
149
|
|
|
@@ -165,18 +166,18 @@ tooling release:simple --release-as major
|
|
|
165
166
|
tooling release:simple --release-as 2.0.0
|
|
166
167
|
|
|
167
168
|
# Create a prerelease
|
|
168
|
-
tooling release:simple --release-as major --prerelease beta #
|
|
169
|
+
tooling release:simple --release-as major --prerelease beta # -> 2.0.0-beta.0
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
The generated release workflow exposes these as optional `workflow_dispatch` inputs (`bump` and `prerelease`), so bumps can also be controlled from the CI UI.
|
|
172
173
|
|
|
173
|
-
**Authentication:** `release:simple` requires `RELEASE_TOKEN` (a Personal Access Token with write access on the target forge) on every platform
|
|
174
|
+
**Authentication:** `release:simple` requires `RELEASE_TOKEN` (a Personal Access Token with write access on the target forge) on every platform -- Forgejo, GitHub, and GitHub Enterprise alike. The runner's auto-injected token (`github.token` / Forgejo Actions' internal auth) is never used: `actions/checkout@v6` writes that token to git config as an `http.<serverUrl>/.extraheader`, which silently overrides URL-embedded credentials and authenticates pushes as the runner pseudo-user. On Forgejo that pseudo-user is `uid=-2`, which fails the pre-receive hook (`Internal Server Error`); on GitHub Enterprise the same shape can mask custom hook rejections. `release:simple` actively unsets the extraheader and rewrites the remote URL with `RELEASE_TOKEN` before pushing.
|
|
174
175
|
|
|
175
176
|
GitHub consumers that previously relied on `github.token` must mint a PAT (scope: `contents: write`) and store it as the `RELEASE_TOKEN` secret on the repo or org.
|
|
176
177
|
|
|
177
178
|
#### `workflow:dispatch`
|
|
178
179
|
|
|
179
|
-
Triggers a `workflow_dispatch` event for an arbitrary workflow in an arbitrary repo on the same git host
|
|
180
|
+
Triggers a `workflow_dispatch` event for an arbitrary workflow in an arbitrary repo on the same git host -- useful for release->deploy patterns where one repo's CI kicks off a workflow in another (e.g. an infra repo). `release:trigger` is a special case of this command, pinned to `release.yml` in the current repo.
|
|
180
181
|
|
|
181
182
|
```bash
|
|
182
183
|
tooling workflow:dispatch \
|
|
@@ -191,22 +192,22 @@ On Forgejo it POSTs to the `actions/workflows/{workflow}/dispatches` API; on Git
|
|
|
191
192
|
|
|
192
193
|
##### Generated deploy step
|
|
193
194
|
|
|
194
|
-
For the common release
|
|
195
|
+
For the common release->deploy case, `repo:sync` can generate the dispatch step for you. Add a `deploy` block to `.tooling.json`:
|
|
195
196
|
|
|
196
197
|
```jsonc
|
|
197
198
|
{
|
|
198
199
|
"deploy": {
|
|
199
|
-
"dispatchRepo": "bensandee/ansible_infra", // required
|
|
200
|
-
"stack": "dirty_draft", // optional
|
|
201
|
-
"workflow": "deploy.yml", // optional
|
|
202
|
-
"ref": "main", // optional
|
|
200
|
+
"dispatchRepo": "bensandee/ansible_infra", // required -- repo whose workflow to trigger
|
|
201
|
+
"stack": "dirty_draft", // optional -- defaults to the package name with '-' -> '_'
|
|
202
|
+
"workflow": "deploy.yml", // optional -- default "deploy.yml"
|
|
203
|
+
"ref": "main", // optional -- default "main"
|
|
203
204
|
},
|
|
204
205
|
}
|
|
205
206
|
```
|
|
206
207
|
|
|
207
|
-
When `deploy` is set, `repo:sync` appends a **"Dispatch deploy"** step to the release workflow (`release.yml` for the `simple` strategy, `ci.yml` for `changesets`). The step runs `bst workflow:dispatch` only after a successful release
|
|
208
|
+
When `deploy` is set, `repo:sync` appends a **"Dispatch deploy"** step to the release workflow (`release.yml` for the `simple` strategy, `ci.yml` for `changesets`). The step runs `bst workflow:dispatch` only after a successful release -- gated on `steps.release.outputs.pushed` (simple) or `steps.release.outputs.published` (changesets) -- and forwards the release tag as the `release_tag` input. `dispatchRepo` is deliberately explicit: a deploy target is never inferred. The `release-it` strategy does not support deploy dispatch (`repo:sync` warns and skips the step). The resolved release token must have rights on `dispatchRepo`; supply a separate cross-repo token by adding its secret name to `postReleaseHookSecrets`.
|
|
208
209
|
|
|
209
|
-
> **Note:** the `release_tag` input is sourced from the release step's `tag` output. On GitHub with the `changesets` strategy the release runs via `changesets/action`, which exposes no `tag` output
|
|
210
|
+
> **Note:** the `release_tag` input is sourced from the release step's `tag` output. On GitHub with the `changesets` strategy the release runs via `changesets/action`, which exposes no `tag` output -- the deploy still fires, but `release_tag` is dispatched empty. The `simple` strategy and Forgejo `changesets` both populate it.
|
|
210
211
|
|
|
211
212
|
#### Release assets
|
|
212
213
|
|
|
@@ -242,7 +243,7 @@ If the script runs but writes no files to `release-assets/`, `release:assets` fa
|
|
|
242
243
|
| `command` | argv run before upload (optional; omit for pre-built files). Not interpreted by a shell. |
|
|
243
244
|
| `name` | Asset name in the release (optional; defaults to the file's basename). |
|
|
244
245
|
|
|
245
|
-
Convention and overrides coexist
|
|
246
|
+
Convention and overrides coexist -- a repo can use both.
|
|
246
247
|
|
|
247
248
|
Works for both `changesets` and `simple` release strategies, on Forgejo (via API upload) and GitHub (via `gh release upload --clobber`). The asset step in the workflow is gated on the release itself having published/pushed, so failed releases never upload stale artifacts.
|
|
248
249
|
|
|
@@ -253,7 +254,7 @@ Works for both `changesets` and `simple` release strategies, on Forgejo (via API
|
|
|
253
254
|
| `tooling coverage:summary` | Print a labeled per-package summary of the latest run. `--json` for machine-readable form. |
|
|
254
255
|
| `tooling coverage:check` | Compare the latest run against `coverage-baseline.json`; exit non-zero on regression. |
|
|
255
256
|
| `tooling coverage:record` | Append the latest run to a `bst/coverage-metrics` orphan branch (CI: push to default branch). |
|
|
256
|
-
| `tooling coverage:status` | Show recent coverage trend. Read-only
|
|
257
|
+
| `tooling coverage:status` | Show recent coverage trend. Read-only -- fetches the orphan branch, no checkout. |
|
|
257
258
|
|
|
258
259
|
The feature has two halves: a **CI history** that captures one record per commit on the default branch (visible to anyone with the repo), and a **local ratchet** that the agent (or developer) consults before declaring a task complete.
|
|
259
260
|
|
|
@@ -262,12 +263,12 @@ The feature has two halves: a **CI history** that captures one record per commit
|
|
|
262
263
|
`repo:sync` enables coverage tracking automatically when a `vitest.config.ts` is present. The generated `vitest.config.ts` includes the `json-summary` reporter so each `pnpm test:coverage` writes `coverage/coverage-summary.json` alongside the human-readable HTML report. A generated `coverage.yml` workflow runs on push to the default branch and pipes:
|
|
263
264
|
|
|
264
265
|
```text
|
|
265
|
-
pnpm test:coverage
|
|
266
|
+
pnpm test:coverage -> coverage:summary -> coverage:record -> coverage:status
|
|
266
267
|
```
|
|
267
268
|
|
|
268
269
|
so every CI log ends with the trend table for the last 10 records.
|
|
269
270
|
|
|
270
|
-
The history file (`coverage-history.jsonl`) lives on a dedicated `bst/coverage-metrics` orphan branch
|
|
271
|
+
The history file (`coverage-history.jsonl`) lives on a dedicated `bst/coverage-metrics` orphan branch -- never on `main`. This sidesteps branch protection (no privileged token needed beyond `contents: write`) and keeps `git log main` clean. `coverage:record` uses pure git plumbing (`hash-object` -> `mktree` -> `commit-tree` -> `push --force-with-lease`) so the working tree is never touched, and concurrent CI runs are handled safely via compare-and-swap with retry.
|
|
271
272
|
|
|
272
273
|
#### The local ratchet
|
|
273
274
|
|
|
@@ -275,7 +276,7 @@ A tracked `coverage-baseline.json` at the repo root captures the four overall to
|
|
|
275
276
|
|
|
276
277
|
`repo:sync` also inserts a `<!-- @tooling:coverage-rule -->` managed block into `CLAUDE.md` describing the workflow:
|
|
277
278
|
|
|
278
|
-
> Before declaring a task complete that touched files under `packages/*/src/`, run `pnpm test:coverage` then `pnpm exec bst coverage:check`. If any total dropped, add tests until parity is restored
|
|
279
|
+
> Before declaring a task complete that touched files under `packages/*/src/`, run `pnpm test:coverage` then `pnpm exec bst coverage:check`. If any total dropped, add tests until parity is restored -- or, if a touched file is genuinely not worth covering, exclude it via `coverage.exclude` in `vitest.config.ts` and explain why in the commit message. If all four are flat-or-up, run `pnpm exec bst coverage:check --update-baseline` and stage `coverage-baseline.json`.
|
|
279
280
|
|
|
280
281
|
Skip the rule for docs-only, config-only, dependency-bump, or test-only changes.
|
|
281
282
|
|
|
@@ -309,16 +310,16 @@ Or keep the local ratchet but skip the CI history append:
|
|
|
309
310
|
{ "coverage": { "history": "none" } }
|
|
310
311
|
```
|
|
311
312
|
|
|
312
|
-
| Override field | Default | Description
|
|
313
|
-
| -------------------- | ------------------------ |
|
|
314
|
-
| `history` | `"orphan-branch"` | `"none"` disables CI history; the local ratchet still works
|
|
315
|
-
| `historyBranch` | `"bst/coverage-metrics"` | Override the orphan branch name
|
|
316
|
-
| `tolerancePp` | `0.1` | Tolerance in percentage points before `coverage:check` flags a drop
|
|
317
|
-
| `gateOnPullRequests` | `false` | Run coverage on PR builds too (deferred follow-up
|
|
313
|
+
| Override field | Default | Description |
|
|
314
|
+
| -------------------- | ------------------------ | ----------------------------------------------------------------------- |
|
|
315
|
+
| `history` | `"orphan-branch"` | `"none"` disables CI history; the local ratchet still works |
|
|
316
|
+
| `historyBranch` | `"bst/coverage-metrics"` | Override the orphan branch name |
|
|
317
|
+
| `tolerancePp` | `0.1` | Tolerance in percentage points before `coverage:check` flags a drop |
|
|
318
|
+
| `gateOnPullRequests` | `false` | Run coverage on PR builds too (deferred follow-up -- currently a no-op) |
|
|
318
319
|
|
|
319
320
|
#### Per-package re-sum
|
|
320
321
|
|
|
321
|
-
The trimmed record's `packages` map is computed by aggregating files whose path starts with `packages/<name>/`, so monorepo packages get their own line/branch percentages without per-package vitest configs. Single-package repos collapse to one entry keyed by the directory basename. Per-package numbers are recorded but **not gated**
|
|
322
|
+
The trimmed record's `packages` map is computed by aggregating files whose path starts with `packages/<name>/`, so monorepo packages get their own line/branch percentages without per-package vitest configs. Single-package repos collapse to one entry keyed by the directory basename. Per-package numbers are recorded but **not gated** -- only the four overall totals control the ratchet.
|
|
322
323
|
|
|
323
324
|
### Docker
|
|
324
325
|
|
|
@@ -351,7 +352,7 @@ The `context` field defaults to `"."` (project root) when omitted. Versions for
|
|
|
351
352
|
|
|
352
353
|
#### Per-package build args
|
|
353
354
|
|
|
354
|
-
Each `docker` entry may declare `buildArgs`
|
|
355
|
+
Each `docker` entry may declare `buildArgs` -- a map of `ARG_NAME` to value that is passed to that package's `docker build` as `--build-arg ARG_NAME=<value>`. Args flow to the configured package only, so they don't trigger "unused build arg" warnings on sibling images. They apply to both `docker:build` and `docker:publish`, so CI-published images match local builds.
|
|
355
356
|
|
|
356
357
|
```json
|
|
357
358
|
{
|
|
@@ -412,8 +413,8 @@ Each package is tagged independently using its own version, so packages in a mon
|
|
|
412
413
|
| --------------------------- | -------- | --------------------------------------------------------------------- |
|
|
413
414
|
| `DOCKER_REGISTRY_HOST` | variable | Registry hostname (e.g. `code.orangebikelabs.com`) |
|
|
414
415
|
| `DOCKER_REGISTRY_NAMESPACE` | variable | Full namespace for tagging (e.g. `code.orangebikelabs.com/bensandee`) |
|
|
415
|
-
| `<REGISTRY>_USERNAME` | secret | Registry username
|
|
416
|
-
| `<REGISTRY>_PASSWORD` | secret | Registry password
|
|
416
|
+
| `<REGISTRY>_USERNAME` | secret | Registry username -- name depends on registry host (see below) |
|
|
417
|
+
| `<REGISTRY>_PASSWORD` | secret | Registry password -- name depends on registry host (see below) |
|
|
417
418
|
|
|
418
419
|
**Registry-specific secret names.** The workflow generator picks the secret reference at gen time from the registry host. The env-var name inside the step is always `DOCKER_USERNAME` / `DOCKER_PASSWORD` (so the CLI reads them under stable names), but the right-hand side of `${{ secrets.X }}` is registry-specific:
|
|
419
420
|
|
|
@@ -433,7 +434,7 @@ This rename exists because Forgejo's [act fork auto-injects](https://code.forgej
|
|
|
433
434
|
|
|
434
435
|
## Config file
|
|
435
436
|
|
|
436
|
-
`.tooling.json` stores **overrides only**
|
|
437
|
+
`.tooling.json` stores **overrides only** -- fields where the project differs from what convention/detection produces. A fully conventional project has `{}` or no `.tooling.json` at all.
|
|
437
438
|
|
|
438
439
|
Available override fields:
|
|
439
440
|
|
|
@@ -452,7 +453,7 @@ Available override fields:
|
|
|
452
453
|
|
|
453
454
|
## Debug logging
|
|
454
455
|
|
|
455
|
-
All CLI commands support a `--verbose` flag for detailed debug output. Alternatively, set `TOOLING_DEBUG=true` as an environment variable
|
|
456
|
+
All CLI commands support a `--verbose` flag for detailed debug output. Alternatively, set `TOOLING_DEBUG=true` as an environment variable -- useful in CI workflows:
|
|
456
457
|
|
|
457
458
|
```yaml
|
|
458
459
|
env:
|
|
@@ -461,29 +462,161 @@ env:
|
|
|
461
462
|
|
|
462
463
|
Debug output is prefixed with `[debug]` and includes exec results (exit codes, stdout/stderr), compose configuration details, container health statuses, and retry attempts.
|
|
463
464
|
|
|
464
|
-
##
|
|
465
|
+
## The agent daemon's web control panel
|
|
465
466
|
|
|
466
|
-
|
|
467
|
+
`agent:daemon --ui` serves an opt-in board that renders the `agent/<stage>[-<status>]` label state
|
|
468
|
+
machine, shows the daemon's own live state, and lets you drive the transitions you would otherwise
|
|
469
|
+
apply by hand with `bst issue:set-labels` -- plus read a branch's plan and reply on its pull request
|
|
470
|
+
without leaving the page. It is off by default.
|
|
467
471
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
472
|
+
The problem it solves is that a parked terminal is silent: `agent/plan-ready`,
|
|
473
|
+
`agent/implement-mergeable`, `agent/review-ready` and every `-failed` are states where the machine
|
|
474
|
+
has stopped and is waiting on a person, and nothing announces them. The board's lanes are ordered by
|
|
475
|
+
what needs you, not by pipeline position.
|
|
476
|
+
|
|
477
|
+
It also closes a real gap: **the board can kill a wedged run on a headless daemon.** The interactive
|
|
478
|
+
`k` keypress needs a TTY, so under systemd there was previously no way to stop a stuck container
|
|
479
|
+
short of a hand-run `docker kill`.
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
# Loopback, port 4600, token minted at startup and logged once.
|
|
483
|
+
bst agent:daemon --ui
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The startup log carries one clickable line -- open it and the token is exchanged for a session
|
|
487
|
+
cookie, so it never sits in your history:
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
board: http://127.0.0.1:4600/?t=<token>
|
|
491
|
+
board listening on http://127.0.0.1:4600
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Reaching it from a phone
|
|
495
|
+
|
|
496
|
+
The documented deployment binds **loopback only** and publishes the board through a tailnet TLS
|
|
497
|
+
terminator, so nothing is listening on your LAN at all and no phone is asked to trust a self-signed
|
|
498
|
+
certificate:
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
bst agent:daemon --ui \
|
|
502
|
+
--ui-public-url https://box.tail1234.ts.net \
|
|
503
|
+
--ui-behind-tls
|
|
504
|
+
|
|
505
|
+
# and, separately, whatever your terminator wants:
|
|
506
|
+
tailscale serve --bg 4600
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Both flags are required, and each fails closed in a way that looks like a bug if you omit it:**
|
|
510
|
+
|
|
511
|
+
- `--ui-public-url` adds the external hostname to the Host-header allowlist. Your browser sends
|
|
512
|
+
`Host: box.tail1234.ts.net`, not `127.0.0.1:4600`, so without it **every** proxied request is
|
|
513
|
+
rejected with a bare `403`. It is also what makes the startup line print the URL you can actually
|
|
514
|
+
open.
|
|
515
|
+
- `--ui-behind-tls` marks the session cookie `Secure`. It has to be explicit: the daemon serves plain
|
|
516
|
+
HTTP and cannot see that something in front terminated TLS, and `X-Forwarded-Proto` is
|
|
517
|
+
attacker-supplied unless a trusted proxy is guaranteed -- which nothing here can guarantee.
|
|
518
|
+
|
|
519
|
+
A non-loopback `--ui-bind` still works, but **pass `--ui-public-url` with it too.** The Host-header
|
|
520
|
+
allowlist is built from the bind string literally, so `--ui-bind 0.0.0.0` allows the useless
|
|
521
|
+
`Host: 0.0.0.0:4600` while your browser sends the machine's actual address -- and every request comes
|
|
522
|
+
back a bare `403` with nothing to explain it. `--ui-public-url http://<host-or-ip>:4600` names the host
|
|
523
|
+
you will really reach it at. Binding a specific address you then type verbatim needs nothing extra.
|
|
524
|
+
|
|
525
|
+
### The board token, and the session it mints
|
|
526
|
+
|
|
527
|
+
There is exactly one credential to be careful with, and its lifetime is the part people get wrong:
|
|
528
|
+
|
|
529
|
+
| | Board token | Session cookie it mints |
|
|
530
|
+
| --------- | ----------------------------------------------------------------- | ----------------------- |
|
|
531
|
+
| Lifetime | the daemon **process** -- a service, so weeks | 12 hours |
|
|
532
|
+
| Scope | label transitions + comments + kill, every repo the bot can write | the board, nothing else |
|
|
533
|
+
| Holders | one, shared | one browser |
|
|
534
|
+
| Persisted | never | never |
|
|
535
|
+
|
|
536
|
+
The board token gates reaching the board at all, and everything it grants happens under the daemon's
|
|
537
|
+
bot token. It is "short-lived" only relative to a config file; against the process it gates it is
|
|
538
|
+
long-lived, and rotating it means restarting the daemon -- at which point a phone bookmark goes dead
|
|
539
|
+
and you return to the log for a fresh link. That is the accepted cost of never persisting it.
|
|
540
|
+
|
|
541
|
+
Session cookies are `HttpOnly`, `SameSite=Strict`, and expire after 12 hours. Every request is
|
|
542
|
+
checked against a Host-header allowlist **and** an `Origin` check; neither replaces the other,
|
|
543
|
+
because an `Origin` check alone does not defeat DNS rebinding.
|
|
544
|
+
|
|
545
|
+
There is no second forge credential. **Everything the board writes -- labels and comments alike --
|
|
546
|
+
is written under the daemon's own bot token**, so there is no operator PAT to mint and no account to
|
|
547
|
+
connect. What that costs is authorship, and the next section is about why paying it is safe.
|
|
548
|
+
|
|
549
|
+
### Reading a plan, and replying to one
|
|
550
|
+
|
|
551
|
+
Three things a parked PR row offers beyond its label transitions:
|
|
552
|
+
|
|
553
|
+
**`View doc`** shows that branch's plan or spec without a clone. The doc is located by the same
|
|
554
|
+
`<kind>-<n>` naming convention the runs themselves use, and it is fetched **at the PR's head ref** --
|
|
555
|
+
what you read is the plan as the run left it, not as the default branch has it. The page reports
|
|
556
|
+
checkbox progress (`14 of 21 tasks`) and links out to the forge's own rendered view. The body itself
|
|
557
|
+
is shown as **plain escaped text, never rendered markdown**: branch content is written by anyone who
|
|
558
|
+
can push to it, and the board will not render it.
|
|
559
|
+
|
|
560
|
+
**`Comment`** opens a form on the PR's own thread (never a backing issue, whose comments are replayed
|
|
561
|
+
to every later run rather than windowed). Posting takes the same two-step confirm as kill, because
|
|
562
|
+
the forge has no comment-edit API and nothing you post can be retracted from here.
|
|
563
|
+
|
|
564
|
+
The comment is **authored by the daemon's bot account**, with a plain-prose suffix saying so:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
Looks right, but rebase on main first.
|
|
568
|
+
|
|
569
|
+
-- posted from the agent daemon control panel
|
|
477
570
|
```
|
|
478
571
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
572
|
+
That is safe because the boundary the agent uses to tell its own words from yours is **marker-based,
|
|
573
|
+
not author-based**: a run's own comments start with a reserved marker (`**agent:run**` and friends),
|
|
574
|
+
and everything without one is treated as human feedback no matter who wrote it. So a bot-authored
|
|
575
|
+
board comment reaches the next run exactly like one you typed in the forge's web UI. The attribution
|
|
576
|
+
is a **suffix** for the same reason -- markers are matched at the start, so trailing prose can never
|
|
577
|
+
be mistaken for one. The flip side: **the board refuses a comment whose text starts with a reserved
|
|
578
|
+
agent marker.** Typing `**agent:run**` yourself would move the run's own watermark and silently hide
|
|
579
|
+
every human comment written before it, so the form rejects it by name rather than posting something
|
|
580
|
+
that lies to the next run.
|
|
581
|
+
|
|
582
|
+
**The `N new comments` badge** on a parked row means: human comments on that PR newer than the last
|
|
583
|
+
agent operation on it -- the same window the next run will actually read, computed from the same two
|
|
584
|
+
functions, so the badge cannot promise the run something different. It appears on **pull request rows
|
|
585
|
+
only**; an issue's comments are replayed to every run whole, so "new since the last run" has no
|
|
586
|
+
meaning there and a number would be a lie. The counts are fetched in the daemon's poll cycle and
|
|
587
|
+
memoized, so a row nothing has touched costs nothing and `GET /` still makes no forge call.
|
|
588
|
+
|
|
589
|
+
### What it costs, and what it does not
|
|
590
|
+
|
|
591
|
+
Reads are cheap, not free: the publishers write a snapshot into memory and `GET /` is a synchronous
|
|
592
|
+
render off it, so **a board page load makes no forge call of its own**. What it does do is record
|
|
593
|
+
that someone is watching, which arms a refresher that drains queued actions and re-searches the
|
|
594
|
+
forge every few seconds until nobody is. That traffic is bounded by one refresher per daemon rather
|
|
595
|
+
than by how many tabs are open, and it stops an idle window after the last reader's last request --
|
|
596
|
+
a window derived from the poll interval, never shorter than two minutes. Closing the tab does not
|
|
597
|
+
cut that short: the stream ends at once, but presence also counts that reader's last request, and
|
|
598
|
+
nothing clears it early. The page renders its own
|
|
599
|
+
staleness (`as of 12s ago`) rather than implying it is live. Opening a doc view or posting a comment
|
|
600
|
+
hits the forge from the request itself -- those are explicit navigations you asked for, and they are
|
|
601
|
+
the only two that do.
|
|
602
|
+
|
|
603
|
+
**One honest caveat:** a request can still be delayed by a single in-flight _synchronous_ git `exec`
|
|
604
|
+
in the run pipeline -- `clone`, `rev-parse`, `diff`, `status`, `commit`, `ls-remote` -- each of which
|
|
605
|
+
briefly blocks the event loop, a `git clone --depth 1` being the longest. The container run itself
|
|
606
|
+
does not block. Making those git calls async is out of scope, so the board is usually instant and
|
|
607
|
+
occasionally waits out one clone.
|
|
608
|
+
|
|
609
|
+
An action is **queued, not applied**, when the page comes back. The board never writes a label
|
|
610
|
+
itself; the daemon drains queued actions -- at the top of its next claim cycle, and on the board
|
|
611
|
+
refresher's own timer while a viewer is present -- which keeps it the only writer of agent labels in
|
|
612
|
+
the process. A row says `queued` in either state, because the refresher applies it within a tick or
|
|
613
|
+
two whatever the claim cycle is doing. Each action is re-checked against live labels when it is
|
|
614
|
+
applied, and one whose target has since been claimed is skipped with the reason shown on the row
|
|
615
|
+
rather than silently dropped. Kill is the exception: it writes no label, so it takes effect
|
|
616
|
+
immediately rather than at a drain.
|
|
617
|
+
|
|
618
|
+
`--ui` is refused together with `--once`, which runs a single cycle and returns -- the board would
|
|
619
|
+
serve one page and then be a dead surface whose every control did nothing.
|
|
487
620
|
|
|
488
621
|
## Docker check
|
|
489
622
|
|
|
@@ -493,7 +626,7 @@ import type {
|
|
|
493
626
|
|
|
494
627
|
A smoketest is a command that runs against the live Docker stack after all containers are healthy and HTTP health checks pass. This is useful for running integration tests, API contract checks, or any validation that requires the full stack to be running.
|
|
495
628
|
|
|
496
|
-
**Auto-detection:** If the root `package.json` has a `"test:smoke"` script, it is automatically used as the smoketest command
|
|
629
|
+
**Auto-detection:** If the root `package.json` has a `"test:smoke"` script, it is automatically used as the smoketest command -- no configuration needed.
|
|
497
630
|
|
|
498
631
|
**Manual configuration:** For non-conventional command names, configure via `.tooling.json`:
|
|
499
632
|
|
|
@@ -519,82 +652,23 @@ services:
|
|
|
519
652
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
|
520
653
|
```
|
|
521
654
|
|
|
522
|
-
**Unset variables are filled with an empty string, automatically.** Compose interpolates each file _before_ merging them, so on a host where `ANTHROPIC_API_KEY` is unset it prints `The "ANTHROPIC_API_KEY" variable is not set. Defaulting to a blank string.` on every invocation
|
|
655
|
+
**Unset variables are filled with an empty string, automatically.** Compose interpolates each file _before_ merging them, so on a host where `ANTHROPIC_API_KEY` is unset it prints `The "ANTHROPIC_API_KEY" variable is not set. Defaulting to a blank string.` on every invocation -- including teardown, after the check has already passed. Before each stack comes up, `docker:check` asks compose which variables are still unset and sets each to the empty string compose was substituting anyway: the warnings go away and the value every container sees is unchanged. One line names what was filled.
|
|
523
656
|
|
|
524
657
|
Two things follow from this that are worth knowing:
|
|
525
658
|
|
|
526
659
|
- **Setting the value in the check overlay does not silence the warning.** The overlay overrides the _merged_ value, but the warning comes from interpolating the base file, which happens per-file beforehand. Nothing you can write in the overlay reaches it.
|
|
527
660
|
- **`docker-compose.check.env` is the file that does feed interpolation.** Drop it beside the base compose file and `docker:check` auto-detects it and passes it to compose as `--env-file`. Use it when the check needs _real_ values rather than blanks. Override the location with `dockerCheck.envFile` in `.tooling.json`.
|
|
528
661
|
|
|
529
|
-
**The `PORT` trap:** when a service publishes a `${PORT}`-parameterized port, `docker:check` allocates a free host port and injects it, so a check run never collides with something already bound on the host. Assigning `PORT` in `docker-compose.check.env` (or in the ambient environment) opts out of that
|
|
662
|
+
**The `PORT` trap:** when a service publishes a `${PORT}`-parameterized port, `docker:check` allocates a free host port and injects it, so a check run never collides with something already bound on the host. Assigning `PORT` in `docker-compose.check.env` (or in the ambient environment) opts out of that -- your value is used verbatim, collisions included.
|
|
530
663
|
|
|
531
|
-
`docker-compose.check.env` is exempt from the bst-hook's env-file deny rules, so an agent working in the repo can create and read it. It is a committed, placeholder-holding file for a throwaway stack
|
|
664
|
+
`docker-compose.check.env` is exempt from the bst-hook's env-file deny rules, so an agent working in the repo can create and read it. It is a committed, placeholder-holding file for a throwaway stack -- do not put real secrets in it.
|
|
532
665
|
|
|
533
666
|
### Derived HTTP health checks
|
|
534
667
|
|
|
535
|
-
A service with a published port but no compose-level `healthcheck:` automatically gets an HTTP health check
|
|
536
|
-
|
|
537
|
-
This matters because compose publishes a port in the _host's_ network namespace, not the caller's. On a normal developer machine those are the same thing, so probing `localhost:<hostPort>` directly works fine — but on a CI runner that shares the host's docker daemon over a mounted socket (the standard Forgejo/GitHub Actions runner shape), the job container's `localhost` is its own netns, and the published port is unreachable from it. Probing from inside the target's own namespace works in every topology, including that one.
|
|
538
|
-
|
|
539
|
-
The probe image (`curlimages/curl`) is pinned by digest, and a `docker run` exit code of `125` (Docker's convention for "the daemon failed to start the container at all" — missing image, pull denied, daemon unreachable) fails the check immediately instead of waiting out the full timeout.
|
|
540
|
-
|
|
541
|
-
**This only applies to auto-derived checks.** A health check you configure yourself via `.tooling.json`'s `dockerCheck.healthChecks` is still probed with a direct `fetch()` from wherever `bst docker:check` runs — an arbitrary user-supplied URL isn't necessarily compose-related at all, so making it reachable from the caller remains your responsibility.
|
|
542
|
-
|
|
543
|
-
### Library API
|
|
668
|
+
A service with a published port but no compose-level `healthcheck:` automatically gets an HTTP health check -- no configuration needed. That check is probed **from inside the service's own container network namespace** (`docker run --network container:<id> curlimages/curl ... http://localhost:<containerPort>/`), not from the process running `bst docker:check`.
|
|
544
669
|
|
|
545
|
-
|
|
670
|
+
This matters because compose publishes a port in the _host's_ network namespace, not the caller's. On a normal developer machine those are the same thing, so probing `localhost:<hostPort>` directly works fine -- but on a CI runner that shares the host's docker daemon over a mounted socket (the standard Forgejo/GitHub Actions runner shape), the job container's `localhost` is its own netns, and the published port is unreachable from it. Probing from inside the target's own namespace works in every topology, including that one.
|
|
546
671
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
```ts
|
|
550
|
-
import { createRealExecutor, runDockerCheck } from "@bensandee/tooling/docker-check";
|
|
551
|
-
import type { CheckConfig } from "@bensandee/tooling/docker-check";
|
|
552
|
-
|
|
553
|
-
const config: CheckConfig = {
|
|
554
|
-
compose: {
|
|
555
|
-
cwd: "./deploy",
|
|
556
|
-
composeFiles: ["docker-compose.yaml"],
|
|
557
|
-
services: ["api", "db"],
|
|
558
|
-
},
|
|
559
|
-
buildCommand: ["pnpm", "image:build"],
|
|
560
|
-
healthChecks: [
|
|
561
|
-
{
|
|
562
|
-
name: "API",
|
|
563
|
-
url: "http://localhost:3000/health",
|
|
564
|
-
validate: async (res) => res.ok,
|
|
565
|
-
},
|
|
566
|
-
],
|
|
567
|
-
smoketest: ["pnpm", "test:smoke"],
|
|
568
|
-
timeoutMs: 120_000,
|
|
569
|
-
pollIntervalMs: 5_000,
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
const result = await runDockerCheck(createRealExecutor(), config);
|
|
573
|
-
if (!result.success) {
|
|
574
|
-
console.error(result.reason, result.message);
|
|
575
|
-
}
|
|
576
|
-
```
|
|
672
|
+
The probe image (`curlimages/curl`) is pinned by digest, and a `docker run` exit code of `125` (Docker's convention for "the daemon failed to start the container at all" -- missing image, pull denied, daemon unreachable) fails the check immediately instead of waiting out the full timeout.
|
|
577
673
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
| Export | Description |
|
|
581
|
-
| -------------------------------------- | ---------------------------------------------------------------------------- |
|
|
582
|
-
| `runDockerCheck(executor, config)` | Full lifecycle: build, compose up, health check polling, smoketest, teardown |
|
|
583
|
-
| `createRealExecutor()` | Production executor (real shell, fetch, timers) |
|
|
584
|
-
| `composeUp(executor, config)` | Start compose services |
|
|
585
|
-
| `composeDown(executor, config)` | Stop and remove compose services |
|
|
586
|
-
| `composeLogs(executor, config)` | Stream compose logs |
|
|
587
|
-
| `composePs(executor, config)` | List running containers |
|
|
588
|
-
| `checkHttpHealth(executor, check)` | Run a single HTTP health check |
|
|
589
|
-
| `getContainerHealth(executor, config)` | Check container-level health status |
|
|
590
|
-
|
|
591
|
-
#### Types
|
|
592
|
-
|
|
593
|
-
| Type | Description |
|
|
594
|
-
| --------------------- | ------------------------------------------------------------------------------------------ |
|
|
595
|
-
| `CheckConfig` | Full check config (compose settings, build command, health checks, smoketest, timeouts) |
|
|
596
|
-
| `ComposeConfig` | Docker Compose settings (cwd, compose files, env file, services) |
|
|
597
|
-
| `HttpHealthCheck` | Health check definition (name, URL, validate function) |
|
|
598
|
-
| `CheckResult` | Result: `{ success: true, elapsedMs }` or `{ success: false, reason, message, elapsedMs }` |
|
|
599
|
-
| `DockerCheckExecutor` | Side-effect abstraction (exec, fetch, timers) for testability |
|
|
600
|
-
| `ContainerInfo` | Container status info from `composePs` |
|
|
674
|
+
**This only applies to auto-derived checks.** A health check you configure yourself via `.tooling.json`'s `dockerCheck.healthChecks` is still probed with a direct `fetch()` from wherever `bst docker:check` runs -- an arbitrary user-supplied URL isn't necessarily compose-related at all, so making it reachable from the caller remains your responsibility.
|