code-foundry 1.34.0 → 1.36.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 +94 -67
- package/.github/workflows/codeql.yml +51 -4
- package/.github/workflows/opencode-security_self-ci.yml +7 -1
- package/CHANGELOG.md +21 -0
- package/docs/CONFIGURATION.md +16 -1
- package/docs/WORKFLOWS.md +5 -1
- package/package.json +1 -1
- package/src/runtime-core.mjs +116 -3
|
@@ -8,6 +8,11 @@ on:
|
|
|
8
8
|
required: false
|
|
9
9
|
type: string
|
|
10
10
|
default: preview
|
|
11
|
+
deploy-tool:
|
|
12
|
+
description: 'Deployment CLI: wrangler (default, reads a wrangler config) or cf (Cloudflare cf CLI; deploys the cf Build Output produced by cf-wrangler build).'
|
|
13
|
+
required: false
|
|
14
|
+
type: string
|
|
15
|
+
default: wrangler
|
|
11
16
|
draft-protection:
|
|
12
17
|
description: 'Skip draft pull requests by default; set false to run deployments for drafts.'
|
|
13
18
|
required: false
|
|
@@ -77,31 +82,71 @@ concurrency:
|
|
|
77
82
|
cancel-in-progress: false
|
|
78
83
|
|
|
79
84
|
jobs:
|
|
80
|
-
|
|
81
|
-
name:
|
|
85
|
+
deploy:
|
|
86
|
+
name: Cloudflare Deploy / ${{ inputs.mode }}
|
|
87
|
+
# The Turbo impact check lives here as steps instead of a separate job:
|
|
88
|
+
# one checkout and one dependency install serve both the query and the
|
|
89
|
+
# deploy. Every deployment step is skipped when the check reports the
|
|
90
|
+
# package unaffected, and the check defaults to deploying whenever it
|
|
91
|
+
# cannot run.
|
|
82
92
|
if: >-
|
|
83
|
-
inputs['turbo-filter'] != '' &&
|
|
84
|
-
(github.event_name == 'pull_request' || github.event_name == 'push') &&
|
|
85
93
|
vars.CI_BILLING_PAUSED != 'true' &&
|
|
86
94
|
(inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
|
|
87
95
|
(github.event_name != 'pull_request' || inputs.mode != 'preview' || !startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')) &&
|
|
88
96
|
github.event_name != 'pull_request_target' &&
|
|
89
97
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
|
90
98
|
runs-on: ${{ inputs.runner }}
|
|
91
|
-
timeout-minutes:
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
timeout-minutes: 30
|
|
100
|
+
# The explicit deployment record below is the sole GitHub deployment record.
|
|
101
|
+
# Keep the environment for approvals, secrets, and branch protection without
|
|
102
|
+
# creating a second, competing PR timeline entry.
|
|
103
|
+
environment:
|
|
104
|
+
name: ${{ inputs.environment || (inputs.mode == 'production' && 'Production' || 'Preview') }}
|
|
105
|
+
deployment: false
|
|
106
|
+
url: ${{ steps.deploy.outputs.url }}
|
|
94
107
|
outputs:
|
|
95
|
-
|
|
108
|
+
url: ${{ steps.deploy.outputs.url }}
|
|
109
|
+
version-id: ${{ steps.deploy.outputs.version-id }}
|
|
110
|
+
github-deployment-id: ${{ steps.start.outputs.id }}
|
|
96
111
|
steps:
|
|
97
|
-
- name: Checkout
|
|
112
|
+
- name: Checkout
|
|
98
113
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
99
114
|
with:
|
|
100
|
-
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
101
|
-
fetch-depth: 0
|
|
102
115
|
persist-credentials: false
|
|
116
|
+
# The Turbo affected query compares the head tree against the base
|
|
117
|
+
# commit, so the checkout needs history; blob:none keeps it light.
|
|
118
|
+
fetch-depth: 0
|
|
119
|
+
filter: blob:none
|
|
120
|
+
- name: Validate deployment inputs
|
|
121
|
+
env:
|
|
122
|
+
MODE: ${{ inputs.mode }}
|
|
123
|
+
WRANGLER_VERSION: ${{ inputs.wrangler-version }}
|
|
124
|
+
DEPLOY_TOOL: ${{ inputs.deploy-tool }}
|
|
125
|
+
BUILD_SCRIPT: ${{ inputs.build-script }}
|
|
126
|
+
run: |
|
|
127
|
+
set -euo pipefail
|
|
128
|
+
[[ "$MODE" == preview || "$MODE" == production ]]
|
|
129
|
+
[[ "$DEPLOY_TOOL" == wrangler || "$DEPLOY_TOOL" == cf ]]
|
|
130
|
+
[[ "$WRANGLER_VERSION" == local || "$WRANGLER_VERSION" == latest || "$WRANGLER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]
|
|
131
|
+
# The cf path runs the project's cf-wrangler delegate to refresh the
|
|
132
|
+
# Build Output; that delegate comes from the app's own dependencies,
|
|
133
|
+
# which only the build step installs.
|
|
134
|
+
[[ "$DEPLOY_TOOL" != cf || -n "$BUILD_SCRIPT" ]]
|
|
135
|
+
[[ "$WRANGLER_VERSION" == local || "$WRANGLER_VERSION" == latest || "$WRANGLER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]
|
|
136
|
+
- name: Set up Bun
|
|
137
|
+
if: inputs.build-script != '' || inputs['turbo-filter'] != ''
|
|
138
|
+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
139
|
+
with:
|
|
140
|
+
bun-version: ${{ inputs.bun-version }}
|
|
141
|
+
cache: true
|
|
142
|
+
cache-dependency-path: ${{ inputs.install-working-directory }}/bun.lock
|
|
143
|
+
- name: Install dependencies
|
|
144
|
+
if: inputs.build-script != '' || inputs['turbo-filter'] != ''
|
|
145
|
+
working-directory: ${{ inputs.install-working-directory }}
|
|
146
|
+
run: bun install --frozen-lockfile
|
|
103
147
|
- name: Resolve Turbo package
|
|
104
148
|
id: resolve
|
|
149
|
+
if: inputs['turbo-filter'] != ''
|
|
105
150
|
working-directory: ${{ inputs.install-working-directory }}
|
|
106
151
|
env:
|
|
107
152
|
TURBO_FILTER: ${{ inputs.turbo-filter }}
|
|
@@ -150,20 +195,9 @@ jobs:
|
|
|
150
195
|
}
|
|
151
196
|
}
|
|
152
197
|
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
198
|
- name: Check affected build task
|
|
165
|
-
if: steps.resolve.outputs.turbo_enabled == 'true'
|
|
166
199
|
id: check
|
|
200
|
+
if: inputs['turbo-filter'] != '' && steps.resolve.outputs.turbo_enabled == 'true'
|
|
167
201
|
working-directory: ${{ inputs.install-working-directory }}
|
|
168
202
|
env:
|
|
169
203
|
TURBO_PACKAGE: ${{ steps.resolve.outputs.turbo_package }}
|
|
@@ -193,49 +227,8 @@ jobs:
|
|
|
193
227
|
console.log(`${packageName}: ${affected ? 'affected' : 'unaffected'}`)
|
|
194
228
|
appendFileSync(process.env.GITHUB_OUTPUT, `should_deploy=${affected}\n`)
|
|
195
229
|
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
230
|
- name: Record pending deployment
|
|
231
|
+
if: steps.check.outputs.should_deploy != 'false'
|
|
239
232
|
id: start
|
|
240
233
|
env:
|
|
241
234
|
GH_TOKEN: ${{ github.token }}
|
|
@@ -263,7 +256,7 @@ jobs:
|
|
|
263
256
|
working-directory: ${{ inputs.install-working-directory }}
|
|
264
257
|
run: bun install --frozen-lockfile
|
|
265
258
|
- name: Build Worker
|
|
266
|
-
if: inputs.build-script != ''
|
|
259
|
+
if: inputs.build-script != '' && steps.check.outputs.should_deploy != 'false'
|
|
267
260
|
working-directory: ${{ inputs.working-directory }}
|
|
268
261
|
env:
|
|
269
262
|
BUILD_SCRIPT: ${{ inputs.build-script }}
|
|
@@ -273,6 +266,7 @@ jobs:
|
|
|
273
266
|
run: bun run "$BUILD_SCRIPT"
|
|
274
267
|
- name: Deploy to Cloudflare
|
|
275
268
|
id: deploy
|
|
269
|
+
if: steps.check.outputs.should_deploy != 'false'
|
|
276
270
|
working-directory: ${{ inputs.working-directory }}
|
|
277
271
|
env:
|
|
278
272
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
@@ -281,8 +275,41 @@ jobs:
|
|
|
281
275
|
WRANGLER_VERSION: ${{ inputs.wrangler-version }}
|
|
282
276
|
MODE: ${{ inputs.mode }}
|
|
283
277
|
DEPLOYMENT_URL: ${{ inputs.deployment-url }}
|
|
278
|
+
DEPLOY_TOOL: ${{ inputs.deploy-tool }}
|
|
279
|
+
PREVIEW_NAME: ${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
|
|
284
280
|
run: |
|
|
285
281
|
set -euo pipefail
|
|
282
|
+
if [ "$DEPLOY_TOOL" = cf ]; then
|
|
283
|
+
# cf path: refresh the Build Output with the project's own
|
|
284
|
+
# cf-wrangler delegate, then deploy/preview it with the cf CLI.
|
|
285
|
+
# Both CLIs come from the app's devDependencies installed by the
|
|
286
|
+
# build step; the Node launcher avoids the Bun-runtime trap.
|
|
287
|
+
cfbin=(npx --no-install)
|
|
288
|
+
"${cfbin[@]}" cf-wrangler build
|
|
289
|
+
if [ "$MODE" = production ]; then
|
|
290
|
+
out="$("${cfbin[@]}" cf deploy --prebuilt)"
|
|
291
|
+
printf '%s\n' "$out"
|
|
292
|
+
version_id="$(printf '%s\n' "$out" | sed -n 's/.*Current Version ID: *//p' | head -1 | tr -d '[:space:]')"
|
|
293
|
+
value="${DEPLOYMENT_URL:-$(printf '%s\n' "$out" | grep -o 'https://[^[:space:]]*' | head -1)}"
|
|
294
|
+
else
|
|
295
|
+
"${cfbin[@]}" cf previews deploy "$PREVIEW_NAME" > "$RUNNER_TEMP/cf-preview.json"
|
|
296
|
+
value="$(jq -r '.preview_urls[0] // empty' "$RUNNER_TEMP/cf-preview.json")"
|
|
297
|
+
version_id="$(jq -r '.deployment_id // empty' "$RUNNER_TEMP/cf-preview.json")"
|
|
298
|
+
fi
|
|
299
|
+
if [[ ! "$version_id" =~ ^[0-9a-f-]{36}$ ]]; then
|
|
300
|
+
echo 'cf deploy did not report a Worker version ID' >&2
|
|
301
|
+
exit 1
|
|
302
|
+
fi
|
|
303
|
+
if [[ -z "$value" || "$value" != https://* || "$value" == *'@'* || "$value" == *'#'* || "$value" == *' '* ]]; then
|
|
304
|
+
echo 'cf deploy did not report a valid deployment URL' >&2
|
|
305
|
+
exit 1
|
|
306
|
+
fi
|
|
307
|
+
{
|
|
308
|
+
echo "url=$value"
|
|
309
|
+
echo "version-id=$version_id"
|
|
310
|
+
} >> "$GITHUB_OUTPUT"
|
|
311
|
+
exit 0
|
|
312
|
+
fi
|
|
286
313
|
if [ "$WRANGLER_VERSION" = local ]; then
|
|
287
314
|
if [ -n "$BUILD_SCRIPT" ]; then
|
|
288
315
|
wrangler=(npx --no-install wrangler)
|
|
@@ -318,7 +345,7 @@ jobs:
|
|
|
318
345
|
appendFileSync(process.env.GITHUB_OUTPUT, `url=${url.href}\nversion-id=${record.version_id}\n`)
|
|
319
346
|
NODE
|
|
320
347
|
- name: Complete deployment status
|
|
321
|
-
if: always() && steps.start.outputs.id != ''
|
|
348
|
+
if: always() && steps.check.outputs.should_deploy != 'false' && steps.start.outputs.id != ''
|
|
322
349
|
env:
|
|
323
350
|
GH_TOKEN: ${{ github.token }}
|
|
324
351
|
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,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.36.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.35.0...v1.36.0) (2026-09-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **cloudflare:** add a cf deploy mode to cloudflare-deploy.yml ([#658](https://github.com/0xPlayerOne/code-foundry/issues/658)) ([31ef8ca](https://github.com/0xPlayerOne/code-foundry/commit/31ef8ca607c189d7ca0bbe4f42de85ee27edabc9))
|
|
9
|
+
|
|
10
|
+
## [1.35.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.34.0...v1.35.0) (2026-09-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* 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))
|
|
16
|
+
* 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))
|
|
17
|
+
* 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))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Maintenance
|
|
21
|
+
|
|
22
|
+
* 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))
|
|
23
|
+
|
|
3
24
|
## [1.34.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.33.3...v1.34.0) (2026-09-29)
|
|
4
25
|
|
|
5
26
|
|
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
|
|
@@ -292,7 +301,13 @@ repository and configure environment reviewers separately.
|
|
|
292
301
|
The legacy `cloudflare-deploy.yml` workflow remains available for direct
|
|
293
302
|
(unverified) deployments. It runs `wrangler versions upload` for previews and
|
|
294
303
|
`wrangler deploy` for production, records a GitHub deployment plus status, and
|
|
295
|
-
respects `CI_BILLING_PAUSED`.
|
|
304
|
+
respects `CI_BILLING_PAUSED`. With `deploy-tool: cf` it instead deploys through
|
|
305
|
+
the Cloudflare cf CLI: production runs `cf-wrangler build` plus
|
|
306
|
+
`cf deploy --prebuilt` (consuming the project's cf Build Output), previews run
|
|
307
|
+
`cf previews deploy` named after the pull request or branch, and the cf/wrangler
|
|
308
|
+
CLIs must be devDependencies of the deployed package (the build step installs
|
|
309
|
+
them), so `build-script` is required. Deployment URLs and version IDs are
|
|
310
|
+
extracted from the same job outputs either way. Preview deployment records use the pull request
|
|
296
311
|
head SHA when called from a PR, which lets GitHub show the completed preview in
|
|
297
312
|
the PR's Deployments section; direct pushes use the workflow SHA. Its
|
|
298
313
|
legacy-compatible Wrangler default is `latest`; callers should prefer `local` or
|
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.36.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') {
|