code-foundry 1.34.0 → 1.35.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.
@@ -77,31 +77,63 @@ concurrency:
77
77
  cancel-in-progress: false
78
78
 
79
79
  jobs:
80
- affected:
81
- name: Check Turbo deployment impact
80
+ deploy:
81
+ name: Cloudflare Deploy / ${{ inputs.mode }}
82
+ # The Turbo impact check lives here as steps instead of a separate job:
83
+ # one checkout and one dependency install serve both the query and the
84
+ # deploy. Every deployment step is skipped when the check reports the
85
+ # package unaffected, and the check defaults to deploying whenever it
86
+ # cannot run.
82
87
  if: >-
83
- inputs['turbo-filter'] != '' &&
84
- (github.event_name == 'pull_request' || github.event_name == 'push') &&
85
88
  vars.CI_BILLING_PAUSED != 'true' &&
86
89
  (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
87
90
  (github.event_name != 'pull_request' || inputs.mode != 'preview' || !startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')) &&
88
91
  github.event_name != 'pull_request_target' &&
89
92
  (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
90
93
  runs-on: ${{ inputs.runner }}
91
- timeout-minutes: 10
92
- permissions:
93
- contents: read
94
+ timeout-minutes: 30
95
+ # The explicit deployment record below is the sole GitHub deployment record.
96
+ # Keep the environment for approvals, secrets, and branch protection without
97
+ # creating a second, competing PR timeline entry.
98
+ environment:
99
+ name: ${{ inputs.environment || (inputs.mode == 'production' && 'Production' || 'Preview') }}
100
+ deployment: false
101
+ url: ${{ steps.deploy.outputs.url }}
94
102
  outputs:
95
- should_deploy: ${{ steps.check.outputs.should_deploy || steps.resolve.outputs.should_deploy }}
103
+ url: ${{ steps.deploy.outputs.url }}
104
+ version-id: ${{ steps.deploy.outputs.version-id }}
105
+ github-deployment-id: ${{ steps.start.outputs.id }}
96
106
  steps:
97
- - name: Checkout full history
107
+ - name: Checkout
98
108
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
99
109
  with:
100
- ref: ${{ github.event.pull_request.head.sha || github.sha }}
101
- fetch-depth: 0
102
110
  persist-credentials: false
111
+ # The Turbo affected query compares the head tree against the base
112
+ # commit, so the checkout needs history; blob:none keeps it light.
113
+ fetch-depth: 0
114
+ filter: blob:none
115
+ - name: Validate deployment inputs
116
+ env:
117
+ MODE: ${{ inputs.mode }}
118
+ WRANGLER_VERSION: ${{ inputs.wrangler-version }}
119
+ run: |
120
+ set -euo pipefail
121
+ [[ "$MODE" == preview || "$MODE" == production ]]
122
+ [[ "$WRANGLER_VERSION" == local || "$WRANGLER_VERSION" == latest || "$WRANGLER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]
123
+ - name: Set up Bun
124
+ if: inputs.build-script != '' || inputs['turbo-filter'] != ''
125
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
126
+ with:
127
+ bun-version: ${{ inputs.bun-version }}
128
+ cache: true
129
+ cache-dependency-path: ${{ inputs.install-working-directory }}/bun.lock
130
+ - name: Install dependencies
131
+ if: inputs.build-script != '' || inputs['turbo-filter'] != ''
132
+ working-directory: ${{ inputs.install-working-directory }}
133
+ run: bun install --frozen-lockfile
103
134
  - name: Resolve Turbo package
104
135
  id: resolve
136
+ if: inputs['turbo-filter'] != ''
105
137
  working-directory: ${{ inputs.install-working-directory }}
106
138
  env:
107
139
  TURBO_FILTER: ${{ inputs.turbo-filter }}
@@ -150,20 +182,9 @@ jobs:
150
182
  }
151
183
  }
152
184
  NODE
153
- - name: Set up Bun
154
- if: steps.resolve.outputs.turbo_enabled == 'true'
155
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
156
- with:
157
- bun-version: ${{ inputs.bun-version }}
158
- cache: true
159
- cache-dependency-path: ${{ inputs.install-working-directory }}/bun.lock
160
- - name: Install dependencies for Turbo
161
- if: steps.resolve.outputs.turbo_enabled == 'true'
162
- working-directory: ${{ inputs.install-working-directory }}
163
- run: bun install --frozen-lockfile --ignore-scripts
164
185
  - name: Check affected build task
165
- if: steps.resolve.outputs.turbo_enabled == 'true'
166
186
  id: check
187
+ if: inputs['turbo-filter'] != '' && steps.resolve.outputs.turbo_enabled == 'true'
167
188
  working-directory: ${{ inputs.install-working-directory }}
168
189
  env:
169
190
  TURBO_PACKAGE: ${{ steps.resolve.outputs.turbo_package }}
@@ -193,49 +214,8 @@ jobs:
193
214
  console.log(`${packageName}: ${affected ? 'affected' : 'unaffected'}`)
194
215
  appendFileSync(process.env.GITHUB_OUTPUT, `should_deploy=${affected}\n`)
195
216
  NODE
196
-
197
- deploy:
198
- name: Cloudflare Deploy / ${{ inputs.mode }}
199
- needs: affected
200
- if: >-
201
- always() &&
202
- vars.CI_BILLING_PAUSED != 'true' &&
203
- (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
204
- (github.event_name != 'pull_request' || inputs.mode != 'preview' || !startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')) &&
205
- github.event_name != 'pull_request_target' &&
206
- (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
207
- (
208
- inputs['turbo-filter'] == '' ||
209
- needs.affected.result == 'skipped' ||
210
- needs.affected.outputs.should_deploy == 'true'
211
- )
212
- runs-on: ${{ inputs.runner }}
213
- timeout-minutes: 20
214
- # The explicit deployment record below is the sole GitHub deployment record.
215
- # Keep the environment for approvals, secrets, and branch protection without
216
- # creating a second, competing PR timeline entry.
217
- environment:
218
- name: ${{ inputs.environment || (inputs.mode == 'production' && 'Production' || 'Preview') }}
219
- deployment: false
220
- url: ${{ steps.deploy.outputs.url }}
221
- outputs:
222
- url: ${{ steps.deploy.outputs.url }}
223
- version-id: ${{ steps.deploy.outputs.version-id }}
224
- github-deployment-id: ${{ steps.start.outputs.id }}
225
- steps:
226
- - name: Checkout
227
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
228
- with:
229
- persist-credentials: false
230
- - name: Validate deployment inputs
231
- env:
232
- MODE: ${{ inputs.mode }}
233
- WRANGLER_VERSION: ${{ inputs.wrangler-version }}
234
- run: |
235
- set -euo pipefail
236
- [[ "$MODE" == preview || "$MODE" == production ]]
237
- [[ "$WRANGLER_VERSION" == local || "$WRANGLER_VERSION" == latest || "$WRANGLER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]
238
217
  - name: Record pending deployment
218
+ if: steps.check.outputs.should_deploy != 'false'
239
219
  id: start
240
220
  env:
241
221
  GH_TOKEN: ${{ github.token }}
@@ -263,7 +243,7 @@ jobs:
263
243
  working-directory: ${{ inputs.install-working-directory }}
264
244
  run: bun install --frozen-lockfile
265
245
  - name: Build Worker
266
- if: inputs.build-script != ''
246
+ if: inputs.build-script != '' && steps.check.outputs.should_deploy != 'false'
267
247
  working-directory: ${{ inputs.working-directory }}
268
248
  env:
269
249
  BUILD_SCRIPT: ${{ inputs.build-script }}
@@ -273,6 +253,7 @@ jobs:
273
253
  run: bun run "$BUILD_SCRIPT"
274
254
  - name: Deploy to Cloudflare
275
255
  id: deploy
256
+ if: steps.check.outputs.should_deploy != 'false'
276
257
  working-directory: ${{ inputs.working-directory }}
277
258
  env:
278
259
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -318,7 +299,7 @@ jobs:
318
299
  appendFileSync(process.env.GITHUB_OUTPUT, `url=${url.href}\nversion-id=${record.version_id}\n`)
319
300
  NODE
320
301
  - name: Complete deployment status
321
- if: always() && steps.start.outputs.id != ''
302
+ if: always() && steps.check.outputs.should_deploy != 'false' && steps.start.outputs.id != ''
322
303
  env:
323
304
  GH_TOKEN: ${{ github.token }}
324
305
  DEPLOYMENT_ID: ${{ steps.start.outputs.id }}
@@ -125,10 +125,47 @@ jobs:
125
125
  mkdir -p .github/actions
126
126
  mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
127
127
  cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
128
+ - name: List pull request files
129
+ id: changed
130
+ if: github.event_name == 'pull_request'
131
+ env:
132
+ GH_TOKEN: ${{ github.token }}
133
+ PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
134
+ run: |
135
+ set -euo pipefail
136
+ # Newline-separated changed paths for the runtime's change
137
+ # detection. A failed listing leaves the file absent and the
138
+ # runtime fails open toward analyzing every language and shard.
139
+ gh api "repos/$GITHUB_REPOSITORY/pulls/$PULL_REQUEST_NUMBER/files?per_page=100" \
140
+ --paginate --jq '.[].filename' > "$RUNNER_TEMP/changed-files.txt" || true
141
+ echo "file=$RUNNER_TEMP/changed-files.txt" >> "$GITHUB_OUTPUT"
142
+ - name: Detect code scanning merge gate
143
+ id: gate
144
+ if: github.event_name == 'pull_request'
145
+ env:
146
+ GH_TOKEN: ${{ github.token }}
147
+ run: |
148
+ set -euo pipefail
149
+ # The code-scanning merge gate waits for results in every tracked
150
+ # category, so change detection must never skip an upload while it
151
+ # is active. Any ruleset API failure also disables change
152
+ # detection: full analysis is the safe default.
153
+ gated=false
154
+ for id in $(gh api "repos/$GITHUB_REPOSITORY/rulesets" --paginate \
155
+ -q '.[] | select(.enforcement == "active") | .id' 2>/dev/null); do
156
+ if gh api "repos/$GITHUB_REPOSITORY/rulesets/$id" \
157
+ -q 'any(.rules[]?; .type == "code_scanning")' 2>/dev/null | grep -q true; then
158
+ gated=true
159
+ break
160
+ fi
161
+ done
162
+ echo "gated=$gated" >> "$GITHUB_OUTPUT"
128
163
  - id: languages
129
164
  name: Detect languages
130
165
  env:
131
166
  CODEQL_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
167
+ FOUNDRY_CODEQL_CHANGED_FILES_FILE: ${{ steps.changed.outputs.file }}
168
+ FOUNDRY_CODEQL_MERGE_GATE: ${{ steps.gate.outputs.gated }}
132
169
  REPO_FOUNDRY_CODE_SECURITY: ${{ steps.security.outputs.status }}
133
170
  REPO_FOUNDRY_PRIVATE: ${{ github.event.repository.private }}
134
171
  REPO_FOUNDRY_VISIBILITY: ${{ github.event.repository.visibility }}
@@ -166,14 +203,24 @@ jobs:
166
203
  changed: entry.changed === true,
167
204
  }));
168
205
  const rust = languages.find((entry) => entry.language === "rust");
169
- for (const shard of rust ? shards : []) {
206
+ // The runtime emits per-shard change flags when the event is a
207
+ // pull request with a usable changed-file list; fall back to the
208
+ // shard list of the caller with the language-level flag
209
+ // otherwise. No single quotes: this script runs through node -e.
210
+ const rustShards =
211
+ rust && Array.isArray(rust.shards) && rust.shards.length
212
+ ? rust.shards
213
+ : (rust
214
+ ? shards.map((shard) => ({ shard: String(shard), changed: rust.changed === true }))
215
+ : []);
216
+ for (const entry of rustShards) {
170
217
  matrix.push({
171
- display: String(shard) === "all" ? "Rust" : "Rust (" + shard + ")",
218
+ display: entry.shard === "all" ? "Rust" : "Rust (" + entry.shard + ")",
172
219
  language: "rust",
173
220
  category: "/language:rust",
174
221
  build_mode: "none",
175
- changed: rust.changed === true,
176
- shard: String(shard),
222
+ changed: entry.changed === true,
223
+ shard: String(entry.shard),
177
224
  });
178
225
  }
179
226
  if (!matrix.length) {
@@ -14,7 +14,13 @@ permissions:
14
14
  jobs:
15
15
  detect:
16
16
  name: OpenCode Security / Detect
17
- if: vars.CI_BILLING_PAUSED != 'true' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false))
17
+ # The OPENCODE_SECURITY repository or organization variable is the only
18
+ # enablement control, so gate the detector itself on it: a disabled
19
+ # repository must not allocate a runner just to report that it is off.
20
+ if: >-
21
+ vars.CI_BILLING_PAUSED != 'true' &&
22
+ vars.OPENCODE_SECURITY == 'true' &&
23
+ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false))
18
24
  runs-on: ubuntu-slim
19
25
  timeout-minutes: 10
20
26
  outputs:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.35.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.34.0...v1.35.0) (2026-09-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * fold the Turbo impact check into the cloudflare deploy job ([#655](https://github.com/0xPlayerOne/code-foundry/issues/655)) ([b72f5f2](https://github.com/0xPlayerOne/code-foundry/commit/b72f5f26f045103755bb2795f2fc3a84e7625b3c))
9
+ * scope CodeQL analysis to changed languages and Rust shards on pull requests ([#654](https://github.com/0xPlayerOne/code-foundry/issues/654)) ([d5ce743](https://github.com/0xPlayerOne/code-foundry/commit/d5ce74377601c2efb3be4831320cb62cdfbd999b))
10
+ * skip the security detector entirely while OpenCode Security is disabled ([#653](https://github.com/0xPlayerOne/code-foundry/issues/653)) ([9e4271e](https://github.com/0xPlayerOne/code-foundry/commit/9e4271e597d65b4f63c571baab5507eacaab1ce5))
11
+
12
+
13
+ ### Maintenance
14
+
15
+ * raise the packed artifact budget to 286 kB ([#657](https://github.com/0xPlayerOne/code-foundry/issues/657)) ([4512af7](https://github.com/0xPlayerOne/code-foundry/commit/4512af75fc18a36292f495c5e7f51791ba38f3d6))
16
+
3
17
  ## [1.34.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.33.3...v1.34.0) (2026-09-29)
4
18
 
5
19
 
@@ -280,6 +280,15 @@ every tracked category. Two practical consequences:
280
280
  categories in the shard list keeps the gate green but re-runs their analysis
281
281
  on every pull request and is not a substitute for cleanup.
282
282
 
283
+ On pull requests without a code-scanning merge gate, analyzers run only for
284
+ languages with changed files, and Rust shards run only when their scope (or a
285
+ workspace-wide `Cargo` manifest or toolchain file) changed; unchanged matrix
286
+ entries report `Not applicable` instead of analyzing. Repositories whose
287
+ rulesets enforce a code-scanning requirement are detected automatically and
288
+ analyze everything, because that merge gate waits for results in every tracked
289
+ category. Push, schedule, and merge-queue events always analyze the complete
290
+ set so the default branch keeps one full, comparable baseline.
291
+
283
292
  ## Cloudflare Workers
284
293
 
285
294
  Repositories that deploy to Cloudflare Workers can use the opt-in verified
package/docs/WORKFLOWS.md CHANGED
@@ -126,7 +126,11 @@ Custom workflows are repository-owned and are not rewritten by sync. Add
126
126
  honor the shared billing pause. The optional OpenCode Security scan honors a
127
127
  second toggle: the `OPENCODE_SECURITY` repository or organization variable
128
128
  (`true`/`false`) is its only enablement control, so individual repositories can
129
- opt in or out without a code change.
129
+ opt in or out without a code change. When the variable is not `true`, no
130
+ Security job runs at all — a disabled repository allocates no runner for it.
131
+ Repository variables override the organization value, so an organization-level
132
+ variable toggles every consumer in one place while a single repository can
133
+ still opt out (or in) independently.
130
134
 
131
135
  Dependabot is the one billable runner source the variable cannot gate: its
132
136
  update runs execute under the `dynamic` event, outside every job-level guard.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
6
6
  "bugs": {
@@ -1011,8 +1011,13 @@ function codeql() {
1011
1011
  available.push('javascript-typescript')
1012
1012
  if (hasLanguage('python')) available.push('python')
1013
1013
  if (hasLanguage('rust')) available.push('rust')
1014
- // Upload every configured language on every analysis run so GitHub can
1015
- // compare pull requests against the base branch's code-scanning config.
1014
+ // On pull requests, analyzers whose language has no changed files skip
1015
+ // their analysis steps, and Rust shards skip individually by scope. Every
1016
+ // other event analyzes the complete set so the default branch keeps one
1017
+ // full, comparable code-scanning baseline; a missing or unreadable
1018
+ // changed-file list also fails open toward analyzing everything.
1019
+ const changedPaths = pullRequestChangedPaths()
1020
+ /** @type {CodeqlLanguageEntry[]} */
1016
1021
  const languagesJson = available.map((language) => ({
1017
1022
  language,
1018
1023
  name:
@@ -1020,8 +1025,17 @@ function codeql() {
1020
1025
  ? 'TypeScript'
1021
1026
  : language[0].toUpperCase() + language.slice(1),
1022
1027
  'build-mode': 'none',
1023
- changed: true,
1028
+ changed: changedPaths === null || languageChanged(language, changedPaths),
1024
1029
  }))
1030
+ const rustEntry = languagesJson.find((item) => item.language === 'rust')
1031
+ if (rustEntry && changedPaths !== null) {
1032
+ const shards = configuredRustShards().map((shard) => ({
1033
+ shard,
1034
+ changed: shardChanged(shard, changedPaths),
1035
+ }))
1036
+ rustEntry.shards = shards
1037
+ rustEntry.changed = shards.some((entry) => entry.changed)
1038
+ }
1025
1039
  writeOutput('languages', languagesJson)
1026
1040
  for (const language of ['actions', 'javascript-typescript', 'python', 'rust']) {
1027
1041
  const entry = languagesJson.find((item) => item.language === language)
@@ -1032,6 +1046,105 @@ function codeql() {
1032
1046
  }
1033
1047
  }
1034
1048
 
1049
+ /**
1050
+ * Changed file paths for the current pull request, read from the list the
1051
+ * caller materializes with the pull request files API. Returns null when
1052
+ * change detection does not apply — any event other than pull_request, a
1053
+ * repository whose rulesets enforce the code-scanning merge gate (that gate
1054
+ * waits for results in every tracked category, so skipped uploads deadlock
1055
+ * merges), or a missing, unreadable, or truncated list — so callers fail
1056
+ * open toward analyzing everything.
1057
+ * @returns {Set<string> | null}
1058
+ */
1059
+ function pullRequestChangedPaths() {
1060
+ if ((process.env.FOUNDRY_EVENT_NAME ?? '') !== 'pull_request') return null
1061
+ if (process.env.FOUNDRY_CODEQL_MERGE_GATE === 'true') return null
1062
+ const file = process.env.FOUNDRY_CODEQL_CHANGED_FILES_FILE ?? ''
1063
+ if (!file) return null
1064
+ let contents = ''
1065
+ try {
1066
+ contents = readFileSync(file, 'utf8')
1067
+ } catch {
1068
+ return null
1069
+ }
1070
+ const paths = contents
1071
+ .split('\n')
1072
+ .map((line) => line.trim())
1073
+ .filter(Boolean)
1074
+ // The pull request files endpoint stops at 3000 entries; a list that large
1075
+ // cannot prove any language unchanged.
1076
+ if (paths.length >= 3000) return null
1077
+ return new Set(paths)
1078
+ }
1079
+
1080
+ /**
1081
+ * One detected CodeQL language for the analysis matrix. `shards` carries
1082
+ * per-scope change flags for Rust on pull requests; the matrix builder in
1083
+ * codeql.yml falls back to the language-level flag when it is absent.
1084
+ * @typedef {{
1085
+ * language: string,
1086
+ * name: string,
1087
+ * 'build-mode': string,
1088
+ * changed: boolean,
1089
+ * shards?: Array<{ shard: string, changed: boolean }>,
1090
+ * }} CodeqlLanguageEntry
1091
+ */
1092
+
1093
+ /**
1094
+ * Path predicates per CodeQL language. Manifests and lockfiles count as
1095
+ * language changes: dependency bumps alter what the analyzer sees.
1096
+ * @type {Record<string, RegExp>}
1097
+ */
1098
+ const CODEQL_LANGUAGE_PATHS = {
1099
+ actions: /^\.github\/(workflows|actions)\//,
1100
+ 'javascript-typescript':
1101
+ /\.(js|jsx|ts|tsx|mjs|cjs|mts|cts)$|(^|\/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lock|bun\.lockb|tsconfig[^/]*\.json|jsconfig[^/]*\.json)$/,
1102
+ python: /\.py$|(^|\/)(pyproject\.toml|poetry\.lock|setup\.py|setup\.cfg|requirements[^/]*\.txt)$/,
1103
+ rust: /\.rs$|(^|\/)(Cargo\.(toml|lock)|rust-toolchain(\.toml)?)$/,
1104
+ }
1105
+
1106
+ /** @param {string} language @param {Set<string>} changedPaths */
1107
+ function languageChanged(language, changedPaths) {
1108
+ const pattern = CODEQL_LANGUAGE_PATHS[language]
1109
+ if (!pattern) return true
1110
+ for (const path of changedPaths) if (pattern.test(path)) return true
1111
+ return false
1112
+ }
1113
+
1114
+ /** @returns {string[]} */
1115
+ function configuredRustShards() {
1116
+ const raw = configured(config.codeql_rust_shards, '["all"]')
1117
+ try {
1118
+ const parsed = JSON.parse(raw)
1119
+ return Array.isArray(parsed) && parsed.length ? parsed.map(String) : ['all']
1120
+ } catch {
1121
+ return ['all']
1122
+ }
1123
+ }
1124
+
1125
+ /**
1126
+ * A scoped shard runs when a changed path falls inside one of its scope
1127
+ * prefixes, or when a workspace-wide manifest changed: the analysis config
1128
+ * appends the Cargo manifests and toolchain to every shard, so those files
1129
+ * re-arm all shards at once. The `all` shard never skips.
1130
+ * @param {string} shard @param {Set<string>} changedPaths
1131
+ */
1132
+ function shardChanged(shard, changedPaths) {
1133
+ if (shard === 'all') return true
1134
+ const scopes = shard
1135
+ .split(',')
1136
+ .map((scope) => scope.trim())
1137
+ .filter(Boolean)
1138
+ for (const path of changedPaths) {
1139
+ if (/^(Cargo\.toml|Cargo\.lock|rust-toolchain(\.toml)?)$/.test(path)) return true
1140
+ for (const scope of scopes) {
1141
+ const prefix = scope.endsWith('/') ? scope : `${scope}/`
1142
+ if (path === scope || path.startsWith(prefix)) return true
1143
+ }
1144
+ }
1145
+ return false
1146
+ }
1147
+
1035
1148
  /** @param {string} command */
1036
1149
  function printProfile(command) {
1037
1150
  if (command === 'get') {