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.
- package/.github/workflows/cloudflare-deploy.yml +48 -67
- package/.github/workflows/codeql.yml +51 -4
- package/.github/workflows/opencode-security_self-ci.yml +7 -1
- package/CHANGELOG.md +14 -0
- package/docs/CONFIGURATION.md +9 -0
- package/docs/WORKFLOWS.md +5 -1
- package/package.json +1 -1
- package/src/runtime-core.mjs +116 -3
|
@@ -77,31 +77,63 @@ concurrency:
|
|
|
77
77
|
cancel-in-progress: false
|
|
78
78
|
|
|
79
79
|
jobs:
|
|
80
|
-
|
|
81
|
-
name:
|
|
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:
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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.
|
|
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": {
|
package/src/runtime-core.mjs
CHANGED
|
@@ -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
|
-
//
|
|
1015
|
-
//
|
|
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:
|
|
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') {
|