code-foundry 1.38.0 → 1.39.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.39.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.38.1...v1.39.0) (2026-10-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * **ci:** surface ungated scheduled workflows in the billing pause controls ([#681](https://github.com/0xPlayerOne/code-foundry/issues/681)) ([ef48f9a](https://github.com/0xPlayerOne/code-foundry/commit/ef48f9a25aa0b22f82b0ed8a512bc1a65d08d8e9))
9
+
10
+
11
+ ### Maintenance
12
+
13
+ * register the adea-ai ui and themes consumers in the fleet manifest ([#680](https://github.com/0xPlayerOne/code-foundry/issues/680)) ([f50e6c6](https://github.com/0xPlayerOne/code-foundry/commit/f50e6c6a4ef4b7f0396cdce21f53070fec818c50))
14
+
15
+ ## [1.38.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.38.0...v1.38.1) (2026-09-30)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * normalize blank runs when stripping legacy managed markers ([#678](https://github.com/0xPlayerOne/code-foundry/issues/678)) ([acd5afc](https://github.com/0xPlayerOne/code-foundry/commit/acd5afce12b3de3b3574259639e89067e1ab3cb0))
21
+
3
22
  ## [1.38.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.37.3...v1.38.0) (2026-09-30)
4
23
 
5
24
 
package/docs/WORKFLOWS.md CHANGED
@@ -123,7 +123,10 @@ throughout the bounded release flow.
123
123
 
124
124
  Custom workflows are repository-owned and are not rewritten by sync. Add
125
125
  `if: vars.CI_BILLING_PAUSED != 'true'` to each custom root job that should
126
- honor the shared billing pause. The optional OpenCode Security scan honors a
126
+ honor the shared billing pause. `npx code-foundry ci status` lists custom
127
+ scheduled workflows that lack the guard, and `pause` warns about them: a cron
128
+ workflow without the guard keeps allocating runners while CI is paused. The
129
+ optional OpenCode Security scan honors a
127
130
  second toggle: the `OPENCODE_SECURITY` repository or organization variable
128
131
  (`true`/`false`) is its only enablement control, so individual repositories can
129
132
  opt in or out without a code change. When the variable is not `true`, no
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.38.0",
3
+ "version": "1.39.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": {
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { spawnSync } from 'node:child_process'
4
- import { existsSync, readFileSync } from 'node:fs'
4
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
5
5
  import { readConfig } from '../lib/config.mjs'
6
6
 
7
7
  export const CI_BILLING_PAUSED_VARIABLE = 'CI_BILLING_PAUSED'
@@ -29,12 +29,14 @@ export function manageCiBilling(root, action) {
29
29
 
30
30
  if (action === 'status') {
31
31
  const dependabot = dependabotPauseState(root)
32
+ const ungatedSchedules = ungatedScheduledWorkflows(root)
32
33
  const result = {
33
34
  repository,
34
35
  paused,
35
36
  requiredCheck: CI_BILLING_REQUIRED_CHECK,
36
37
  backupPresent: Boolean(backup),
37
38
  dependabot,
39
+ ungatedSchedules,
38
40
  }
39
41
  console.log(JSON.stringify(result, null, 2))
40
42
  return result
@@ -92,6 +94,7 @@ export function manageCiBilling(root, action) {
92
94
  const cancelledRuns = cancelActiveRuns(repository)
93
95
  for (const change of changes) updateRuleset(repository, change.ruleset)
94
96
  const dependabot = dependabotPauseState(root)
97
+ const ungatedSchedules = ungatedScheduledWorkflows(root)
95
98
  const result = {
96
99
  repository,
97
100
  paused: true,
@@ -110,6 +113,19 @@ export function manageCiBilling(root, action) {
110
113
  'and run a sync so dependabot.yml renders open-pull-requests-limit: 0.',
111
114
  }
112
115
  : {}),
116
+ // Cron workflows are repository-owned, so sync cannot add the guard.
117
+ // List any that would keep firing on schedule while paused.
118
+ ungatedSchedules,
119
+ ...(ungatedSchedules.length
120
+ ? {
121
+ scheduleWarning:
122
+ `Scheduled workflows without a ${CI_BILLING_PAUSED_VARIABLE} job guard keep ` +
123
+ `running while paused: ${ungatedSchedules
124
+ .map((/** @type {{ file: string }} */ entry) => entry.file)
125
+ .join(', ')}. Add ` +
126
+ "`if: vars.CI_BILLING_PAUSED != 'true'` to each of their jobs.",
127
+ }
128
+ : {}),
113
129
  }
114
130
  console.log(JSON.stringify(result, null, 2))
115
131
  return result
@@ -175,6 +191,52 @@ export function dependabotPauseState(root) {
175
191
  return { configured, active, file }
176
192
  }
177
193
 
194
+ /**
195
+ * List custom cron workflows that the `CI_BILLING_PAUSED` variable does not
196
+ * gate. Sync keeps every managed workflow referencing the variable, so a
197
+ * scheduled workflow without that token is repository-owned, and its cron
198
+ * jobs keep allocating runners while CI is paused — the same gap Dependabot
199
+ * has, but one a one-line job guard can close.
200
+ *
201
+ * @param {string} root
202
+ * @returns {{ file: string, crons: string[] }[]}
203
+ */
204
+ export function ungatedScheduledWorkflows(root) {
205
+ const dir = `${root.replace(/\/$/, '')}/.github/workflows`
206
+ if (!existsSync(dir)) return []
207
+ const gated = []
208
+ for (const entry of readdirSync(dir)) {
209
+ if (!/\.ya?ml$/.test(entry)) continue
210
+ const source = readFileSync(`${dir}/${entry}`, 'utf8')
211
+ if (source.includes(CI_BILLING_PAUSED_VARIABLE)) continue
212
+ const triggers = workflowTriggerBlock(source)
213
+ if (!triggers || !/^ schedule:/m.test(triggers)) continue
214
+ const crons = [...triggers.matchAll(/^\s*-\s*cron:\s*(.+?)\s*$/gm)].flatMap(
215
+ (/** @type {RegExpMatchArray} */ match) => [
216
+ match[1].replace(/^['"]|['"]$/g, '').replace(/\s+#.*$/, ''),
217
+ ]
218
+ )
219
+ gated.push({ file: `.github/workflows/${entry}`, crons })
220
+ }
221
+ return gated
222
+ }
223
+
224
+ /**
225
+ * Return the block-form `on:` section of a workflow, or null when the file
226
+ * has none. Dependency-free on purpose: the runtime avoids a YAML parser,
227
+ * and the block ends at the next top-level key.
228
+ *
229
+ * @param {string} source
230
+ * @returns {string | null}
231
+ */
232
+ function workflowTriggerBlock(source) {
233
+ const start = source.search(/^on:\s*$/m)
234
+ if (start < 0) return null
235
+ const rest = source.slice(start + 3)
236
+ const end = rest.search(/^[A-Za-z_-]+:/m)
237
+ return end >= 0 ? rest.slice(0, end) : rest
238
+ }
239
+
178
240
  /** @param {any} ruleset */
179
241
  export function buildPausedRuleset(ruleset) {
180
242
  const requiredRuleIndexes = (ruleset.rules ?? []).flatMap(
@@ -1452,6 +1452,10 @@ function refreshManagedPolicyDocument(existing, baseline) {
1452
1452
  }
1453
1453
 
1454
1454
  merged = stripUnpairedManagedMarkers(merged)
1455
+ // Stripping residue and block wrapping can leave the blank runs the old
1456
+ // template carried; oxfmt collapses three or more newlines to one blank
1457
+ // line, so normalize to the same shape the format gate expects.
1458
+ merged = merged.replace(/\n{3,}/g, '\n\n')
1455
1459
 
1456
1460
  for (const match of blocks) {
1457
1461
  if (present.has(match[1])) continue