@cldmv/gh-broker 0.8.0 → 0.10.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.
Files changed (106) hide show
  1. package/README.md +48 -13
  2. package/config.example.json +86 -3
  3. package/config.sample.json +1 -1
  4. package/deploy/README.md +58 -18
  5. package/deploy/cli.mjs +38 -0
  6. package/deploy/gh-broker.service +9 -2
  7. package/deploy/install.mjs +100 -41
  8. package/deploy/lib/agent-cli.mjs +166 -0
  9. package/deploy/lib/agents.mjs +128 -0
  10. package/deploy/lib/cli.mjs +72 -0
  11. package/deploy/lib/config-merge.mjs +29 -8
  12. package/deploy/lib/install-args.mjs +3 -2
  13. package/deploy/lib/mcp-config.mjs +211 -0
  14. package/deploy/lib/otp-cli.mjs +136 -0
  15. package/deploy/lib/otp.mjs +325 -0
  16. package/deploy/lib/secret-store.mjs +1 -0
  17. package/deploy/lib/system.mjs +24 -12
  18. package/deploy/real-deps.mjs +102 -0
  19. package/dist/index.mjs +5171 -1686
  20. package/dist/index.mjs.map +4 -4
  21. package/package.json +14 -3
  22. package/docs/TOOLS.md +0 -538
  23. package/docs/agent-user-model.md +0 -89
  24. package/docs/changelog/v0/v0.2.0.md +0 -26
  25. package/docs/changelog/v0/v0.2.1.md +0 -23
  26. package/docs/changelog/v0/v0.2.2.md +0 -23
  27. package/docs/changelog/v0/v0.2.3.md +0 -33
  28. package/docs/changelog/v0/v0.2.4.md +0 -25
  29. package/docs/changelog/v0/v0.2.5.md +0 -29
  30. package/docs/changelog/v0/v0.3.0.md +0 -84
  31. package/docs/changelog/v0/v0.3.1.md +0 -30
  32. package/docs/changelog/v0/v0.3.2.md +0 -24
  33. package/docs/changelog/v0/v0.3.3.md +0 -28
  34. package/docs/changelog/v0/v0.4.0.md +0 -58
  35. package/docs/changelog/v0/v0.4.1.md +0 -33
  36. package/docs/changelog/v0/v0.4.2.md +0 -45
  37. package/docs/changelog/v0/v0.5.0.md +0 -39
  38. package/docs/changelog/v0/v0.5.1.md +0 -28
  39. package/docs/changelog/v0/v0.6.0.md +0 -52
  40. package/docs/changelog/v0/v0.6.1.md +0 -48
  41. package/docs/changelog/v0/v0.7.0.md +0 -71
  42. package/docs/changelog/v0/v0.8.0.md +0 -115
  43. package/docs/deploy-architecture.md +0 -84
  44. package/src/appauth.ts +0 -56
  45. package/src/audit.ts +0 -34
  46. package/src/availability.ts +0 -42
  47. package/src/catalog.ts +0 -182
  48. package/src/config.ts +0 -157
  49. package/src/index.ts +0 -107
  50. package/src/paths.ts +0 -60
  51. package/src/router.ts +0 -299
  52. package/src/secrets.ts +0 -167
  53. package/src/server.ts +0 -91
  54. package/src/types.ts +0 -128
  55. package/src/upstream.ts +0 -85
  56. package/src/verbs/ghcr/archive.ts +0 -314
  57. package/src/verbs/ghcr/config.ts +0 -142
  58. package/src/verbs/ghcr/jobs.ts +0 -141
  59. package/src/verbs/ghcr/push.ts +0 -192
  60. package/src/verbs/ghcr/registry.ts +0 -275
  61. package/src/verbs/ghcr/tar.ts +0 -203
  62. package/src/verbs/ghcr_push.ts +0 -264
  63. package/src/verbs/issue_labels.ts +0 -329
  64. package/src/verbs/link_issue.ts +0 -84
  65. package/src/verbs/npm-registry.ts +0 -168
  66. package/src/verbs/npm-tarball.ts +0 -76
  67. package/src/verbs/npm_scaffold.ts +0 -325
  68. package/src/verbs/release-analysis.ts +0 -212
  69. package/src/verbs/repo-tier.ts +0 -90
  70. package/src/verbs/report_ci_status.ts +0 -358
  71. package/src/verbs/report_release_prs.ts +0 -238
  72. package/src/verbs/report_security_alerts.ts +0 -442
  73. package/src/verbs/report_unlabeled.ts +0 -194
  74. package/src/verbs/rest-client.ts +0 -181
  75. package/src/verbs/review_thread.ts +0 -138
  76. package/tests/appauth.test.vitest.mts +0 -76
  77. package/tests/audit.test.vitest.mts +0 -48
  78. package/tests/catalog.test.vitest.mts +0 -234
  79. package/tests/config-merge.test.vitest.mts +0 -325
  80. package/tests/config.test.vitest.mts +0 -388
  81. package/tests/credentials.test.vitest.mts +0 -127
  82. package/tests/ghcr-archive.test.vitest.mts +0 -439
  83. package/tests/ghcr-config.test.vitest.mts +0 -96
  84. package/tests/ghcr-push.test.vitest.mts +0 -526
  85. package/tests/ghcr-registry.test.vitest.mts +0 -206
  86. package/tests/helpers/ghcr-fixtures.mts +0 -373
  87. package/tests/install-args.test.vitest.mts +0 -152
  88. package/tests/issue-labels.test.vitest.mts +0 -362
  89. package/tests/npm-scaffold.test.vitest.mts +0 -479
  90. package/tests/npm-setup.test.vitest.mts +0 -196
  91. package/tests/paths.test.vitest.mts +0 -57
  92. package/tests/prompt.test.vitest.mts +0 -128
  93. package/tests/release-prs.test.vitest.mts +0 -247
  94. package/tests/rest-client.test.vitest.mts +0 -115
  95. package/tests/router.test.vitest.mts +0 -505
  96. package/tests/run-vitest.mjs +0 -51
  97. package/tests/secret-store.test.vitest.mts +0 -167
  98. package/tests/secrets.test.vitest.mts +0 -208
  99. package/tests/security-alerts.test.vitest.mts +0 -338
  100. package/tests/server.test.vitest.mts +0 -200
  101. package/tests/smoke.test.vitest.mts +0 -177
  102. package/tests/system.test.vitest.mts +0 -355
  103. package/tests/upstream.test.vitest.mts +0 -91
  104. package/tests/verb-helpers.test.vitest.mts +0 -377
  105. package/tests/verbs.test.vitest.mts +0 -624
  106. package/tsconfig.json +0 -21
