code-foundry 0.31.13 → 0.31.16

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.
@@ -4,6 +4,9 @@ profile: auto
4
4
  languages: typescript
5
5
  features: all
6
6
  codeql: auto
7
+ codeql_rust_shards: '["all"]'
8
+ codeql_rust_threads: 1
9
+ codeql_rust_max_parallel: 1
7
10
  dependency_review: auto
8
11
  package_manager: bun
9
12
  toolchain: auto
@@ -4,7 +4,7 @@ on:
4
4
  push:
5
5
  branches: [main, staging]
6
6
  pull_request:
7
- branches: [staging]
7
+ branches: [main, staging]
8
8
  workflow_dispatch:
9
9
 
10
10
  permissions:
@@ -18,6 +18,21 @@ on:
18
18
  required: false
19
19
  type: string
20
20
  default: ubuntu-latest
21
+ rust-shards:
22
+ description: JSON array of Rust scope sharding values. Use ["all"] for single-pass behavior.
23
+ required: false
24
+ type: string
25
+ default: '["all"]'
26
+ rust-threads:
27
+ description: Threads used for Rust extraction and analysis. Values above 1 opt into local parallelism.
28
+ required: false
29
+ type: string
30
+ default: '1'
31
+ rust-max-parallel:
32
+ description: Maximum Rust shard jobs allowed to run concurrently.
33
+ required: false
34
+ type: number
35
+ default: 1
21
36
 
22
37
  permissions:
23
38
  actions: read
@@ -280,6 +295,11 @@ jobs:
280
295
  if: >-
281
296
  needs.detect.outputs.enabled == 'true' &&
282
297
  needs.detect.outputs.rust_available == 'true'
298
+ strategy:
299
+ fail-fast: false
300
+ max-parallel: ${{ inputs.rust-max-parallel }}
301
+ matrix:
302
+ shard: ${{ fromJson(inputs.rust-shards) }}
283
303
  runs-on: ${{ inputs.runner }}
284
304
  timeout-minutes: 30
285
305
  steps:
@@ -312,16 +332,78 @@ jobs:
312
332
  mkdir -p .github/actions
313
333
  mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
314
334
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
315
- - name: Analyze
335
+ - name: Configure Rust scope
316
336
  if: >-
317
337
  needs.detect.outputs.enabled == 'true' &&
318
338
  needs.detect.outputs.rust_available == 'true' &&
319
339
  needs.detect.outputs.rust_changed == 'true'
