code-foundry 1.38.1 → 1.39.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.39.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.39.0...v1.39.1) (2026-10-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * report inactive main rulesets in github doctor ([#682](https://github.com/0xPlayerOne/code-foundry/issues/682)) ([695a0b4](https://github.com/0xPlayerOne/code-foundry/commit/695a0b437910aa8e664a481c1648dbadcc1a2de8))
9
+
10
+ ## [1.39.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.38.1...v1.39.0) (2026-10-01)
11
+
12
+
13
+ ### Features
14
+
15
+ * **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))
16
+
17
+
18
+ ### Maintenance
19
+
20
+ * 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))
21
+
3
22
  ## [1.38.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.38.0...v1.38.1) (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
@@ -308,6 +311,12 @@ features and enforce only checks that actually run:
308
311
  Apply only checks for enabled workflows.
309
312
  ```
310
313
 
314
+ `code-foundry doctor --github` treats a matching `main` ruleset in `disabled`
315
+ or `evaluate` mode as an error because neither mode blocks merges. If GitHub
316
+ does not expose branch protection or rulesets for the repository, the doctor
317
+ warns that enforcement could not be confirmed instead of inferring an active
318
+ policy from unavailable data.
319
+
311
320
  Keep strict status checks, linear history, and conversation resolution enabled
312
321
  where required. For a repository with optional features disabled, do not require
313
322
  checks that will never run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.38.1",
3
+ "version": "1.39.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": {
@@ -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(
@@ -18,6 +18,24 @@ export function doctorGithub(root) {
18
18
  const details = { repository }
19
19
  const rulesets = hydrateRulesets(repository, ghJson(['api', `repos/${repository}/rulesets`]))
20
20
  const mainRulesets = rulesetsForBranch(rulesets, 'main')
21
+ details.mainRulesets = mainRulesets.map((ruleset) => ({
22
+ id: ruleset.id ?? null,
23
+ name: ruleset.name ?? String(ruleset.id ?? 'unnamed'),
24
+ enforcement: ruleset.enforcement ?? null,
25
+ }))
26
+ for (const ruleset of mainRulesets) {
27
+ const name = ruleset.name ?? ruleset.id ?? 'unnamed'
28
+ if (ruleset.enforcement === 'disabled') {
29
+ errors.push(`main ruleset ${name} is disabled; its branch rules are not enforced.`)
30
+ } else if (ruleset.enforcement === 'evaluate') {
31
+ errors.push(`main ruleset ${name} is evaluate-only; its branch rules are not enforced.`)
32
+ } else if (ruleset.enforcement !== 'active') {
33
+ const state = ruleset.enforcement ?? 'missing'
34
+ warnings.push(
35
+ `main ruleset ${name} enforcement state '${state}' could not be confirmed; expected active.`
36
+ )
37
+ }
38
+ }
21
39
  const protection = ghJson(['api', `repos/${repository}/branches/main/protection`])
22
40
  const required = mainRulesets.length
23
41
  ? requiredContextsFromRulesets(mainRulesets)