@@ -1,52 +0,0 @@
1
- # gh-broker v0.6.0 Changelog
2
-
3
- **Release Date**: October 2026
4
- **Release Type**: Minor
5
- **Branch**: `release/0.6.0`
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- v0.6.0 makes the two report verbs from v0.5.0 usable on a whole org. `report_security_alerts` can now return a compact per-repo roll-up instead of about 700 KB of alerts, filter by severity and repo, and tag each repo with its flow tier. `report_release_prs` can now judge whether each release is ready for its changelog, including whether an existing changelog is still up to date. All additions are opt-in parameters on verbs that already exist, so nothing changes for existing callers and no config change is needed.
12
-
13
- ---
14
-
15
- ## ✨ Features
16
-
17
- ### Per-repo roll-up, filters and flow tier for `report_security_alerts` ([#99](https://github.com/CLDMV/gh-broker/pull/99))
18
-
19
- - **`byRepo` on every result.** One row per repo with at least one alert, worst first (criticals, then highs, then total), with per-type counts and critical / high / medium / low counts.
20
- - **`summaryOnly`.** Returns just the roll-up and the counts. An org run drops from about 700 KB to a few KB, and `alertsOmitted` says how many alerts were left out.
21
- - **`minSeverity` and `repos`.** Keep only alerts at or above a severity (secret-scanning alerts have no severity and are always kept), or only the named repos.
22
- - **`classify`.** Adds each repo's flow tier to its row: `v4` (carries the v4 release workflow), `dev-trunk` (a v4 repo whose real code lives on a persistent `dev` branch while `next` holds only the scaffold) or `non-v4` (no release workflow on the default branch, even if `next` and `hotfixes` branches exist). The raw facts behind the tier are returned as `tierSignals`, because the tier is a structural signal and not a verdict. It costs about three calls per repo that has alerts.
23
-
24
- ### Release readiness for `report_release_prs` ([#99](https://github.com/CLDMV/gh-broker/pull/99))
25
-
26
- `analyze: true` adds an `analysis` block to each release PR and a `verdicts` tally:
27
-
28
- - **Open PRs against the release branch**, classified as doc, dependency, feature or draft.
29
- - **Changelog freshness.** The release PR's commits give the PRs that actually shipped; the changelog is checked for each one. The status is `current`, `minor-gap` (only CI, chore, style or test PRs missing), `stale` (a user-facing PR missing), `version-drift` (the heading names another version) or `thin` (a stub). The README What's New block is checked for the version too.
30
- - **A verdict:** `done`, `in-flight` (a doc PR is already open), `wait` (feature PRs still open), `gray` (only dependency PRs or drafts open), `write`, `update` or `inspect`.
31
-
32
- With `analyze`, `onlyMissing` lists every release PR whose verdict is not `done`, so a changelog that exists but is out of date is not hidden. It costs about five more calls per release PR, so raise `maxApiCalls` for a whole org.
33
-
34
- ## 🔧 Dependencies
35
-
36
- All dev-only; no runtime dependency changed and the lockfile moves only these two packages.
37
-
38
- - `@cldmv/fix-headers` `^2.1.1` to `^2.2.0` (locked 2.1.1 to 2.2.0), in two steps: 2.1.4 ([#97](https://github.com/CLDMV/gh-broker/pull/97), no headers needed restamping) and then 2.2.0 ([#104](https://github.com/CLDMV/gh-broker/pull/104)). From 2.2.0 the `@Last modified by` tag follows content edits only, so a header-only rewrite keeps the recorded editor. Running it added the standard header block to `src/verbs/release-analysis.ts` and `src/verbs/repo-tier.ts`, the two files from the verb work above that had none; no existing header or code line changed.
39
- - `@cldmv/configs` `^1.2.0` to `^1.2.4` (locked 1.2.0 to 1.2.4) ([#104](https://github.com/CLDMV/gh-broker/pull/104)). Version 1.2.4 sets `forceAuthorUpdate` and `forceLastModifiedAuthorUpdate` to `false`, so `@Author` is never rewritten. Re-running `fix:headers` under the new config restamped no files.
40
-
41
- ## 📚 Documentation
42
-
43
- - **NEW:** [docs/changelog/v0/v0.6.0.md](./v0.6.0.md): this changelog.
44
- - **NEW:** backfilled [v0.3.3](./v0.3.3.md), [v0.4.1](./v0.4.1.md), [v0.4.2](./v0.4.2.md), [v0.5.0](./v0.5.0.md) and [v0.5.1](./v0.5.1.md), which shipped without a changelog file.
45
- - The new parameters, the tier definitions and the verdict table are documented in `docs/TOOLS.md`.
46
-
47
- ---
48
-
49
- ## Upgrade notes
50
-
51
- - No breaking changes. The new parameters are optional, and the output of an existing call only gains a `byRepo` field.
52
- - Redeploy and reconnect your client to use them. A broker older than this version rejects the new parameters as unknown properties.
@@ -1,48 +0,0 @@
1
- # gh-broker v0.6.1 Changelog
2
-
3
- **Release Date**: October 2026
4
- **Release Type**: Patch
5
- **Branch**: `release/0.6.1`
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- v0.6.1 updates the MCP SDK the broker is built on, `@modelcontextprotocol/sdk` from 1.31.0 to 1.32.0, and refreshes the development toolchain: vitest 5.0.3, `@cldmv/vitest-runner` 1.5.3 and `@types/node` 26.6.4.
12
-
13
- No broker source changed. The net diff against v0.6.0 is `package.json` and `package-lock.json` only, and the SDK is the single runtime package that moved: its own dependency ranges are identical between the two versions, so no new runtime package is installed. The SDK's 1.32.0 release does change how its HTTP client follows redirects (see Upgrade notes), and the broker uses that client to reach the upstream GitHub MCP endpoint, so the SDK bump is a runtime-affecting update even though no line of broker code differs.
14
-
15
- ---
16
-
17
- ## 🔧 Dependencies
18
-
19
- ### Runtime
20
-
21
- - `@modelcontextprotocol/sdk` `^1.31.0` → `^1.32.0` (locked 1.31.0 → 1.32.0) ([#108](https://github.com/CLDMV/gh-broker/pull/108)). The 1.32.0 release notes list these changes:
22
- - **Redirects (upgrade note).** The HTTP client transports now follow a redirect only when it stays on the same origin (same scheme, host and port; http to https on the same host is allowed). `redirectPolicy: 'follow'` on `StreamableHTTPClientTransport` restores the old behavior.
23
- - **Two new options, off unless set:** `maxToolInputElements` on `McpServer` and `expectedResource` on `requireBearerAuth`. The broker uses neither: it builds its server on the SDK's low-level `Server` class (with a stdio transport bound to a local socket) and does not use the SDK's bearer-auth middleware.
24
- - **Server fixes:** `tools/call` and `prompts/get` requests that omit `arguments` are accepted, and the in-memory task store keeps tasks within the session that created them. Neither was exercised by a broker code path that was changed.
25
- - Documentation and example changes in the SDK repository (SECURITY.md, example session handling).
26
-
27
- ### Development
28
-
29
- - `@cldmv/vitest-runner` `^1.5.1` → `^1.5.3` (locked 1.5.1 → 1.5.3) ([#109](https://github.com/CLDMV/gh-broker/pull/109)). Version 1.5.3 makes the runner's CommonJS entry fail with a clear error on a Node.js version without `require(esm)` and raises the runner's `engines.node` from `>=20.19.0` to `>=22.12.0`. The locked package's license field also changes from MIT to Apache-2.0. The runner's `chalk` dependency moves from 5.x to 6, so the locked `chalk` goes 5.6.2 → 6.0.1 and now requires Node.js 22 (`engines` `>=22`). The broker's own `engines.node` was already `>=22.12.0` and the CI matrix already starts at 22.12.0, so nothing about supported Node.js versions changes here.
30
- - `vitest` `^5.0.1` → `^5.0.3` (locked 5.0.2 → 5.0.3) ([#106](https://github.com/CLDMV/gh-broker/pull/106)), with `@vitest/coverage-v8`, `@vitest/mocker` and `@vitest/spy` following to 5.0.3 (the `@vitest/coverage-v8` range stays `^5.0.2`). Transitive, lockfile only: `vite` 8.3.1 → 8.3.2, `rolldown` and its platform bindings 1.2.11 → 1.2.12, `@oxc-project/types` 0.151.0 → 0.152.0, and `why-is-node-running` 3.2.2 → 3.2.1 (vitest 5.0.3 pins the older release).
31
- - `@types/node` `^26.6.3` → `^26.6.4` (locked 26.6.3 → 26.6.4) ([#109](https://github.com/CLDMV/gh-broker/pull/109)).
32
-
33
- `esbuild`, `typescript`, `@octokit/auth-app` and `@octokit/core` are unchanged.
34
-
35
- ---
36
-
37
- ## 📚 Documentation
38
-
39
- - **NEW:** [docs/changelog/v0/v0.6.1.md](./v0.6.1.md), this changelog.
40
- - README `✨ What's New` is promoted to v0.6.1.
41
-
42
- ---
43
-
44
- ## Upgrade notes
45
-
46
- - No breaking changes to the broker's verbs, configuration or installer; the source is identical to v0.6.0.
47
- - If the `upstream` URL in your config (the default is `https://api.githubcopilot.com/mcp/x/all`) ever answers with a redirect to a different origin, the SDK will no longer follow it. Point `upstream` at the final URL. It was not verified here whether the default endpoint redirects at all; if it does not, nothing changes for you.
48
- - Redeploy to pick up the new SDK; the development changes do not affect the installed broker.
@@ -1,71 +0,0 @@
1
- # gh-broker v0.7.0 Changelog
2
-
3
- **Release Date**: October 2026
4
- **Release Type**: Minor
5
- **Branch**: `release/0.7.0`
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- v0.7.0 adds `issue_labels`, a write verb that edits an issue's or PR's labels one at a time. The hosted MCP's `issue_write` treats `labels` as a full replacement, so changing one status label through it silently drops every other label on the issue ([#117](https://github.com/CLDMV/gh-broker/issues/117)). `issue_labels` uses GitHub's per-label endpoints instead and only touches the labels it is given. The advertised `issue_write` description now carries a warning about the replacement behavior and points at the new verb.
12
-
13
- The rest of the release is dependency updates. The only runtime packages that changed are `@modelcontextprotocol/sdk` 1.32.0 → 1.32.1 (documentation-only upstream) and the transitive `proxy-addr` 2.0.7 → 2.0.8 (a security fix). Everything else is development tooling.
14
-
15
- Nothing is breaking: `issue_labels` is a new allowlist entry, and every existing tool behaves as before. A redeploy is needed to pick up the new verb, since the installer merges the new `config.example.json` entry into the live config.
16
-
17
- ---
18
-
19
- ## ✨ Features
20
-
21
- ### `issue_labels`: add, remove, swap or set labels without touching the others ([#118](https://github.com/CLDMV/gh-broker/pull/118), closes [#117](https://github.com/CLDMV/gh-broker/issues/117))
22
-
23
- A custom verb (`mode: "write"`, `custom: true`) that runs on the write PAT (Issues: write) and works on issues and pull requests alike.
24
-
25
- - **`add` / `remove`** take one label or several and combine in one call. A label already present (`add`) or absent (`remove`) is a no-op, not an error.
26
- - **`swap: { prefix, to }`** removes every current label starting with `prefix` and adds `to`, e.g. `{ "prefix": "status: ", "to": "status: verified" }`. A `to` label that is already on the issue stays in place rather than being removed and re-added.
27
- - **`set`** replaces the whole set, only as an explicit choice. It cannot be combined with the other operations, and an empty `set` is refused so a call can never clear every label.
28
- - **Unknown labels are refused.** Every name on the add side is looked up first (case-insensitively, applying the repo's own spelling). A missing one fails the call with nothing written, unless `create_missing` is set, in which case the label is created (`color` defaults to `ededed`).
29
- - **Add before remove.** Writes run in a fixed order: create missing labels, one POST for the whole add side, then the DELETEs. A failure part-way never leaves an issue with fewer labels than it should have; a `swap` whose POST is rate-limited keeps its old status label.
30
- - **Partial failures are reported.** If any write fails, the error names what was applied and what was not, and keeps the HTTP status. A failure before the first write passes through unchanged, since nothing was written.
31
- - **Strict arguments.** The handler enforces its schema: an unknown key (a typo such as `remvoe`), a non-integer `issue_number`, a non-boolean `create_missing`, an empty list, a bad `color`, or more than 100 labels refuses the call before any request is made. Naming the same label on both the add and remove sides is refused too.
32
- - **Result.** The issue's resulting label set, plus what this call `added`, `removed` and `created`.
33
-
34
- Full parameter table and examples: [docs/TOOLS.md](https://github.com/CLDMV/gh-broker/blob/master/docs/TOOLS.md#issue_labels).
35
-
36
- ### `issue_write` warns that `labels` replaces the whole set ([#118](https://github.com/CLDMV/gh-broker/pull/118))
37
-
38
- The broker appends a warning to the advertised description of the proxied `issue_write` tool: `labels` replaces the issue's whole label set. When `issue_labels` is allowlisted, the warning points at it; otherwise it says to read the current labels first and pass the full set back. The tool's schema and its forwarded behavior are unchanged.
39
-
40
- ---
41
-
42
- ## 📚 Documentation
43
-
44
- - **NEW:** [docs/changelog/v0/v0.7.0.md](./v0.7.0.md), this changelog.
45
- - `docs/TOOLS.md` gains an `issue_labels` section, and its tool-category table lists the new verb and the `issue_write` description warning.
46
- - README: a feature section for label edits, the new `src/verbs/issue_labels.ts` in the layout, and `✨ What's New` promoted to v0.7.0.
47
-
48
- ---
49
-
50
- ## 🔧 Dependencies
51
-
52
- ### Runtime
53
-
54
- - `@modelcontextprotocol/sdk` `^1.32.0` → `^1.32.1` (locked 1.32.0 → 1.32.1) ([#114](https://github.com/CLDMV/gh-broker/pull/114)). The upstream release contains only documentation changes.
55
- - `proxy-addr` 2.0.7 → 2.0.8, transitive, lockfile only ([#113](https://github.com/CLDMV/gh-broker/pull/113)). Fixes CVE-2026-90711 (GHSA-jqcg-44mw-7w3h): IPv4 trust granted through IPv4-mapped IPv6 subnets with a short prefix. It arrives through the SDK's HTTP server dependencies; the broker serves MCP over a local Unix socket and does not use that server, but the patched version is now what gets installed.
56
-
57
- ### Development
58
-
59
- - `@cldmv/configs` `^1.2.4` → `^1.3.0` ([#115](https://github.com/CLDMV/gh-broker/pull/115)). Version 1.3.0 adds shared Prettier, ESLint and Vitest configs and makes the ESLint plugins it configures regular dependencies of the package, which adds 147 dev-only packages to the lockfile (`eslint`, the `@eslint/*` language plugins, `@cldmv/eslint-plugin-jsonv` and their dependencies). The broker only uses the package's `fix-headers.json`, which is unchanged.
60
- - `@cldmv/fix-headers` `^2.2.0` → `^2.2.1`, `@cldmv/vitest-runner` `^1.5.3` → `^1.5.4` and `@types/node` `^26.6.4` → `^26.6.5` ([#114](https://github.com/CLDMV/gh-broker/pull/114)). The two CLDMV packages ship nothing new; both releases only update their own development setup.
61
- - `source-map-js` 1.2.1 → 1.2.2, transitive, dev-only ([#113](https://github.com/CLDMV/gh-broker/pull/113)). Fixes CVE-2026-93749, a denial of service from malicious indexed source maps.
62
-
63
- `@octokit/auth-app`, `@octokit/core`, `esbuild`, `typescript`, `vitest` and `@vitest/coverage-v8` are unchanged.
64
-
65
- ---
66
-
67
- ## Upgrade notes
68
-
69
- - No breaking changes. Every existing verb and proxied tool behaves as in v0.6.1; the only visible change to an existing tool is the extra warning in `issue_write`'s description.
70
- - Re-run the installer to deploy: it merges the new `issue_labels` allowlist entry into the live config, rebuilds and restarts the service. MCP clients need to reconnect afterwards to see the new tool.
71
- - `issue_labels` uses the existing write PAT and needs no new token permissions (Issues: write is already part of the write token's scope).
@@ -1,115 +0,0 @@
1
- # gh-broker v0.8.0 Changelog
2
-
3
- **Release Date**: October 2026
4
- **Release Type**: Minor
5
- **Branch**: `release/0.8.0`
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- v0.8.0 adds two write integrations that let an agent do work which used to need a token in its own hands or a human at a web form. `npm_scaffold` creates a new npm package and registers its trusted publisher, so the repo's CI can publish the first real release through OIDC. `ghcr_push` pushes a `docker save` archive to the GitHub Container Registry with a credential the agent never sees, with `ghcr_push_status` and `ghcr_tags` to follow the job and check the result.
12
-
13
- Both are optional. Their credentials (an npm granular token and a classic `write:packages` PAT) can be skipped at install, and while one isn't stored the broker starts normally and leaves the matching tools out of the tool list. The installer also gains a `--secret <cred-name>=<token>` flag, and supplying a token explicitly now replaces the stored one on the `file` backend as well.
14
-
15
- Owner-specific settings no longer ship in `config.example.json`: the installer takes them from `--config <file>` or a flag per setting, and an existing install keeps what it has.
16
-
17
- Nothing is breaking. Every existing verb and proxied tool behaves as in v0.7.0. The only runtime dependency change is `@modelcontextprotocol/sdk` 1.32.1 → 1.32.2.
18
-
19
- ---
20
-
21
- ## ✨ Features
22
-
23
- ### `npm_scaffold`: create an npm package and its trusted publisher ([#127](https://github.com/CLDMV/gh-broker/pull/127))
24
-
25
- npm only accepts a trusted publisher on a package that already exists, so every new package needed a hand-run first publish and a form on npmjs.com. `npm_scaffold` (`mode: "write"`, `custom: true`) does both.
26
-
27
- - **Placeholder publish.** The broker builds a tarball itself (a `package.json` at `0.0.0-stage` marked `scaffoldedBy: "gh-broker"`, and a README pointing at the repo) and publishes it. Access is `public` for a public repo and `restricted` for a private one.
28
- - **Trusted publisher.** It then registers the repo's publish workflow (default `publish.yml`) as the package's GitHub Actions trusted publisher, the same request `npm trust github` sends.
29
- - **One OTP per write.** The broker's npm token has no 2FA bypass, so each npm write carries a one-time password from the user. The first call publishes and returns `status: "needs_otp"` with the trust step remaining; a second call with the next code adds the trusted publisher. A refused or expired code also returns `needs_otp` with what is left.
30
- - **Create-only.** The scope must be in `config.npm.scopes`. The repo must exist under exactly the owner and name given (a redirect from a rename or transfer is refused), must not be archived, and its `package.json` on the default branch must name this package and not be private. The publish workflow must exist. A package already on the registry is refused, unless it is this verb's own placeholder for the same repo, which is how an interrupted call resumes. The lookup reads the registry primary (`?write=true`), not a cache.
31
- - **Existing trust.** If npm answers that a trusted publisher already exists (HTTP 409), the configuration is read back: the same repo and workflow counts as done, anything else fails naming what is configured.
32
- - **No secrets out.** Neither the OTP nor the token appears in a result, an error or the audit log.
33
-
34
- Installer support: `--npm` runs an npm web login, asks for the npm password and an OTP (both hidden, the password is never stored), and creates a granular token for the chosen scopes (`--npm-scope`, repeatable) with `--npm-token-days` lifetime (default 90). `--npm-org` (repeatable) also grants org read-write and is saved as `npm.tokenOrgs` so a rotation creates the same kind of token. Unattended installs take an existing token from `--secrets-file` or `GH_BROKER_SECRET_NPM_TOKEN`. Rotating says plainly that the old token stays valid until it expires or is revoked.
35
-
36
- Full reference: [docs/TOOLS.md](https://github.com/CLDMV/gh-broker/blob/master/docs/TOOLS.md#npm_scaffold) and [deploy/README.md](https://github.com/CLDMV/gh-broker/blob/master/deploy/README.md#npm-integration-npm_scaffold).
37
-
38
- ### `ghcr_push`, `ghcr_push_status` and `ghcr_tags`: GHCR pushes without an agent-held token ([#128](https://github.com/CLDMV/gh-broker/pull/128))
39
-
40
- `ghcr_push` pushes an image archive to `ghcr.io/<owner>/<image>:<tag>` using the owner's GHCR credential from the new `config.ghcr` block, a classic PAT with `write:packages` (GHCR does not accept fine-grained PATs).
41
-
42
- - **Archives.** Both `docker save` formats are read: the OCI image layout from Docker 25+ (also `buildx --output type=oci`) and the older docker-archive. An archive must hold exactly one single-platform image; attestation manifests are ignored. Compressed layers are pushed byte-for-byte, uncompressed layers are gzipped while they upload.
43
- - **Push flow.** The OCI distribution API from Node, with no docker socket and no extra binaries. Blobs the registry already has are skipped, so re-pushing an unchanged image uploads nothing. Nothing is buffered in memory or written to disk.
44
- - **Background jobs.** The call validates, exchanges the PAT for a registry token, checks tag protection and returns a job id at once. Jobs run one at a time, at most 8 queued or running. `ghcr_push_status` reports the phase, blob counts and bytes, then the manifest digest or the error. Job state is in memory only; a push cut off by a restart leaves nothing tagged.
45
- - **`ghcr_tags`** lists an allowlisted image's tags with a pull-only token, to check that a push landed.
46
- - **Guardrails, all before any upload.** The image must match the owner's `images` allowlist (`*` stays within one path segment). The archive must sit inside `ghcr.dropDir`: the path is checked as text, the file is opened once with `O_NOFOLLOW`, and the open descriptor must be a regular file with a single link whose real path is inside the real drop directory, so a symlink, hard link or symlinked subdirectory can't point the broker at a file the agent can't read. With `protectReleaseTags` (default on), an existing `1.2.3` / `v1.2.3` tag is never overwritten, checked at submit and again before the manifest is written.
47
- - **Credential containment.** The PAT is only sent to `https://ghcr.io/token`, the registry token only to `https://ghcr.io`, redirects are never followed, and every error passes through a scrubber before it reaches a result, audit line or log.
48
-
49
- GHCR is set up per install with `--ghcr-owner`, `--ghcr-username` and `--ghcr-image` (or a `--config` file), and the credential is `<org>-packages`. The installer creates the drop directory as `2750 <agent-user>:gh-broker`.
50
-
51
- Full reference and threat model: [docs/TOOLS.md](https://github.com/CLDMV/gh-broker/blob/master/docs/TOOLS.md#ghcr_push) and [deploy/README.md](https://github.com/CLDMV/gh-broker/blob/master/deploy/README.md#configuring-the-install).
52
-
53
- ### Optional credentials hide their tools instead of blocking startup ([#127](https://github.com/CLDMV/gh-broker/pull/127), [#128](https://github.com/CLDMV/gh-broker/pull/128))
54
-
55
- The npm token and the GHCR credentials are optional. The installer lets them be skipped (Enter at the prompt; an unattended run skips with a note), and only stored credentials go into the systemd drop-in. At startup the broker leaves a tool whose credential isn't stored out of the tool list, refuses a call to it like an unlisted tool, and logs why. Only the owners' GitHub PATs are required.
56
-
57
- ### Generic shipped config: `--config` and a flag per setting ([#135](https://github.com/CLDMV/gh-broker/pull/135))
58
-
59
- `config.example.json` now ships only the generic defaults: the socket, the upstream and the tools allowlist. It used to carry this project's own owner, GHCR account and image list, and the installer forced them onto every install, so anyone installing the package was asked for `cldmv-read`, `cldmv-write` and `cldmv-packages`.
60
-
61
- Owner-specific settings now come from the operator, layered from lowest to highest precedence: the shipped defaults, the deployed config, a `--config <file>`, then the CLI flags. There is a flag for every setting: `--owner <org>` (repeatable, credentials `<org>-read` / `<org>-write`), `--default-owner`, `--listen`, `--upstream`, `--installation-id`, `--app-id`, `--app-private-key`, `--secrets-path`, `--npm-registry`, and for GHCR `--ghcr-owner`, `--ghcr-username`, `--ghcr-image`, `--ghcr-drop-dir`, `--ghcr-max-archive-bytes` and `--[no-]protect-release-tags`.
62
-
63
- - The tools allowlist still always comes from the repo. A `--config` file that sets `tools`, has an unknown top-level key or holds anything shaped like a token is refused.
64
- - A first interactive install asks for the owner; an unattended one with no owner fails and names `--owner` / `--config`.
65
- - An upgrade without config flags keeps the deployed owners and GHCR settings as they are.
66
- - `config.sample.json` shows a full config with placeholder values. Nothing loads it.
67
-
68
- Full reference: [deploy/README.md](https://github.com/CLDMV/gh-broker/blob/master/deploy/README.md#configuring-the-install).
69
-
70
- ### Installer: `--secret <cred-name>=<token>` ([#127](https://github.com/CLDMV/gh-broker/pull/127))
71
-
72
- A fourth way to supply a token, repeatable, taking precedence over `--secrets-file`, `GH_BROKER_SECRET_<NAME>` and the prompt. A name the config doesn't use is refused, so a typo can't silently do nothing. It shows up in the process list and shell history, so the file or env are still the better choice for real tokens.
73
-
74
- ---
75
-
76
- ## 🐛 Bug Fixes
77
-
78
- ### The `file` secrets backend ignored a newly supplied token ([#127](https://github.com/CLDMV/gh-broker/pull/127))
79
-
80
- When a token was passed through `--secrets-file` or env for a credential that was already stored, the `file` backend kept the old value. It now replaces it, which is what `systemd-creds` already did and what the docs described.
81
-
82
- ---
83
-
84
- ## 📚 Documentation
85
-
86
- - **NEW:** [docs/changelog/v0/v0.8.0.md](./v0.8.0.md), this changelog.
87
- - `docs/TOOLS.md` gains sections for `npm_scaffold`, `ghcr_push`, `ghcr_push_status`, `ghcr_tags`, and the GHCR configuration and threat model.
88
- - `deploy/README.md` is now generic (`<org>` placeholders in place of this project's own setup) and documents the npm integration, the GHCR classic PAT and drop directory, `--config`, every config flag and `--secret`. `docs/deploy-architecture.md` lists `ghcr` as an example-owned config field.
89
- - README: feature sections for npm package creation and container image pushes, and `✨ What's New` promoted to v0.8.0.
90
-
91
- ---
92
-
93
- ## 🔧 Dependencies
94
-
95
- All patch bumps, in [#132](https://github.com/CLDMV/gh-broker/pull/132), which replaces the Dependabot PRs [#121](https://github.com/CLDMV/gh-broker/pull/121) and [#131](https://github.com/CLDMV/gh-broker/pull/131).
96
-
97
- ### Runtime
98
-
99
- - `@modelcontextprotocol/sdk` `^1.32.1` → `^1.32.2`.
100
-
101
- ### Development
102
-
103
- - `@cldmv/fix-headers` `^2.2.1` → `^2.2.3`, `@cldmv/vitest-runner` `^1.5.4` → `^1.5.6`, `@types/node` `^26.6.5` → `^26.6.6`, `vitest` and `@vitest/coverage-v8` → `^5.0.5`, and the transitive `vite` → 8.3.5.
104
-
105
- `@octokit/auth-app`, `@octokit/core`, `esbuild` and `typescript` are unchanged. The new verbs add no dependencies.
106
-
107
- ---
108
-
109
- ## Upgrade notes
110
-
111
- - A fresh unattended install now needs `--owner <org>` (or a `--config` file with an `owners` block); it used to pick up the owner shipped in `config.example.json`. Upgrades are not affected.
112
- - No breaking changes. Without an `npm` block or a stored GHCR credential, the broker behaves exactly as v0.7.0 and the new tools are not exposed.
113
- - Re-run the installer to deploy: it merges the new allowlist entries into the live config, rebuilds and restarts. Existing owners and settings are kept. MCP clients need to reconnect to see new tools.
114
- - To enable `npm_scaffold`, run the installer with `--npm` (add `--npm-org <org>` if creating packages in the scope needs org permissions).
115
- - To enable `ghcr_push`, create a classic PAT with `write:packages` only ([create the token](https://github.com/settings/tokens/new?scopes=write:packages&description=gh-broker%20ghcr)), and run the installer with `--ghcr-owner <org> --ghcr-username <login> --ghcr-image <name>` (repeat `--ghcr-image` per image). It asks for the token as `<org>-packages`, or takes it from `--secret <org>-packages=<token>`.
@@ -1,84 +0,0 @@
1
- # Deploy architecture
2
-
3
- How gh-broker is installed, configured, and upgraded. The guiding goals are **portability** (copy the repo to another machine, run one installer, it works), a **clean upgrade path** (updates never lose operator-set data), and **platform-neutrality** (Linux today, with seams so macOS/Windows are additive later — not a rewrite).
4
-
5
- ## Config: one portable, token-free file, regenerated by layering
6
-
7
- There is a single config file. It is **portable** and **contains no secrets** — only the tool allowlist, owner names, and credential *references* (`"read": "cred:acme-read"`). Keeping tokens out of it is what makes it safe to copy, commit, and carry between machines; a file designed to be copied must never hold live credentials.
8
-
9
- On install *and* upgrade the deployed config is **regenerated by layering**, not overwritten. Lowest to highest precedence:
10
-
11
- ```
12
- new_config = flags( config_file( merge(example_defaults, current_config) ) )
13
- ```
14
-
15
- - `example_defaults` — `config.example.json`, shipped with the code and generic: `listen`, `upstream` and the `tools` allowlist, with nothing specific to any deployment. Updated freely on upgrade; this is where allowlist changes arrive.
16
- - `current_config` — whatever is already deployed on this machine (absent on a first install).
17
- - `config_file` — the operator's `--config <file>`, same shape as the deployed config, merged on top. Only `cred:` references are allowed; a value shaped like a token is refused.
18
- - `flags` — one command-line flag per setting (`--owner`, `--listen`, `--ghcr-owner`, ...), applied last.
19
-
20
- The merge is **field-level**, because the layers have different owners:
21
-
22
- | Field | Source of truth | Why |
23
- |---|---|---|
24
- | `tools` (the allowlist) | **example** (repo), always | It is a security control. A removal or tightening in an update must apply; a naive "current wins" would silently keep a tool the repo dropped. Neither a `--config` file nor a flag can change it. |
25
- | `owners`, `ghcr`, `app`, `secrets`, `npm`, `listen`, `upstream`, `defaultOwner` | **operator**: flags, then `--config`, then **current** (deployed), then the example | Deployment data. The example ships none of the owner-specific ones, so a first install gets its owners from `--owner`, `--config` or an interactive prompt. Higher layers merge on top of the deployed copy (per owner and per field), and a run with no config flags leaves the deployed values untouched. `ghcr` holds only a `cred:` reference, never the token. |
26
-
27
- So an upgrade brings new defaults and allowlist changes through the example, while everything the operator configured survives. Nothing is blindly clobbered.
28
-
29
- ## Secrets: separate, agent-unreadable, and the storage backend is the operator's choice
30
-
31
- Secrets never live in the config — the config carries only `cred:<name>` references. The tokens live in a separate store the agent user cannot read, behind a **provider seam** (`config.secrets.provider`) the installer sets from a prompt. Two providers ship:
32
-
33
- - **`systemd-creds`** (default, recommended on Linux): tokens are encrypted with a host/TPM-bound key as `.cred` files, delivered by a unit drop-in (`LoadCredentialEncrypted`) into a memory-only `$CREDENTIALS_DIRECTORY` at start. **Encrypted at rest** and resistant to backups/disk theft (the key isn't beside the ciphertext); not portable to another machine.
34
- - **`file`**: a JSON `{ "<name>": "<token>" }` map at `0640 root:gh-broker` inside the `0750` config dir. **Portable and platform-neutral**; at-rest protection is filesystem permissions only (plaintext on disk).
35
-
36
- The **agent-read boundary is identical either way** — it's filesystem permissions (the agent isn't in the `gh-broker` group), not encryption. The choice trades *at-rest protection* (systemd-creds) against *portability* (file); it does not weaken the agent boundary. This is a genuine trade because the daemon must auto-start, so any decryption key must be available without a human at boot — which only a host/TPM-bound key (systemd-creds) achieves without leaving the key beside the ciphertext.
37
-
38
- The installer moves tokens between backends without re-entry where it can: switching to `file` decrypts the existing host-bound `.cred` files (as root, same host) into the JSON; switching to `systemd-creds` encrypts the tokens into `.cred` files. It prompts only for tokens it can't find. Further providers (OS keychain, DPAPI, encrypted vault) slot into the same seam without touching the daemon's call sites.
39
-
40
- ## Paths: resolved, not hardcoded
41
-
42
- Neither the config nor the secrets path is hardcoded to `/etc`. Both resolve through the same order:
43
-
44
- 1. explicit `--config` flag / `GH_BROKER_CONFIG` (and `GH_BROKER_SECRETS`) — highest,
45
- 2. the platform's system config dir — Linux `/etc/gh-broker`, macOS `/Library/Application Support/gh-broker`, Windows `%ProgramData%\gh-broker`,
46
- 3. the packaged default.
47
-
48
- Only the Linux defaults are exercised today; the others are wired so a later platform port is a table entry, not surgery.
49
-
50
- ## Node runtime: a maintained symlink, no runtime resolution
51
-
52
- The service runs jailed (`ProtectHome`), so it cannot use a `/home` nvm, and there is no login shell at start. Rather than have the unit *resolve* node at runtime, the installer maintains a **stable symlink** — `/usr/local/bin/node` → the chosen node — and the unit simply runs it:
53
-
54
- ```
55
- ExecStart=/usr/local/bin/node /opt/gh-broker/dist/index.mjs --config <resolved>
56
- ```
57
-
58
- The symlink is the single indirection: the CLI (via `PATH`) and the service resolve the *same* link, so "CLI node == service node" holds by construction. Bumping node is re-pointing the symlink — which is exactly the node step the installer runs on setup and on update, so it is not a special case. This removes `launch.sh` (a runtime nvm resolver that duplicated the symlink) and keeps the hardened unit free of a `bash`/`nvm` startup dependency.
59
-
60
- ## Tooling: Node, with one optional shell bootstrap
61
-
62
- All install/deploy *logic* is Node (`deploy/install.mjs` + `deploy/lib/*.mjs`): config layering and validation (validation reuses the daemon's own loader via `node dist/index.mjs --check`), the interactive prompts, the privileged system operations (shelled out to `useradd` / `systemctl` / `rsync`), building `dist/`/`node_modules/` as the checkout's own owner via `sudo -u '#<uid>'` rather than leaving them root-owned, and the smoke test (a real MCP client over the socket). One language, testable with the repo's vitest suite.
63
-
64
- The only thing that can't be Node is bootstrapping node itself. So the entrypoint is **`sudo node deploy/install.mjs`** with node ≥20 as the one prerequisite — which runs on any platform node runs on. For zero-prerequisite convenience on Linux, an **optional** `deploy/install.sh` (~20 lines) sets up nvm + node + the symlink if node is absent, then `exec`s the Node installer. It is a convenience, not the real installer.
65
-
66
- ## The installer flow (install and upgrade are the same command)
67
-
68
- `sudo node deploy/install.mjs` is idempotent — first install and later upgrade are one command:
69
-
70
- 1. detect platform + check prerequisites (`socat`, `rsync`, `systemctl`, `sudo`),
71
- 2. **create or confirm the agent user** — hard stop if it can't (no silent warn-and-continue),
72
- 3. ensure node on a stable path + the `/usr/local/bin/node` symlink,
73
- 4. **regenerate the config** by layering the example, the deployed config, `--config` and the flags (a first install with no owner asks for one),
74
- 5. **choose the secret backend and store the PATs** — keep existing, migrate a legacy `systemd-creds` token, or take it from `--secrets-file` / env / a prompt (see the Secrets section),
75
- 6. validate the config with the real loader (`node dist/index.mjs --check`),
76
- 7. build + deploy (unit → `/usr/local/bin/node`, `dist/`, config, secrets),
77
- 8. **smoke test** — MCP `initialize` + one real read over the socket → ✅/❌.
78
-
79
- The same flow runs **fully unattended** via flags — `--agent-user`, `--secrets-provider`, `--secrets-file` / `GH_BROKER_SECRET_<NAME>`, `--create-agent-user`, `--non-interactive`, and the config flags (`--owner`, `--config`, ...); prompts are the default when flags are omitted. See [`../deploy/README.md`](../deploy/README.md) and `--help`.
80
-
81
- ## What is deliberately deferred
82
-
83
- - Non-Linux secret providers and path defaults (the seams exist; the implementations are additive).
84
- - `link_issue` (App-token-only) stays deferred — implemented but not advertised or wired; see [`../deploy/README.md`](../deploy/README.md).
package/src/appauth.ts DELETED
@@ -1,56 +0,0 @@
1
- /**
2
- *
3
- * @Project: @cldmv/gh-broker
4
- * @Filename: /src/appauth.ts
5
- * @Date: 2026-07-07T18:39:48-07:00 (1783474788)
6
- * @Author: Nate Corcoran <CLDMV>
7
- * @Email: <Shinrai@users.noreply.github.com>
8
- * -----
9
- * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
10
- * @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
11
- * -----
12
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
13
- *
14
- */
15
-
16
- import { Octokit } from "@octokit/core";
17
- import { createAppAuth } from "@octokit/auth-app";
18
- import type { Config, SecretStore } from "./types.ts";
19
-
20
- /**
21
- * Provides per-owner GitHub App–authenticated Octokit clients for the operations
22
- * that fine-grained PATs can't do (`link_issue` → addClosingIssueReference, which
23
- * is not in the fine-grained-PAT GraphQL schema — only App/classic tokens see it).
24
- *
25
- * `@octokit/auth-app` handles the JWT signing + installation-token minting/refresh
26
- * internally, so there's no hand-rolled token machinery here. The App private key
27
- * lives only in the broker (systemd-creds), never in the agent.
28
- */
29
- export class AppAuth {
30
- private readonly config: Config;
31
- private readonly appId: number;
32
- private readonly privateKey: string;
33
- private readonly cache = new Map<number, Octokit>();
34
-
35
- constructor(config: Config, secrets: SecretStore) {
36
- if (!config.app) throw new Error("AppAuth: no app config");
37
- this.config = config;
38
- this.appId = config.app.appId;
39
- this.privateKey = secrets.resolve(config.app.privateKey);
40
- }
41
-
42
- /** App-authed Octokit for this owner's installation, or null if none configured. */
43
- octokitFor(owner: string): Octokit | null {
44
- const installationId = this.config.owners[owner]?.installationId;
45
- if (installationId === undefined) return null;
46
- let ok = this.cache.get(installationId);
47
- if (!ok) {
48
- ok = new Octokit({
49
- authStrategy: createAppAuth,
50
- auth: { appId: this.appId, privateKey: this.privateKey, installationId }
51
- });
52
- this.cache.set(installationId, ok);
53
- }
54
- return ok;
55
- }
56
- }
package/src/audit.ts DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- *
3
- * @Project: @cldmv/gh-broker
4
- * @Filename: /src/audit.ts
5
- * @Date: 2026-07-07T00:24:51-07:00 (1783409091)
6
- * @Author: Nate Corcoran <CLDMV>
7
- * @Email: <Shinrai@users.noreply.github.com>
8
- * -----
9
- * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
10
- * @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
11
- * -----
12
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
13
- *
14
- */
15
-
16
- /**
17
- * Structured audit log. One JSON line per decision/call to stdout (journald
18
- * captures it). NEVER include token values — only tool, owner, mode, repo, result.
19
- */
20
-
21
- export interface AuditEntry {
22
- tool: string;
23
- owner?: string;
24
- mode?: "read" | "write";
25
- repo?: string;
26
- decision: "forward" | "custom" | "augment" | "deny" | "error";
27
- ok?: boolean;
28
- detail?: string;
29
- }
30
-
31
- export function audit(entry: AuditEntry): void {
32
- // eslint-disable-next-line no-console -- journald is the sink
33
- console.log(JSON.stringify({ ts: new Date().toISOString(), ...entry }));
34
- }
@@ -1,42 +0,0 @@
1
- /**
2
- *
3
- * @Project: @cldmv/gh-broker
4
- * @Filename: /src/availability.ts
5
- * @Date: 2026-10-31T12:09:43-07:00 (1793473783)
6
- * @Author: Nate Corcoran <CLDMV>
7
- * @Email: <Shinrai@users.noreply.github.com>
8
- * -----
9
- * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
10
- * @Last modified time: 2026-10-31T13:14:38-07:00 (1793477678)
11
- * -----
12
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
13
- *
14
- */
15
-
16
- /**
17
- * Tools that depend on an optional integration. An allowlisted tool whose integration isn't
18
- * configured, or whose credential isn't stored, is unavailable: the catalog doesn't advertise
19
- * it and the router refuses it like a tool that isn't allowed. Everything else keeps working.
20
- * Catalog and router both ask here, so they can never disagree.
21
- */
22
-
23
- import type { Config, SecretStore, ToolRule } from "./types.ts";
24
- import { ghcrCredRefs } from "./verbs/ghcr/config.ts";
25
-
26
- const GHCR_TOOLS = new Set(["ghcr_push", "ghcr_push_status", "ghcr_tags"]);
27
-
28
- /** Why `rule` can't be used on this broker, or undefined when it can. */
29
- export function unavailableReason(rule: ToolRule, config: Config, secrets: SecretStore): string | undefined {
30
- if (rule.name === "npm_scaffold") {
31
- if (!config.npm) return "the npm integration is not configured (enable it with the installer's --npm)";
32
- if (!secrets.has(config.npm.token)) return `its credential "${config.npm.token.replace(/^cred:/, "")}" is not stored (re-run the installer with --npm)`;
33
- }
34
- if (GHCR_TOOLS.has(rule.name)) {
35
- if (!config.ghcr) return "GHCR pushes are not configured (config.ghcr)";
36
- const refs = ghcrCredRefs(config.ghcr);
37
- if (!refs.some((ref) => secrets.has(ref))) {
38
- return `no GHCR credential is stored (${refs.map((r) => `"${r.replace(/^cred:/, "")}"`).join(", ")}; re-run the installer to add one)`;
39
- }
40
- }
41
- return undefined;
42
- }