code-foundry 1.3.1 → 1.4.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/code-foundry.yml +1 -1
- package/.github/dependabot.yml +1 -1
- package/.github/workflows/opencode-security_self-ci.yml +9 -1
- package/CHANGELOG.md +14 -0
- package/docs/CONFIGURATION.md +14 -0
- package/docs/WORKFLOWS.md +4 -1
- package/package.json +1 -1
- package/src/commands/sync.mjs +93 -18
- package/src/lib/github-doctor.mjs +6 -0
package/.github/code-foundry.yml
CHANGED
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"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.3.2...v1.4.0) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **sync:** oxfmt-stable config merges, gitignore preservation, and OPENCODE_SECURITY variable override ([455ac37](https://github.com/0xPlayerOne/code-foundry/commit/455ac37635d7e88a0bfe007a98ee12b56bc430dc))
|
|
9
|
+
|
|
10
|
+
## [1.3.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.3.1...v1.3.2) (2026-09-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* preserve consumer-only Oxc config keys across syncs ([#476](https://github.com/0xPlayerOne/code-foundry/issues/476)) ([07aee3a](https://github.com/0xPlayerOne/code-foundry/commit/07aee3aed2aadeccf9740e1966941281e3f031f4))
|
|
16
|
+
|
|
3
17
|
## [1.3.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.3.0...v1.3.1) (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
|
@@ -67,7 +67,10 @@ throughout the bounded release flow.
|
|
|
67
67
|
|
|
68
68
|
Custom workflows are repository-owned and are not rewritten by sync. Add
|
|
69
69
|
`if: vars.CI_BILLING_PAUSED != 'true'` to each custom root job that should
|
|
70
|
-
honor the shared billing pause.
|
|
70
|
+
honor the shared billing pause. The optional OpenCode Security scan honors a
|
|
71
|
+
second toggle: the `OPENCODE_SECURITY` repository variable (`true`/`false`)
|
|
72
|
+
overrides the `opencode_security` configuration per run, so individual
|
|
73
|
+
repositories can opt in or out without a code change.
|
|
71
74
|
|
|
72
75
|
## Standard workflow responsibilities
|
|
73
76
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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/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,41 +777,113 @@ 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
|
/**
|
|
784
825
|
* Merge a baseline Oxc config (.oxfmtrc.json / .oxlintrc.json) with the
|
|
785
|
-
* consumer's current config. The baseline owns every key except
|
|
826
|
+
* consumer's current config. The baseline owns every key it defines, except
|
|
786
827
|
* `ignorePatterns`, where consumer-specific patterns (added by sync
|
|
787
828
|
* migrations or by the repository) are preserved so repeated syncs never
|
|
788
|
-
* churn them.
|
|
829
|
+
* churn them. Keys the baseline does not define (e.g. a repository-owned
|
|
830
|
+
* `overrides` block) belong to the consumer and are preserved as well, so
|
|
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.
|
|
789
837
|
* @param {string} baseline @param {string} existing @returns {string}
|
|
790
838
|
*/
|
|
791
839
|
function mergeIgnorePatternsConfig(baseline, existing) {
|
|
792
|
-
/** @type {string
|
|
793
|
-
let
|
|
840
|
+
/** @type {Record<string, any>} */
|
|
841
|
+
let consumer = {}
|
|
794
842
|
try {
|
|
795
|
-
|
|
796
|
-
if (Array.isArray(
|
|
843
|
+
consumer = JSON.parse(existing)
|
|
844
|
+
if (consumer === null || typeof consumer !== 'object' || Array.isArray(consumer))
|
|
845
|
+
return baseline
|
|
797
846
|
} catch {
|
|
798
847
|
return baseline
|
|
799
848
|
}
|
|
800
|
-
|
|
801
|
-
let config
|
|
849
|
+
/** @type {Record<string, any>} */
|
|
850
|
+
let config = {}
|
|
802
851
|
try {
|
|
803
852
|
config = JSON.parse(baseline)
|
|
853
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) return baseline
|
|
804
854
|
} catch {
|
|
805
855
|
return baseline
|
|
806
856
|
}
|
|
807
|
-
|
|
857
|
+
/** @type {Record<string, any>} */
|
|
858
|
+
const merged = { ...config }
|
|
859
|
+
for (const [key, value] of Object.entries(consumer)) {
|
|
860
|
+
if (!(key in merged)) merged[key] = value
|
|
861
|
+
}
|
|
862
|
+
const extra = Array.isArray(consumer.ignorePatterns) ? consumer.ignorePatterns : []
|
|
863
|
+
const base = Array.isArray(merged.ignorePatterns) ? merged.ignorePatterns : []
|
|
808
864
|
const missing = extra.filter((pattern) => !base.includes(pattern))
|
|
809
|
-
if (missing.length
|
|
810
|
-
|
|
811
|
-
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]))
|
|
812
887
|
}
|
|
813
888
|
|
|
814
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',
|