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.
@@ -11,7 +11,7 @@ dependency_review: auto
11
11
  package_manager: bun
12
12
  toolchain: auto
13
13
  runtime_repository: 0xPlayerOne/code-foundry
14
- runtime_ref: v1.2.0
14
+ runtime_ref: v1.3.1
15
15
  release_type: node
16
16
  npm_publish: true
17
17
  license: agpl-3.0-or-later
@@ -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/eslint toolchain supports TS 7.
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
- if grep -Eq '^opencode_security:\s*(true|auto)\s*$' .github/code-foundry.yml 2>/dev/null; then enabled=true; fi
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
 
@@ -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.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": {
@@ -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
- if (file === '.github/workflows/opencode-security.yml')
436
- return ['true', 'auto'].includes(config.opencode_security ?? 'false')
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 custom = existing.includes(marker)
778
- ? existing.slice(existing.indexOf(marker) + marker.length).trim()
779
- : ''
780
- return custom ? `${baseline.trimEnd()}\n\n${marker}\n${custom}\n` : baseline
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 extra = []
840
+ /** @type {Record<string, any>} */
841
+ let consumer = {}
794
842
  try {
795
- const parsed = JSON.parse(existing)
796
- if (Array.isArray(parsed.ignorePatterns)) extra = parsed.ignorePatterns
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
- if (extra.length === 0) return baseline
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
- const base = Array.isArray(config.ignorePatterns) ? config.ignorePatterns : []
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 === 0) return baseline
810
- config.ignorePatterns = [...base, ...missing]
811
- return `${JSON.stringify(config, null, 2)}\n`
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',