code-foundry 0.32.4 → 0.34.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.
@@ -0,0 +1,183 @@
1
+ name: Code Foundry Validation
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ mode:
7
+ description: Validation tier (fast, audit, or release).
8
+ required: true
9
+ type: string
10
+ runtime-repository:
11
+ description: Repository containing the Code Foundry runtime.
12
+ required: false
13
+ type: string
14
+ default: 0xPlayerOne/code-foundry
15
+ runtime-ref:
16
+ description: Code Foundry runtime tag or ref.
17
+ required: false
18
+ type: string
19
+ default: v0.32.4
20
+ ci-runner:
21
+ description: Runner used by CI jobs.
22
+ required: false
23
+ type: string
24
+ default: ubuntu-latest
25
+ test-runner:
26
+ description: Runner used by non-unit test jobs.
27
+ required: false
28
+ type: string
29
+ default: ubuntu-latest
30
+ unit-runner:
31
+ description: Runner used by unit tests.
32
+ required: false
33
+ type: string
34
+ default: ubuntu-slim
35
+ security-runner:
36
+ description: Runner used by Security jobs.
37
+ required: false
38
+ type: string
39
+ default: ubuntu-slim
40
+ codeql-runner:
41
+ description: Runner used by CodeQL jobs.
42
+ required: false
43
+ type: string
44
+ default: ubuntu-latest
45
+ rust-shards:
46
+ description: JSON array of Rust scope sharding values. Use ["all"] for single-pass behavior.
47
+ required: false
48
+ type: string
49
+ default: '["all"]'
50
+ rust-threads:
51
+ description: Threads used for Rust extraction and analysis. Values above 1 opt into local parallelism.
52
+ required: false
53
+ type: string
54
+ default: '1'
55
+ rust-max-parallel:
56
+ description: Maximum Rust shard jobs allowed to run concurrently.
57
+ required: false
58
+ type: number
59
+ default: 1
60
+ secrets:
61
+ TURBO_TOKEN:
62
+ required: false
63
+ NEXTAUTH_SECRET:
64
+ required: false
65
+
66
+ # The orchestrator keeps the union of permissions needed by the chain it calls
67
+ # (CodeQL uploads require security-events: write). Nested reusable workflows
68
+ # can only maintain or reduce permissions, never elevate them.
69
+ permissions:
70
+ actions: read
71
+ contents: read
72
+ packages: read
73
+ security-events: write
74
+
75
+ jobs:
76
+ ci:
77
+ name: CI
78
+ if: inputs.mode == 'fast' || inputs.mode == 'audit'
79
+ uses: ./.github/workflows/ci.yml
80
+ with:
81
+ runtime-repository: ${{ inputs.runtime-repository }}
82
+ runtime-ref: ${{ inputs.runtime-ref }}
83
+ runner: ${{ inputs.ci-runner }}
84
+ secrets:
85
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
86
+ NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
87
+
88
+ test:
89
+ name: Test
90
+ if: inputs.mode == 'fast' || inputs.mode == 'audit'
91
+ uses: ./.github/workflows/test.yml
92
+ with:
93
+ runtime-repository: ${{ inputs.runtime-repository }}
94
+ runtime-ref: ${{ inputs.runtime-ref }}
95
+ runner: ${{ inputs.test-runner }}
96
+ unit-runner: ${{ inputs.unit-runner }}
97
+ unit-only: ${{ inputs.mode == 'fast' }}
98
+ secrets:
99
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
100
+
101
+ security:
102
+ name: Security
103
+ if: inputs.mode == 'audit'
104
+ uses: ./.github/workflows/security.yml
105
+ with:
106
+ runtime-repository: ${{ inputs.runtime-repository }}
107
+ runtime-ref: ${{ inputs.runtime-ref }}
108
+ runner: ${{ inputs.security-runner }}
109
+
110
+ codeql:
111
+ name: CodeQL
112
+ if: inputs.mode == 'audit'
113
+ uses: ./.github/workflows/codeql.yml
114
+ with:
115
+ runtime-repository: ${{ inputs.runtime-repository }}
116
+ runtime-ref: ${{ inputs.runtime-ref }}
117
+ runner: ${{ inputs.codeql-runner }}
118
+ rust-shards: ${{ inputs.rust-shards }}
119
+ rust-threads: ${{ inputs.rust-threads }}
120
+ rust-max-parallel: ${{ inputs.rust-max-parallel }}
121
+
122
+ release-policy:
123
+ name: Release Policy
124
+ if: inputs.mode == 'release'
125
+ runs-on: ubuntu-slim
126
+ timeout-minutes: 10
127
+ steps:
128
+ - name: Checkout
129
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
130
+ with:
131
+ persist-credentials: false
132
+ fetch-depth: 0
133
+ filter: blob:none
134
+ - name: Checkout runtime
135
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
136
+ with:
137
+ persist-credentials: false
138
+ repository: ${{ inputs.runtime-repository }}
139
+ ref: ${{ inputs.runtime-ref }}
140
+ path: .github/.code-foundry
141
+ sparse-checkout: |
142
+ src/lib
143
+ src/runtime.mjs
144
+ - name: Install runtime
145
+ run: mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
146
+ - name: Validate generated release diff
147
+ env:
148
+ FOUNDRY_BASE_SHA: ${{ github.event.pull_request.base.sha }}
149
+ FOUNDRY_HEAD_REF: ${{ github.event.pull_request.head.ref }}
150
+ FOUNDRY_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
151
+ FOUNDRY_REPOSITORY: ${{ github.repository }}
152
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation release_diff
153
+
154
+ # Aggregate check: "Validation / Gate" (caller job name + this job name).
155
+ # Always evaluates; only jobs required for the mode must have succeeded.
156
+ gate:
157
+ name: Gate
158
+ needs: [ci, test, security, codeql, release-policy]
159
+ if: always()
160
+ runs-on: ubuntu-slim
161
+ timeout-minutes: 10
162
+ env:
163
+ FOUNDRY_MODE: ${{ inputs.mode }}
164
+ FOUNDRY_CI: ${{ needs.ci.result }}
165
+ FOUNDRY_TEST: ${{ needs.test.result }}
166
+ FOUNDRY_SECURITY: ${{ needs.security.result }}
167
+ FOUNDRY_CODEQL: ${{ needs.codeql.result }}
168
+ FOUNDRY_RELEASE_POLICY: ${{ needs.release-policy.result }}
169
+ steps:
170
+ - name: Checkout runtime
171
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
172
+ with:
173
+ persist-credentials: false
174
+ repository: ${{ inputs.runtime-repository }}
175
+ ref: ${{ inputs.runtime-ref }}
176
+ path: .github/.code-foundry
177
+ sparse-checkout: |
178
+ src/lib
179
+ src/runtime.mjs
180
+ - name: Install runtime
181
+ run: mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
182
+ - name: Evaluate aggregate gate
183
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation gate
@@ -0,0 +1,82 @@
1
+ name: Code Foundry
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main, staging]
6
+ types:
7
+ - opened
8
+ - synchronize
9
+ - reopened
10
+ - ready_for_review
11
+ schedule:
12
+ - cron: '31 6 * * 1'
13
+ workflow_dispatch:
14
+
15
+ # Default every caller job to no repository permissions. Individual jobs grant
16
+ # only the scopes their own validation path consumes.
17
+ permissions: {}
18
+
19
+ # Superseded PR updates cancel the previous run; schedule and dispatch runs
20
+ # are keyed by event so periodic audits and manual runs stay independent.
21
+ concurrency:
22
+ group: code-foundry-validation-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
23
+ cancel-in-progress: true
24
+
25
+ jobs:
26
+ mode:
27
+ name: Mode
28
+ runs-on: ubuntu-slim
29
+ timeout-minutes: 10
30
+ permissions:
31
+ contents: read
32
+ outputs:
33
+ mode: ${{ steps.classify.outputs.mode }}
34
+ steps:
35
+ - name: Checkout runtime
36
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
37
+ with:
38
+ persist-credentials: false
39
+ repository: 0xPlayerOne/code-foundry
40
+ ref: ${{ github.sha }}
41
+ path: .github/.code-foundry
42
+ sparse-checkout: |
43
+ src/lib
44
+ src/runtime.mjs
45
+ - name: Install runtime
46
+ run: mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
47
+ - name: Classify validation mode
48
+ id: classify
49
+ env:
50
+ FOUNDRY_EVENT_NAME: ${{ github.event_name }}
51
+ FOUNDRY_BASE_REF: ${{ github.event.pull_request.base.ref }}
52
+ FOUNDRY_HEAD_REF: ${{ github.event.pull_request.head.ref }}
53
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation mode >> "$GITHUB_OUTPUT"
54
+
55
+ # The stable aggregate check name is the caller job name plus the
56
+ # orchestrator gate job name: "Validation / Gate". Keep both names fixed.
57
+ validation:
58
+ name: Validation
59
+ needs: mode
60
+ # Reusable workflows can only maintain or reduce the caller job's scopes.
61
+ # The audit tier needs security-events: write for CodeQL uploads.
62
+ permissions:
63
+ actions: read
64
+ contents: read
65
+ packages: read
66
+ security-events: write
67
+ uses: ./.github/workflows/validation.yml
68
+ with:
69
+ mode: ${{ needs.mode.outputs.mode }}
70
+ runtime-repository: 0xPlayerOne/code-foundry
71
+ runtime-ref: ${{ github.sha }}
72
+ ci-runner: ubuntu-latest
73
+ test-runner: ubuntu-latest
74
+ unit-runner: ubuntu-slim
75
+ security-runner: ubuntu-slim
76
+ codeql-runner: ubuntu-latest
77
+ rust-shards: '["all"]'
78
+ rust-threads: '1'
79
+ rust-max-parallel: 1
80
+ secrets:
81
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
82
+ NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.34.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.33.0...v0.34.0) (2026-08-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * **release:** classify reconcile push failures and log sanitized details ([#307](https://github.com/0xPlayerOne/code-foundry/issues/307)) ([0651fca](https://github.com/0xPlayerOne/code-foundry/commit/0651fca3eb4f11eeb21b91462397eef865eee7cc))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **release:** include ssh executable in reconcile transport ([#308](https://github.com/0xPlayerOne/code-foundry/issues/308)) ([49713ed](https://github.com/0xPlayerOne/code-foundry/commit/49713edeaf5262a0ed872aa6d3fdf55927b6e1c5))
14
+ * **release:** keep reconcile fetch over HTTPS ([#305](https://github.com/0xPlayerOne/code-foundry/issues/305)) ([022afad](https://github.com/0xPlayerOne/code-foundry/commit/022afade430a00b9656dcfa50af3056bb27a54e3))
15
+
16
+ ## [0.33.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.32.4...v0.33.0) (2026-08-01)
17
+
18
+
19
+ ### Features
20
+
21
+ * add tiered validation workflow ([#301](https://github.com/0xPlayerOne/code-foundry/issues/301)) ([dd3015e](https://github.com/0xPlayerOne/code-foundry/commit/dd3015ea22ab94b2ed3d55f6e900a04f3e968aeb))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * preserve regex escape in test assertion ([#303](https://github.com/0xPlayerOne/code-foundry/issues/303)) ([0a3c56f](https://github.com/0xPlayerOne/code-foundry/commit/0a3c56f5318baa8002b1079687797702f2c06383))
27
+
3
28
  ## [0.32.4](https://github.com/0xPlayerOne/code-foundry/compare/v0.32.3...v0.32.4) (2026-08-01)
4
29
 
5
30
 
package/README.md CHANGED
@@ -88,10 +88,13 @@ GitHub Code Quality or other paid GitHub features.
88
88
  See [Workflow and CI conventions](docs/WORKFLOWS.md) for triggers, required
89
89
  checks, runners, coverage, caching, and custom workflow extensions.
90
90
 
91
- The default contribution policy uses the `staging-release` workflow with
92
- rebase merges. Repositories can set `merge_strategy: squash` or
93
- `merge_strategy: merge` in `.github/code-foundry.yml` when their governance
94
- requires another merge convention.
91
+ The contribution policy uses the `staging-release` workflow: feature PRs squash
92
+ into `staging`, the promotion PR rebases into `main` (`merge_strategy: rebase`),
93
+ and Release Please version PRs squash into `main` (`release_merge_strategy:
94
+ squash`). Release automation never defaults to a merge method and never merges
95
+ with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on
96
+ any other strategy. GitHub Stacks is not part of this topology and does not
97
+ reduce the required workflow runs.
95
98
 
96
99
  ## Releases and publishing
97
100
 
@@ -48,7 +48,8 @@ repository manifests and source
48
48
  | `npm_publish` | `true` or `false` | Opt into npm publication |
49
49
  | `license` | `gpl-3.0-or-later`, `agpl-3.0-or-later`, `mit`, `preserve`, `none` | License policy; new repositories default to GPLv3 |
50
50
  | `git_workflow` | `staging-release` | Branch/release model; the standard model promotes `staging` into `main` |
51
- | `merge_strategy` | `rebase`, `squash`, `merge` | Preferred merge method for contribution and release PRs; defaults to `merge` |
51
+ | `merge_strategy` | `rebase` | Promotion merge method for `staging` `main`; the staging-release topology requires rebase |
52
+ | `release_merge_strategy` | `squash` | Merge method for Release Please version PRs into `main`; release automation fails closed unless squash |
52
53
  | `runner` fields | GitHub runner names | Per-workflow runner policy |
53
54
 
54
55
  Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
package/docs/RELEASES.md CHANGED
@@ -12,9 +12,13 @@ Keep commits Conventional Commit-shaped (`feat:`, `fix:`, `docs:`, `ci:`,
12
12
  `chore:`, and so on). Release Please uses them to select patch/minor/major
13
13
  versions and generate grouped changelog notes.
14
14
 
15
- The default `merge_strategy` is `merge`, matching promotion PRs from `staging` into `main`.
16
- Configure it to `squash` or `rebase` in `.github/code-foundry.yml` if a repository intentionally differs. The current
17
- supported `git_workflow` is `staging-release`.
15
+ The merge audit pins one merge method per transition in this topology. Feature
16
+ and fix branches land on `staging` with **squash** merges, the `staging` `main`
17
+ promotion PR merges with **rebase** (`merge_strategy: rebase`), and Release
18
+ Please version PRs merge with **squash** (`release_merge_strategy: squash`).
19
+ Release automation never defaults to a merge method and never merges with
20
+ `--admin`: the release workflow fails closed unless `release_merge_strategy` is
21
+ exactly `squash`. The current supported `git_workflow` is `staging-release`.
18
22
 
19
23
  The release workflow opens or updates a versioned PR after changes reach
20
24
  `main`. Merging that PR updates the changelog, creates the Git tag and GitHub
@@ -30,23 +34,27 @@ Set these values in `.github/code-foundry.yml`:
30
34
  ```yaml
31
35
  release_type: auto # auto, node, python, rust, simple, or none
32
36
  npm_publish: false # true only for an npm package
33
- merge_strategy: rebase # merge, squash, or rebase; used for promotion PRs
34
- release_merge_strategy: rebase # merge, squash, or rebase; used for Release Please PRs
37
+ merge_strategy: rebase # required: staging -> main promotion PRs rebase
38
+ release_merge_strategy: squash # required: Release Please version PRs squash only
35
39
  ```
36
40
 
37
41
  `merge_strategy` applies to promotion PRs (`staging` into `main`) and
38
- `release_merge_strategy` to Release Please version PRs; both default to
39
- `merge` when unset. Use **rebase** for promotion PRs and **squash** for
40
- Release Please PRs. Both keep `main` fully linear, which is what makes the
41
- post-release reconciliation possible: `staging` requires linear history, and
42
- any merge commit on `main` becomes an uncrossable barrier for `staging` (the
43
- linear-history rule rejects pushes whose new commits contain merge commits).
44
- Release Please itself also requires squash merges for its version PRs (rebase
45
- merges rewrite commit SHAs and break its merged-PR detection). With a linear
46
- `main`, the release workflow mirrors `staging` onto `main`'s tip after every
47
- release a verified fast-forward when possible, otherwise a forced linear
48
- update, otherwise a single-parent synchronization commit so `staging` is
49
- never left behind and never needs a manual rebase.
42
+ `release_merge_strategy` to Release Please version PRs; feature PRs into
43
+ `staging` use squash merges. The topology requires `merge_strategy: rebase`
44
+ and `release_merge_strategy: squash`; `code-foundry doctor` and
45
+ `code-foundry sync` reject any other value, and the release workflow fails
46
+ closed instead of falling back to `merge`. Both keep `main` fully linear,
47
+ which is what makes the post-release reconciliation possible: release-only
48
+ main commits cannot be discarded because they are allowed metadata-only, and
49
+ all non-metadata drift is rejected before mutation.
50
+
51
+ Patch-equivalent divergence between `main` and `staging` is treated as aligned.
52
+ When `staging` has pending commits that are not yet represented on `main`, the
53
+ release workflow replays those staging-only commits in order onto a detached
54
+ worktree rooted at `main`, and then updates `staging` with an exact
55
+ `--force-with-lease` to prevent
56
+ unintended branch rewrites. There is no unconditional mirror force-push and no
57
+ fallback synchronization commit path.
50
58
 
51
59
  `auto` selects a supported manifest. Use `simple` with `version.txt` for a
52
60
  repository without a package manifest and `none` for a repository that should
@@ -78,6 +86,6 @@ the generated version pull request before using the token to merge it.
78
86
 
79
87
  1. Merge tested changes from `staging` into `main`.
80
88
  2. Review the generated Release Please PR and changelog.
81
- 3. Merge the release PR with the repository.s configured `release_merge_strategy` (default `merge`, recommended `squash`).
89
+ 3. Merge the release PR with the repository's configured `release_merge_strategy` (**squash**; the release workflow fails closed on any other value).
82
90
  4. Confirm the GitHub Release and any package publication.
83
91
  5. Synchronize `staging` with the new `main` release commit.
package/docs/WORKFLOWS.md CHANGED
@@ -2,16 +2,23 @@
2
2
 
3
3
  ## Standard triggers
4
4
 
5
- The default standard callers use:
5
+ The canonical validation caller uses pull requests plus bounded audit entry
6
+ points; it does not run the same suites again on branch pushes:
6
7
 
7
8
  ```yaml
8
- push:
9
- branches: [main, staging]
10
9
  pull_request:
11
10
  branches: [main, staging]
11
+ schedule:
12
+ - cron: '31 6 * * 1'
13
+ workflow_dispatch:
12
14
  ```
13
15
 
14
- Draft PR automation may additionally listen to supported topic branches.
16
+ Pull requests into `staging` run the fast tier, ordinary pull requests into
17
+ `main` run the full audit tier, and exact Release Please pull requests into
18
+ `main` run only release policy. Scheduled and manual runs select the audit
19
+ tier. Draft PR automation separately listens to supported topic-branch pushes,
20
+ promotion automation listens to `staging` pushes, and release automation
21
+ listens to `main` pushes.
15
22
  Custom deployment, indexing, search, Slither, or other workflows are
16
23
  repository-owned extensions and should keep their own triggers and permissions.
17
24
 
@@ -31,6 +38,51 @@ Use concise job names such as `CI / Format`, `Test / Unit`, and
31
38
  `CodeQL / Analyze (Python)`. Required checks should match the jobs actually
32
39
  enabled for the repository profile.
33
40
 
41
+ ## Merge methods
42
+
43
+ The merge audit pins one merge method per transition. `code-foundry doctor`
44
+ and `code-foundry sync` fail closed on any other strategy, and the release
45
+ workflow refuses to run unless its strategy is exactly `squash`.
46
+
47
+ | Transition | Merge method | Enforcement |
48
+ | --- | --- | --- |
49
+ | Feature/fix PR into `staging` | Squash | Contribution policy; see `CONTRIBUTING.md` |
50
+ | `staging` → `main` promotion PR | Rebase (`merge_strategy: rebase`) | `merge_strategy` must be `rebase`; merge commits are rejected |
51
+ | Release Please version PR into `main` | Squash (`release_merge_strategy: squash`) | Release automation fails closed unless `squash`; never defaults to `merge`, never uses `--admin` |
52
+
53
+ Keeping `main` linear — rebase promotions and squash release PRs — is what
54
+ lets the post-release reconciliation fast-forward or replay `staging` safely.
55
+
56
+ Protect `staging` with the aggregate `Validation / Gate`, squash-only pull
57
+ requests, and a single GitHub Actions integration path. That path uses the
58
+ GitHub Actions integration token by default, and optionally an SSH deploy key
59
+ when `STAGING_DEPLOY_KEY` is configured. The deploy key is required only when
60
+ a personal-repository ruleset for `staging` enforces a Deploy Key bypass for
61
+ `release reconcile`; repositories without that bypass continue with tokenless
62
+ checkout + GitHub API calls.
63
+
64
+ When used, the optional `STAGING_DEPLOY_KEY` is written at runtime only for
65
+ reconcile, then used to set `GIT_SSH_COMMAND` and trusted host settings for
66
+ `release reconcile` over SSH push operations only; regular `git fetch` reads still
67
+ use HTTPS from the checked-out origin configuration. The key material is scrubbed
68
+ after the step.
69
+
70
+ When absent, the job keeps `GH_TOKEN = github.token` and runs `gh auth setup-git`
71
+ so repositories without a Deploy Key ruleset bypass still reconcile successfully.
72
+
73
+ For this reconciliation path, maintainer PATs and administrator roles are not
74
+ authorized bypasses; the job deliberately authenticates with `github.token`,
75
+ not `CODE_FOUNDRY_TOKEN` or `RELEASE_PLEASE_TOKEN`.
76
+
77
+ ## GitHub Stacks
78
+
79
+ GitHub Stacks (stacked pull requests) is not part of this topology and does
80
+ not reduce required workflow runs. Every pull request in a stack still
81
+ triggers its own validation run, and each branch keeps its own required
82
+ checks; stacking never collapses or skips a required check in the tiered
83
+ validation gate. Land changes through the standard `staging-release` flow
84
+ instead.
85
+
34
86
  ## Language defaults
35
87
 
36
88
  - TypeScript/JavaScript: ESLint, Prettier, and Bun's native `bun test`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "0.32.4",
3
+ "version": "0.34.0",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",
@@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process'
6
6
  import { includesValue, readConfig } from '../lib/config.mjs'
7
7
  import { recommendRunners, resolveProfile } from '../lib/profile.mjs'
8
8
  import { doctorGithub } from '../lib/github-doctor.mjs'
9
+ import { isGeneratedEventCaller } from './sync.mjs'
9
10
 
10
11
  /** @param {string} root @param {{ github?: boolean }} [options] */
11
12
  export function doctor(root, options = {}) {
@@ -68,15 +69,64 @@ export function doctor(root, options = {}) {
68
69
  }
69
70
  }
70
71
 
71
- for (const workflow of ['ci', 'codeql', 'security', 'test', 'draft-pr', 'release-pr', 'release']) {
72
- if (includesValue(config.features ?? 'all', workflow) && !existsSync(join(target, `.github/workflows/${workflow}.yml`))) error(`missing enabled workflow: ${workflow}.yml`)
72
+ const features = config.features ?? 'all'
73
+ const mergeStrategy = config.merge_strategy ?? 'rebase'
74
+ if (mergeStrategy !== 'rebase') {
75
+ error(`merge_strategy must be "rebase" for the staging-release promotion topology; got "${mergeStrategy}".`)
73
76
  }
74
- console.log('Remote CI, Test, Security, CodeQL, and release runtimes are loaded by reusable workflow wrappers.')
77
+ const releaseMergeStrategy = config.release_merge_strategy ?? ''
78
+ if (includesValue(features, 'release') && releaseMergeStrategy !== 'squash') {
79
+ error(`release_merge_strategy must be "squash" for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`)
80
+ }
81
+ for (const workflow of ['validation', 'draft-pr', 'release-pr', 'release']) {
82
+ if (includesValue(features, workflow) && !existsSync(join(target, `.github/workflows/${workflow}.yml`))) error(`missing enabled workflow: ${workflow}.yml`)
83
+ }
84
+ const validationEnabled = includesValue(features, 'validation') || ['ci', 'test', 'security', 'codeql'].some((legacy) => includesValue(features, legacy))
85
+ const validationCaller = ['validation.yml', 'validation_self-ci.yml']
86
+ .map((file) => join(target, `.github/workflows/${file}`))
87
+ .find((file) => existsSync(file) && /pull_request:/.test(readFileSync(file, 'utf8')))
88
+ if (validationEnabled && !validationCaller) {
89
+ error('missing tiered validation caller; run code-foundry sync to adopt validation.yml')
90
+ } else if (validationCaller) {
91
+ const caller = readFileSync(validationCaller, 'utf8')
92
+ if (!/^ validation:\n name: Validation/m.test(caller)) {
93
+ error('validation caller is missing the Validation job; the Validation / Gate aggregate check cannot form.')
94
+ }
95
+ if (!/uses:\s+(?:\.\/\.github\/workflows\/validation\.yml|\S+\/\.github\/workflows\/validation\.yml@)/.test(caller)) {
96
+ error('validation caller does not reference the validation orchestrator.')
97
+ }
98
+ const runtimeRef = caller.match(/^\s+runtime-ref:\s+(.+?)\s*$/m)?.[1]
99
+ const checkoutRef = caller.match(/^\s+ref:\s+(.+?)\s*$/m)?.[1]
100
+ if (!runtimeRef || !checkoutRef) {
101
+ error('validation caller is missing runtime ref wiring (mode checkout or orchestrator input).')
102
+ } else if (runtimeRef !== checkoutRef) {
103
+ error(`validation caller pins mismatched runtime refs (${runtimeRef} vs ${checkoutRef}).`)
104
+ } else if (runtimeRef !== '${{ github.sha }}' && !/^v\d+\.\d+\.\d+$/.test(runtimeRef)) {
105
+ warn(`validation caller runtime ref ${runtimeRef} is not a released tag; pin a vX.Y.Z tag.`)
106
+ }
107
+ }
108
+ const runtimeRepository = config.runtime_repository ?? '0xPlayerOne/code-foundry'
109
+ for (const stem of ['ci', 'test', 'security', 'codeql']) {
110
+ const legacy = join(target, `.github/workflows/${stem}.yml`)
111
+ if (existsSync(legacy) && isGeneratedEventCaller(readFileSync(legacy, 'utf8'), stem, runtimeRepository)) {
112
+ warn(`stale generated legacy caller ${stem}.yml still triggers canonical suites; run code-foundry sync to migrate to validation.yml.`)
113
+ }
114
+ }
115
+ console.log('Remote CI, Test, Security, CodeQL, and release runtimes are loaded by the tiered validation orchestrator.')
75
116
  if (options.github) {
76
117
  const github = doctorGithub(target)
118
+ /** @type {{ codeFoundryTokenPresent?: boolean, releasePleaseTokenPresent?: boolean, stagingDeployKeyPresent?: boolean }} */
119
+ const secrets = github.details.secrets ?? {}
77
120
  for (const message of github.warnings) warn(`GitHub: ${message}`)
78
121
  for (const message of github.errors) error(`GitHub: ${message}`)
79
122
  console.log(`GitHub doctor inspected ${github.details.repository}.`)
123
+ const codeFoundryToken = secrets.codeFoundryTokenPresent ? 'present' : 'absent'
124
+ const releasePleaseToken = secrets.releasePleaseTokenPresent ? 'present' : 'absent'
125
+ const stagingDeployKey = secrets.stagingDeployKeyPresent ? 'present' : 'absent'
126
+ console.log(`GitHub secrets: CODE_FOUNDRY_TOKEN=${codeFoundryToken}, RELEASE_PLEASE_TOKEN=${releasePleaseToken}, STAGING_DEPLOY_KEY=${stagingDeployKey}`)
127
+ if (!secrets.codeFoundryTokenPresent && !secrets.releasePleaseTokenPresent) {
128
+ warn('GitHub token routing fallback: PR creation will remain draft and requires manual ready-for-review to trigger validation.')
129
+ }
80
130
  }
81
131
  if (errors) throw new Error(`Repository doctor found ${errors} error(s).`)
82
132
  console.log('Repository doctor passed.')