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.
@@ -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"
@@ -109,7 +109,9 @@ jobs:
109
109
 
110
110
  codeql:
111
111
  name: CodeQL
112
- if: vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit'
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
 
@@ -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. In the
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.2",
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": {
@@ -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',
@@ -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 requires no suite jobs: the generated release diff
86
- // check runs as a conditional step inside the gate itself, so an empty
87
- // requirement list is complete and any suite result is irrelevant.
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
  /**