320
- uses: ./.github/actions/codeql
340
+ id: scope
341
+ env:
342
+ RUST_SCOPE: ${{ matrix.shard }}
343
+ run: |
344
+ set -euo pipefail
345
+ scope="$RUST_SCOPE"
346
+ scope_id="$(node -e 'process.stdout.write(require("node:crypto").createHash("sha256").update(process.argv[1]).digest("hex").slice(0, 12))' "$scope")"
347
+ config_file="$RUNNER_TEMP/codeql-rust-$scope_id.yml"
348
+ cat > "$config_file" <<'EOF'
349
+ name: code-foundry-rust
350
+ EOF
351
+ if [ "$scope" != "all" ]; then
352
+ IFS=',' read -ra paths <<< "$scope"
353
+ if [ "${#paths[@]}" -eq 0 ]; then
354
+ echo "Empty Rust scope shard for CodeQL"
355
+ exit 1
356
+ fi
357
+ echo "paths:" >> "$config_file"
358
+ for path in "${paths[@]}"; do
359
+ path="${path#"${path%%[![:space:]]*}"}"
360
+ path="${path%"${path##*[![:space:]]}"}"
361
+ if [ -z "$path" ] ||
362
+ [[ "$path" = /* ]] ||
363
+ [[ "/$path/" = *"/../"* ]] ||
364
+ [[ ! "$path" =~ ^[A-Za-z0-9._/@+\ -]+$ ]]; then
365
+ echo "Invalid Rust CodeQL shard path: $path" >&2
366
+ exit 1
367
+ fi
368
+ if ! git ls-files -- "$path" | grep -Eq '(^|/)(Cargo\.toml|[^/]+\.rs)$'; then
369
+ echo "Rust CodeQL shard path contains no tracked Rust source: $path" >&2
370
+ exit 1
371
+ fi
372
+ printf " - '%s'\n" "$path" >> "$config_file"
373
+ done
374
+ fi
375
+ if [ -f Cargo.toml ] && grep -q '^paths:' "$config_file"; then
376
+ printf " - 'Cargo.toml'\n" >> "$config_file"
377
+ fi
378
+ if [ -f Cargo.lock ] && grep -q '^paths:' "$config_file"; then
379
+ printf " - 'Cargo.lock'\n" >> "$config_file"
380
+ fi
381
+ if [ -f rust-toolchain.toml ] && grep -q '^paths:' "$config_file"; then
382
+ printf " - 'rust-toolchain.toml'\n" >> "$config_file"
383
+ fi
384
+ echo "config_file=$config_file" >> "$GITHUB_OUTPUT"
385
+ echo "scope_id=$scope_id" >> "$GITHUB_OUTPUT"
386
+ - name: Initialize
387
+ if: >-
388
+ needs.detect.outputs.enabled == 'true' &&
389
+ needs.detect.outputs.rust_available == 'true' &&
390
+ needs.detect.outputs.rust_changed == 'true'
391
+ uses: github/codeql-action/init@v4
321
392
  with:
322
- language: rust
393
+ languages: rust
323
394
  build-mode: ${{ needs.detect.outputs.rust_build_mode }}
324
- category: /language:rust
395
+ config-file: ${{ steps.scope.outputs.config_file }}
396
+ threads: ${{ inputs.rust-threads }}
397
+ - name: Analyze
398
+ if: >-
399
+ needs.detect.outputs.enabled == 'true' &&
400
+ needs.detect.outputs.rust_available == 'true' &&
401
+ needs.detect.outputs.rust_changed == 'true'
402
+ uses: github/codeql-action/analyze@v4
403
+ with:
404
+ category: /language:rust/${{ steps.scope.outputs.scope_id }}
405
+ upload-database: false
406
+ wait-for-processing: false
325
407
  - name: Not applicable
326
408
  if: >-
327
409
  needs.detect.outputs.enabled != 'true' ||
@@ -4,7 +4,7 @@ on:
4
4
  push:
5
5
  branches: [main, staging]
6
6
  pull_request:
7
- branches: [staging]
7
+ branches: [main, staging]
8
8
  schedule:
9
9
  - cron: '31 6 * * 1'
10
10
  workflow_dispatch:
@@ -23,4 +23,7 @@ jobs:
23
23
  runtime-repository: 0xPlayerOne/code-foundry
24
24
  runtime-ref: ${{ github.sha }}
25
25
  runner: ubuntu-latest
26
+ rust-shards: '["all"]'
27
+ rust-threads: '1'
28
+ rust-max-parallel: 1
26
29
  secrets: inherit
@@ -2,7 +2,7 @@ name: Code Foundry
2
2
 
3
3
  on:
4
4
  pull_request:
5
- branches: [staging]
5
+ branches: [main, staging]
6
6
  workflow_dispatch:
7
7
 
8
8
  permissions:
@@ -25,6 +25,12 @@ jobs:
25
25
  group: code-foundry-release-pr-${{ github.repository }}-${{ github.ref }}
26
26
  cancel-in-progress: true
27
27
  steps:
28
+ - name: Checkout
29
+ uses: actions/checkout@v6
30
+ with:
31
+ fetch-depth: 0
32
+ filter: blob:none
33
+
28
34
  - name: Check
29
35
  id: check-pr
30
36
  run: |
@@ -44,16 +50,73 @@ jobs:
44
50
  fi
45
51
  COMPARE_URL="repos/${GITHUB_REPOSITORY}/compare/main...staging"
46
52
  TOTAL_COMMITS=$(gh api "$COMPARE_URL?per_page=1" --jq '.total_commits // 0')
53
+
54
+ MAIN_IS_ANCESTOR=false
55
+ if git merge-base --is-ancestor origin/main origin/staging; then
56
+ MAIN_IS_ANCESTOR=true
57
+ fi
58
+ DIRECT_FILES="$RUNNER_TEMP/release-direct-files.txt"
59
+ git diff --name-only origin/staging origin/main > "$DIRECT_FILES"
60
+ RELEASE_ONLY=false
61
+ if [ "$MAIN_IS_ANCESTOR" != true ] && [ -s "$DIRECT_FILES" ]; then
62
+ if node - "$DIRECT_FILES" <<'NODE'
63
+ const { existsSync, readFileSync } = require('node:fs')
64
+ const directFiles = readFileSync(process.argv[2], 'utf8').split(/\r?\n/).filter(Boolean)
65
+ const defaults = [
66
+ '.release-please-manifest.json', 'CHANGELOG.md', 'Cargo.lock', 'Cargo.toml',
67
+ 'bun.lock', 'bun.lockb', 'package-lock.json', 'package.json', 'pnpm-lock.yaml',
68
+ 'pyproject.toml', 'uv.lock', 'version.txt', 'yarn.lock',
69
+ ]
70
+ const allowed = new Set(defaults)
71
+ let config = {}
72
+ if (existsSync('release-please-config.json')) {
73
+ try { config = JSON.parse(readFileSync('release-please-config.json', 'utf8')) } catch {}
74
+ }
75
+ const addPath = (prefix, file) => {
76
+ const clean = file.replace(/^\.\//, '').replace(/\\/g, '/')
77
+ allowed.add(prefix ? `${prefix}/${clean}` : clean)
78
+ }
79
+ const addExtras = (entries, prefix = '') => {
80
+ if (!Array.isArray(entries)) return
81
+ for (const entry of entries) {
82
+ const file = typeof entry === 'string' ? entry : entry && typeof entry === 'object' ? entry.path : ''
83
+ if (typeof file === 'string' && file) addPath(prefix, file)
84
+ }
85
+ }
86
+ addExtras(config['extra-files'])
87
+ if (config.packages && typeof config.packages === 'object' && !Array.isArray(config.packages)) {
88
+ for (const [directory, value] of Object.entries(config.packages)) {
89
+ const prefix = directory === '.' ? '' : directory.replace(/\/$/, '')
90
+ for (const file of defaults) addPath(prefix, file)
91
+ if (value && typeof value === 'object' && !Array.isArray(value)) addExtras(value['extra-files'], prefix)
92
+ }
93
+ }
94
+ const unexpected = directFiles.filter((file) => !allowed.has(file))
95
+ if (unexpected.length) {
96
+ console.error(`Direct branch diff includes non-release paths: ${unexpected.join(', ')}`)
97
+ process.exit(1)
98
+ }
99
+ NODE
100
+ then
101
+ RELEASE_ONLY=true
102
+ fi
103
+ fi
47
104
  echo "existing=$EXISTING" >> "$GITHUB_OUTPUT"
48
105
  echo "commits=$TOTAL_COMMITS" >> "$GITHUB_OUTPUT"
49
106
  echo "tree_equal=$TREE_EQUAL" >> "$GITHUB_OUTPUT"
107
+ echo "release_only=$RELEASE_ONLY" >> "$GITHUB_OUTPUT"
50
108
 
51
109
  - name: No changes
52
- if: steps.check-pr.outputs.existing == '0' && (steps.check-pr.outputs.commits == '0' || steps.check-pr.outputs.tree_equal == 'true')
53
- run: echo 'staging is already aligned with main; no promotion PR is needed.'
110
+ if: steps.check-pr.outputs.existing == '0' && (steps.check-pr.outputs.commits == '0' || steps.check-pr.outputs.tree_equal == 'true' || steps.check-pr.outputs.release_only == 'true')
111
+ run: |
112
+ if [ "${{ steps.check-pr.outputs.release_only }}" = true ]; then
113
+ echo 'Only approved release metadata differs between rebased branch histories; reconciliation from main to staging is pending, so no promotion PR is needed.'
114
+ else
115
+ echo 'staging is already aligned with main; no promotion PR is needed.'
116
+ fi
54
117
 
55
118
  - name: Create
56
- if: steps.check-pr.outputs.existing == '0' && steps.check-pr.outputs.commits != '0' && steps.check-pr.outputs.tree_equal != 'true'
119
+ if: steps.check-pr.outputs.existing == '0' && steps.check-pr.outputs.commits != '0' && steps.check-pr.outputs.tree_equal != 'true' && steps.check-pr.outputs.release_only != 'true'
57
120
  run: |
58
121
  DATE=$(date +%Y-%m-%d)
59
122
  BODY_FILE="$RUNNER_TEMP/release-pr.md"
@@ -8,11 +8,8 @@ on:
8
8
  workflow_dispatch:
9
9
 
10
10
  permissions:
11
- actions: read
12
11
  contents: read
13
12
  pull-requests: write
14
- packages: read
15
- security-events: write
16
13
 
17
14
  jobs:
18
15
  release-pr:
@@ -25,44 +22,3 @@ jobs:
25
22
  with:
26
23
  runner: ubuntu-slim
27
24
  secrets: inherit
28
-
29
- ci:
30
- if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'release-please--branches--main')
31
- name: CI
32
- uses: ./.github/workflows/ci.yml
33
- with:
34
- runtime-repository: 0xPlayerOne/code-foundry
35
- runtime-ref: ${{ github.sha }}
36
- runner: ubuntu-latest
37
- secrets: inherit
38
-
39
- test:
40
- if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'release-please--branches--main')
41
- name: Test
42
- uses: ./.github/workflows/test.yml
43
- with:
44
- runtime-repository: 0xPlayerOne/code-foundry
45
- runtime-ref: ${{ github.sha }}
46
- runner: ubuntu-latest
47
- unit-runner: ubuntu-slim
48
- secrets: inherit
49
-
50
- security:
51
- if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'release-please--branches--main')
52
- name: Security
53
- uses: ./.github/workflows/security.yml
54
- with:
55
- runtime-repository: 0xPlayerOne/code-foundry
56
- runtime-ref: ${{ github.sha }}
57
- runner: ubuntu-slim
58
- secrets: inherit
59
-
60
- codeql:
61
- if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'release-please--branches--main')
62
- name: CodeQL
63
- uses: ./.github/workflows/codeql.yml
64
- with:
65
- runtime-repository: 0xPlayerOne/code-foundry
66
- runtime-ref: ${{ github.sha }}
67
- runner: ubuntu-latest
68
- secrets: inherit
@@ -4,7 +4,7 @@ on:
4
4
  push:
5
5
  branches: [main, staging]
6
6
  pull_request:
7
- branches: [staging]
7
+ branches: [main, staging]
8
8
  workflow_dispatch:
9
9
 
10
10
  permissions:
@@ -4,7 +4,7 @@ on:
4
4
  push:
5
5
  branches: [main, staging]
6
6
  pull_request:
7
- branches: [staging]
7
+ branches: [main, staging]
8
8
  workflow_dispatch:
9
9
 
10
10
  permissions:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.31.16](https://github.com/0xPlayerOne/code-foundry/compare/v0.31.15...v0.31.16) (2026-07-30)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **release:** avoid duplicate release PR checks ([42d7354](https://github.com/0xPlayerOne/code-foundry/commit/42d735404cd7b77d5a30a03a4fb8856096e931b0))
9
+ * **workflows:** validate main promotion PRs ([4f41f90](https://github.com/0xPlayerOne/code-foundry/commit/4f41f90a3e4a5eec8e80d46c21083c1380655341))
10
+
11
+ ## [0.31.15](https://github.com/0xPlayerOne/code-foundry/compare/v0.31.14...v0.31.15) (2026-07-30)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **release:** suppress release-only promotion loops ([fb35a6f](https://github.com/0xPlayerOne/code-foundry/commit/fb35a6f6e03a8ab4da65eeaeeeec579389b508b3))
17
+
18
+ ## [0.31.14](https://github.com/0xPlayerOne/code-foundry/compare/v0.31.13...v0.31.14) (2026-07-30)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **codeql:** make Rust parallelism safe and configurable ([545454f](https://github.com/0xPlayerOne/code-foundry/commit/545454fa42a8bb4e6970ff858af186f1cba09900))
24
+
3
25
  ## [0.31.13](https://github.com/0xPlayerOne/code-foundry/compare/v0.31.12...v0.31.13) (2026-07-30)
4
26
 
5
27
 
@@ -37,6 +37,9 @@ repository manifests and source
37
37
  | `toolchain` | `auto`, `native`, `mise` | Environment setup policy; defaults to `auto` |
38
38
  | `features` | `all` or a list | Standard workflow callers |
39
39
  | `codeql` | `auto`, `true`, `false` | CodeQL policy; public repositories default to enabled, non-public repositories default to disabled |
40
+ | `codeql_rust_shards` | JSON array of paths | Rust scan scopes; `["all"]` keeps the safe single full scan |
41
+ | `codeql_rust_threads` | integer, 1-64 | Threads per Rust CodeQL job; values above 1 opt into local parallelism |
42
+ | `codeql_rust_max_parallel` | integer, 1-8 | Maximum Rust shard jobs allowed to run concurrently |
40
43
  | `dependency_review` | `auto`, `true`, `false` | Dependency Review policy; public repositories default to enabled, non-public repositories default to disabled |
41
44
  | `prune_standard` | `true` or `false` | Remove disabled standard callers |
42
45
  | `runtime_repository` | `OWNER/REPO` | Reusable workflow source |
@@ -59,3 +62,17 @@ short and replaceable; custom workflows and project documentation are kept.
59
62
 
60
63
  The generated configuration includes all defaults so humans and agents can
61
64
  understand the repository without memorizing flags or environment variables.
65
+
66
+ Rust CodeQL defaults to one full scan with one worker. Large multi-crate
67
+ repositories can opt into bounded parallelism, for example:
68
+
69
+ ```yaml
70
+ codeql_rust_shards: '["crates/api","crates/worker"]'
71
+ codeql_rust_threads: 2
72
+ codeql_rust_max_parallel: 2
73
+ ```
74
+
75
+ Each scoped shard must contain tracked Rust source. Code Foundry rejects
76
+ absolute paths, parent traversal, duplicates, empty scopes, and more than eight
77
+ shards. Do not split a single crate by arbitrary non-Rust directories: use
78
+ `["all"]` when complete, non-overlapping source scopes are not available.
package/docs/WORKFLOWS.md CHANGED
@@ -8,7 +8,7 @@ The default standard callers use:
8
8
  push:
9
9
  branches: [main, staging]
10
10
  pull_request:
11
- branches: [staging]
11
+ branches: [main, staging]
12
12
  ```
13
13
 
14
14
  Draft PR automation may additionally listen to supported topic branches.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "0.31.13",
3
+ "version": "0.31.16",
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",
@@ -68,6 +68,7 @@ export function syncRepository(options) {
68
68
  let runtimeRef = configured(config.runtime_ref, sourceRuntimeRef)
69
69
  const toolchain = configured(config.toolchain, 'auto')
70
70
  const overlays = overlayPolicy(target, config)
71
+ const rustCodeql = validateRustCodeqlConfig(config)
71
72
  if (!['auto', 'native', 'mise'].includes(toolchain)) {
72
73
  throw new Error(`Unsupported toolchain: ${toolchain}; use auto, native, or mise.`)
73
74
  }
@@ -103,7 +104,7 @@ export function syncRepository(options) {
103
104
  content = Buffer.from(renderReleaseConfig(target, sourceFile))
104
105
  }
105
106
  if (file.endsWith('.yml') && file.startsWith('.github/workflows/')) {
106
- content = Buffer.from(renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef))
107
+ content = Buffer.from(renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef, rustCodeql))
107
108
  }
108
109
  if (file === '.gitignore' && existsSync(destination)) {
109
110
  content = Buffer.from(mergeGitignore(content.toString('utf8'), readFileSync(destination, 'utf8')))
@@ -198,8 +199,14 @@ function sourcePath(source, file) {
198
199
  return join(source, file)
199
200
  }
200
201
 
201
- /** @param {string} content @param {Record<string,string>} config @param {string} repository @param {string} ref */
202
- function renderWorkflow(content, config, repository, ref) {
202
+ /**
203
+ * @param {string} content
204
+ * @param {Record<string,string>} config
205
+ * @param {string} repository
206
+ * @param {string} ref
207
+ * @param {{ shards: string, threads: string, maxParallel: string }} rustCodeql
208
+ */
209
+ function renderWorkflow(content, config, repository, ref, rustCodeql) {
203
210
  const localPrefix = 'uses: ./.github/workflows/'
204
211
  const remotePrefix = `uses: ${repository}/.github/workflows/`
205
212
  let rendered = content.replaceAll(localPrefix, remotePrefix)
@@ -218,9 +225,60 @@ function renderWorkflow(content, config, repository, ref) {
218
225
  const workflow = content.match(/\.github\/workflows\/([^/]+)\.yml/)?.[1]
219
226
  const runner = workflow ? runners[workflow] : undefined
220
227
  if (runner) rendered = rendered.replace(/^(\s+runner:)\s+.*$/m, `$1 ${runner}`)
228
+ if (workflow === 'codeql') {
229
+ rendered = rendered.replace(/^(\s+rust-shards:)\s+.*$/m, `$1 '${rustCodeql.shards}'`)
230
+ rendered = rendered.replace(/^(\s+rust-threads:)\s+.*$/m, `$1 '${rustCodeql.threads}'`)
231
+ rendered = rendered.replace(/^(\s+rust-max-parallel:)\s+.*$/m, `$1 ${rustCodeql.maxParallel}`)
232
+ }
221
233
  return rendered
222
234
  }
223
235
 
236
+ /** @param {Record<string,string>} config */
237
+ function validateRustCodeqlConfig(config) {
238
+ const threads = configured(config.codeql_rust_threads, '1')
239
+ const maxParallel = configured(config.codeql_rust_max_parallel, '1')
240
+ if (!/^(?:[1-9]|[1-5][0-9]|6[0-4])$/.test(threads)) {
241
+ throw new Error('Unsupported codeql_rust_threads; use an integer from 1 to 64.')
242
+ }
243
+ if (!/^(?:[1-8])$/.test(maxParallel)) {
244
+ throw new Error('Unsupported codeql_rust_max_parallel; use an integer from 1 to 8.')
245
+ }
246
+
247
+ const rawShards = configured(config.codeql_rust_shards, '["all"]')
248
+ let shards
249
+ try {
250
+ shards = JSON.parse(rawShards)
251
+ } catch {
252
+ throw new Error('Invalid codeql_rust_shards; use a JSON array of relative Rust source paths.')
253
+ }
254
+ if (!Array.isArray(shards) || shards.length === 0 || shards.length > 8) {
255
+ throw new Error('Invalid codeql_rust_shards; configure between 1 and 8 shards.')
256
+ }
257
+ const seen = new Set()
258
+ for (const shard of shards) {
259
+ if (typeof shard !== 'string' || shard.length === 0 || shard.length > 512 || seen.has(shard)) {
260
+ throw new Error('Invalid codeql_rust_shards; shards must be unique non-empty strings.')
261
+ }
262
+ seen.add(shard)
263
+ if (shard === 'all') continue
264
+ for (const candidate of shard.split(',')) {
265
+ const path = candidate.trim()
266
+ if (
267
+ !path ||
268
+ path.startsWith('/') ||
269
+ path.split('/').includes('..') ||
270
+ !/^[A-Za-z0-9._/@+ -]+$/.test(path)
271
+ ) {
272
+ throw new Error(`Invalid Rust CodeQL shard path: ${path || '(empty)'}`)
273
+ }
274
+ }
275
+ }
276
+ if (seen.has('all') && shards.length !== 1) {
277
+ throw new Error('Invalid codeql_rust_shards; "all" cannot be combined with scoped shards.')
278
+ }
279
+ return { shards: JSON.stringify(shards), threads, maxParallel }
280
+ }
281
+
224
282
  /** @param {string} value */
225
283
  function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') }
226
284
 
@@ -269,6 +327,7 @@ function createDefaultConfig(root, source) {
269
327
  const runners = recommendRunners(root)
270
328
  return {
271
329
  version: '1', profile: detectProfile(root), languages, features: 'all', codeql: 'auto', dependency_review: 'auto', package_manager: packageManager,
330
+ codeql_rust_shards: '["all"]', codeql_rust_threads: '1', codeql_rust_max_parallel: '1',
272
331
  runtime_repository: '0xPlayerOne/code-foundry', runtime_ref: `v${readPackageVersion(source)}`,
273
332
  ...runners,
274
333
  toolchain: 'auto',
@@ -70,6 +70,26 @@ export function unexpectedReleasePaths(paths, allowed) {
70
70
  .filter((path) => !allowed.has(path))
71
71
  }
72
72
 
73
+ /**
74
+ * Detect release-only divergence after a rebased staging promotion. A normal
75
+ * main-ancestor relationship must always remain promotable, even when the new
76
+ * staging commit happens to touch only release metadata.
77
+ * @param {{ mainIsAncestor: boolean, directChangedPaths?: string[], allowed?: Set<string> }} input
78
+ */
79
+ export function classifyPromotion(input) {
80
+ const {
81
+ mainIsAncestor,
82
+ directChangedPaths = [],
83
+ allowed = approvedReleaseFiles(),
84
+ } = input
85
+ const paths = [...new Set(directChangedPaths.map((path) => path.trim()).filter(Boolean))]
86
+ const unexpected = unexpectedReleasePaths(paths, allowed)
87
+ return {
88
+ releaseOnly: !mainIsAncestor && paths.length > 0 && unexpected.length === 0,
89
+ unexpected,
90
+ }
91
+ }
92
+
73
93
  /** @param {{ releasePleaseToken?: string, githubToken?: string }} input */
74
94
  export function selectReleaseCredential(input) {
75
95
  if (input.releasePleaseToken) return { token: input.releasePleaseToken, source: 'release-please-token', autoMerge: true }