code-foundry 1.3.2 → 1.4.1
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/dependabot.yml +1 -1
- package/.github/workflows/opencode-security_self-ci.yml +9 -1
- package/.github/workflows/validation.yml +3 -1
- package/CHANGELOG.md +14 -0
- package/docs/CONFIGURATION.md +14 -0
- package/docs/WORKFLOWS.md +7 -2
- package/package.json +1 -1
- package/src/commands/sync.mjs +78 -15
- package/src/lib/github-doctor.mjs +6 -0
- package/src/lib/validation-policy.mjs +6 -4
package/.github/dependabot.yml
CHANGED
|
@@ -30,7 +30,7 @@ updates:
|
|
|
30
30
|
patterns: ['*']
|
|
31
31
|
ignore:
|
|
32
32
|
# TypeScript 7 cannot resolve node: builtins in this toolchain
|
|
33
|
-
# (verified); revisit when the TS/
|
|
33
|
+
# (verified); revisit when the TS/oxlint toolchain supports TS 7.
|
|
34
34
|
- dependency-name: 'typescript'
|
|
35
35
|
versions: ['>=7.0.0']
|
|
36
36
|
# Held with TypeScript 5.9 above.
|
|
@@ -28,10 +28,18 @@ jobs:
|
|
|
28
28
|
id: detect
|
|
29
29
|
env:
|
|
30
30
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
31
|
+
OPENCODE_SECURITY_OVERRIDE: ${{ vars.OPENCODE_SECURITY }}
|
|
31
32
|
run: |
|
|
32
33
|
enabled=false
|
|
33
34
|
token=false
|
|
34
|
-
|
|
35
|
+
# The OPENCODE_SECURITY repository variable overrides the
|
|
36
|
+
# configuration: true opts a repository in, false opts it out, and
|
|
37
|
+
# an unset value falls back to the code-foundry configuration.
|
|
38
|
+
if [ "$OPENCODE_SECURITY_OVERRIDE" = "true" ]; then
|
|
39
|
+
enabled=true
|
|
40
|
+
elif [ "$OPENCODE_SECURITY_OVERRIDE" = "false" ]; then
|
|
41
|
+
enabled=false
|
|
42
|
+
elif grep -Eq '^opencode_security:\s*(true|auto)\s*$' .github/code-foundry.yml 2>/dev/null; then enabled=true; fi
|
|
35
43
|
if [ -n "$OPENCODE_API_KEY" ]; then token=true; fi
|
|
36
44
|
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
|
|
37
45
|
echo "token=$token" >> "$GITHUB_OUTPUT"
|
|
@@ -109,7 +109,9 @@ jobs:
|
|
|
109
109
|
|
|
110
110
|
codeql:
|
|
111
111
|
name: CodeQL
|
|
112
|
-
|
|
112
|
+
# Release-please pull requests also analyze with CodeQL so repository
|
|
113
|
+
# rulesets requiring code scanning results never deadlock the release.
|
|
114
|
+
if: vars.CI_BILLING_PAUSED != 'true' && (inputs.mode == 'audit' || inputs.mode == 'release')
|
|
113
115
|
uses: ./.github/workflows/codeql.yml
|
|
114
116
|
with:
|
|
115
117
|
runtime-repository: ${{ inputs.runtime-repository }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.4.0...v1.4.1) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **validation:** run CodeQL on the release tier ([e93c36c](https://github.com/0xPlayerOne/code-foundry/commit/e93c36c4be11611117a13aa88212f16b6129f464))
|
|
9
|
+
|
|
10
|
+
## [1.4.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.3.2...v1.4.0) (2026-09-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **sync:** oxfmt-stable config merges, gitignore preservation, and OPENCODE_SECURITY variable override ([455ac37](https://github.com/0xPlayerOne/code-foundry/commit/455ac37635d7e88a0bfe007a98ee12b56bc430dc))
|
|
16
|
+
|
|
3
17
|
## [1.3.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.3.1...v1.3.2) (2026-09-07)
|
|
4
18
|
|
|
5
19
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -56,6 +56,20 @@ repository manifests and source
|
|
|
56
56
|
Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
|
|
57
57
|
`release-pr`, `release`, and `dependabot`.
|
|
58
58
|
|
|
59
|
+
## OpenCode Security opt-in and opt-out
|
|
60
|
+
|
|
61
|
+
`opencode_security` controls the optional OpenCode scan, and the generated
|
|
62
|
+
caller ships in every repository. The `OPENCODE_SECURITY` repository variable
|
|
63
|
+
overrides the configuration at run time, so a scan can be toggled on any
|
|
64
|
+
repository without a configuration change or a new secret:
|
|
65
|
+
|
|
66
|
+
- `OPENCODE_SECURITY: true` opts the repository in.
|
|
67
|
+
- `OPENCODE_SECURITY: false` opts the repository out.
|
|
68
|
+
- unset falls back to the `opencode_security` configuration (default `false`).
|
|
69
|
+
|
|
70
|
+
The scan only runs when it is enabled and the `OPENCODE_API_KEY` secret is
|
|
71
|
+
present.
|
|
72
|
+
|
|
59
73
|
## Git workflow
|
|
60
74
|
|
|
61
75
|
`git_workflow` selects the branch topology:
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -16,7 +16,9 @@ workflow_dispatch:
|
|
|
16
16
|
|
|
17
17
|
In the `staging-release` topology, pull requests into `staging` run the fast
|
|
18
18
|
tier, ordinary pull requests into `main` run the full audit tier, and exact
|
|
19
|
-
Release Please pull requests into `main` run only release policy
|
|
19
|
+
Release Please pull requests into `main` run only release policy plus CodeQL
|
|
20
|
+
(the CodeQL analysis keeps repository rulesets that require code scanning
|
|
21
|
+
results satisfiable for the release commit). In the
|
|
20
22
|
`direct` topology (the default) every pull request targets `main` and runs the
|
|
21
23
|
full audit tier, because there is no integration branch for a fast pass.
|
|
22
24
|
Scheduled and manual runs select the audit tier in both topologies. Draft PR
|
|
@@ -67,7 +69,10 @@ throughout the bounded release flow.
|
|
|
67
69
|
|
|
68
70
|
Custom workflows are repository-owned and are not rewritten by sync. Add
|
|
69
71
|
`if: vars.CI_BILLING_PAUSED != 'true'` to each custom root job that should
|
|
70
|
-
honor the shared billing pause.
|
|
72
|
+
honor the shared billing pause. The optional OpenCode Security scan honors a
|
|
73
|
+
second toggle: the `OPENCODE_SECURITY` repository variable (`true`/`false`)
|
|
74
|
+
overrides the `opencode_security` configuration per run, so individual
|
|
75
|
+
repositories can opt in or out without a code change.
|
|
71
76
|
|
|
72
77
|
## Standard workflow responsibilities
|
|
73
78
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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/commands/sync.mjs
CHANGED
|
@@ -432,8 +432,11 @@ function shouldInclude(file, languages, features, config) {
|
|
|
432
432
|
if (file === '.oxfmtrc.json' || file === '.oxlintrc.json')
|
|
433
433
|
return includesValue(languages, 'typescript')
|
|
434
434
|
if (file === '.github/dependabot.yml') return includesValue(features, 'dependabot')
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
// The OpenCode Security caller is installed in every repository so the
|
|
436
|
+
// OPENCODE_SECURITY repository variable can opt a repository in (or out)
|
|
437
|
+
// without a configuration change. The detect job keeps the scan off unless
|
|
438
|
+
// the configuration or the variable enables it and the API key exists.
|
|
439
|
+
if (file === '.github/workflows/opencode-security.yml') return true
|
|
437
440
|
const workflow = file.match(/^\.github\/workflows\/([^/]+)\.yml$/)?.[1]
|
|
438
441
|
// The staging promotion caller only exists in the staging-release topology;
|
|
439
442
|
// direct repositories open feature branches into main and need no promotion.
|
|
@@ -774,10 +777,48 @@ export function isGeneratedEventCaller(content, stem, runtimeRepository) {
|
|
|
774
777
|
/** @param {string} baseline @param {string} existing */
|
|
775
778
|
function mergeGitignore(baseline, existing) {
|
|
776
779
|
const marker = '# Repository-specific rules'
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
780
|
+
const markerIndex = existing.indexOf(marker)
|
|
781
|
+
const head = markerIndex >= 0 ? existing.slice(0, markerIndex) : existing
|
|
782
|
+
const customSection = markerIndex >= 0 ? existing.slice(markerIndex + marker.length).trim() : ''
|
|
783
|
+
const baselineLines = new Set(
|
|
784
|
+
baseline
|
|
785
|
+
.split(/\r?\n/)
|
|
786
|
+
.map((line) => line.trim())
|
|
787
|
+
.filter(Boolean)
|
|
788
|
+
)
|
|
789
|
+
// Preserve consumer-owned entries the baseline does not define, including
|
|
790
|
+
// the comments that introduce them, so a sync never silently drops
|
|
791
|
+
// repository-specific ignore rules that live outside the managed section
|
|
792
|
+
// (for example Cloudflare or framework build output added by the repo).
|
|
793
|
+
/** @type {string[]} */
|
|
794
|
+
const preserved = []
|
|
795
|
+
/** @type {string[]} */
|
|
796
|
+
let pending = []
|
|
797
|
+
for (const line of head.split(/\r?\n/)) {
|
|
798
|
+
const trimmed = line.trim()
|
|
799
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
800
|
+
pending.push(line)
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
if (baselineLines.has(trimmed)) {
|
|
804
|
+
pending = []
|
|
805
|
+
continue
|
|
806
|
+
}
|
|
807
|
+
preserved.push(...pending, line)
|
|
808
|
+
pending = []
|
|
809
|
+
}
|
|
810
|
+
/** @type {string[]} */
|
|
811
|
+
const customLines = []
|
|
812
|
+
const seen = new Set()
|
|
813
|
+
for (const line of [...preserved, ...(customSection ? customSection.split(/\r?\n/) : [])]) {
|
|
814
|
+
const trimmed = line.trim()
|
|
815
|
+
if (!trimmed || seen.has(trimmed)) continue
|
|
816
|
+
seen.add(trimmed)
|
|
817
|
+
customLines.push(line)
|
|
818
|
+
}
|
|
819
|
+
return customLines.length
|
|
820
|
+
? `${baseline.trimEnd()}\n\n${marker}\n${customLines.join('\n')}\n`
|
|
821
|
+
: baseline
|
|
781
822
|
}
|
|
782
823
|
|
|
783
824
|
/**
|
|
@@ -788,6 +829,11 @@ function mergeGitignore(baseline, existing) {
|
|
|
788
829
|
* churn them. Keys the baseline does not define (e.g. a repository-owned
|
|
789
830
|
* `overrides` block) belong to the consumer and are preserved as well, so
|
|
790
831
|
* a sync never silently drops repository-owned configuration.
|
|
832
|
+
*
|
|
833
|
+
* When the merged semantics already match the consumer file, the exact
|
|
834
|
+
* existing bytes are returned untouched: rewriting canonical JSON here
|
|
835
|
+
* would fight the formatter (which collapses short collections that
|
|
836
|
+
* `JSON.stringify` expands) and churn the file on every sync.
|
|
791
837
|
* @param {string} baseline @param {string} existing @returns {string}
|
|
792
838
|
*/
|
|
793
839
|
function mergeIgnorePatternsConfig(baseline, existing) {
|
|
@@ -808,19 +854,36 @@ function mergeIgnorePatternsConfig(baseline, existing) {
|
|
|
808
854
|
} catch {
|
|
809
855
|
return baseline
|
|
810
856
|
}
|
|
811
|
-
|
|
857
|
+
/** @type {Record<string, any>} */
|
|
858
|
+
const merged = { ...config }
|
|
812
859
|
for (const [key, value] of Object.entries(consumer)) {
|
|
813
|
-
if (!(key in
|
|
814
|
-
config[key] = value
|
|
815
|
-
preserved = true
|
|
816
|
-
}
|
|
860
|
+
if (!(key in merged)) merged[key] = value
|
|
817
861
|
}
|
|
818
862
|
const extra = Array.isArray(consumer.ignorePatterns) ? consumer.ignorePatterns : []
|
|
819
|
-
const base = Array.isArray(
|
|
863
|
+
const base = Array.isArray(merged.ignorePatterns) ? merged.ignorePatterns : []
|
|
820
864
|
const missing = extra.filter((pattern) => !base.includes(pattern))
|
|
821
|
-
if (missing.length)
|
|
822
|
-
if (
|
|
823
|
-
return `${JSON.stringify(
|
|
865
|
+
if (missing.length) merged.ignorePatterns = [...base, ...missing]
|
|
866
|
+
if (jsonDeepEqual(merged, consumer)) return existing
|
|
867
|
+
return `${JSON.stringify(merged, null, 2)}\n`
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Order-insensitive deep equality for JSON-compatible values. Used to detect
|
|
872
|
+
* semantic equality between the merged Oxc configuration and the consumer's
|
|
873
|
+
* current file so formatting differences never trigger a rewrite.
|
|
874
|
+
* @param {any} a @param {any} b @returns {boolean}
|
|
875
|
+
*/
|
|
876
|
+
function jsonDeepEqual(a, b) {
|
|
877
|
+
if (a === b) return true
|
|
878
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
879
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
|
|
880
|
+
return a.every((item, index) => jsonDeepEqual(item, b[index]))
|
|
881
|
+
}
|
|
882
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
|
|
883
|
+
const aKeys = Object.keys(a)
|
|
884
|
+
const bKeys = Object.keys(b)
|
|
885
|
+
if (aKeys.length !== bKeys.length) return false
|
|
886
|
+
return aKeys.every((key) => key in b && jsonDeepEqual(a[key], b[key]))
|
|
824
887
|
}
|
|
825
888
|
|
|
826
889
|
/**
|
|
@@ -109,6 +109,12 @@ export function doctorGithub(root) {
|
|
|
109
109
|
warnings.push(
|
|
110
110
|
'opencode_security is enabled but OPENCODE_API_KEY is not configured; the optional scan will be skipped.'
|
|
111
111
|
)
|
|
112
|
+
if (['true', 'auto'].includes(config.opencode_security ?? 'false'))
|
|
113
|
+
details.credentials.opencodeSecurityConfigured = true
|
|
114
|
+
details.credentials.opencodeSecurityVariable =
|
|
115
|
+
ghJson(['variable', 'list', '--repo', repository, '--json', 'name'])?.some(
|
|
116
|
+
/** @param {{ name?: string }} variable */ (variable) => variable.name === 'OPENCODE_SECURITY'
|
|
117
|
+
) ?? false
|
|
112
118
|
|
|
113
119
|
const prs = ghJson([
|
|
114
120
|
'pr',
|
|
@@ -82,10 +82,12 @@ export function classifyValidationMode(input) {
|
|
|
82
82
|
const REQUIRED_JOBS_BY_MODE = {
|
|
83
83
|
fast: ['ci', 'test'],
|
|
84
84
|
audit: ['ci', 'test', 'security', 'codeql'],
|
|
85
|
-
// The release tier
|
|
86
|
-
// check runs as a conditional step inside the gate itself
|
|
87
|
-
//
|
|
88
|
-
release
|
|
85
|
+
// The release tier skips the CI/test/security suites (the generated release
|
|
86
|
+
// diff check runs as a conditional step inside the gate itself) but still
|
|
87
|
+
// runs CodeQL: repository rulesets that require code scanning results need
|
|
88
|
+
// a CodeQL analysis of the release pull request's commit, and Release
|
|
89
|
+
// Please pull requests must not deadlock against that requirement.
|
|
90
|
+
release: ['codeql'],
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/**
|