code-foundry 1.3.2 → 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.
@@ -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,12 @@
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
+
3
10
  ## [1.3.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.3.1...v1.3.2) (2026-09-07)
4
11
 
5
12
 
@@ -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.2",
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,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 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
  /**
@@ -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
- let preserved = false
857
+ /** @type {Record<string, any>} */
858
+ const merged = { ...config }
812
859
  for (const [key, value] of Object.entries(consumer)) {
813
- if (!(key in config)) {
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(config.ignorePatterns) ? config.ignorePatterns : []
863
+ const base = Array.isArray(merged.ignorePatterns) ? merged.ignorePatterns : []
820
864
  const missing = extra.filter((pattern) => !base.includes(pattern))
821
- if (missing.length) config.ignorePatterns = [...base, ...missing]
822
- if (!preserved && !missing.length) return baseline
823
- 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]))
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',