@homeflare/config 0.11.0 → 0.12.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/README.md +6 -1
- package/dist/hooks/gates.d.ts.map +1 -1
- package/dist/hooks/oxfmt-config.d.ts +25 -0
- package/dist/hooks/oxfmt-config.d.ts.map +1 -0
- package/dist/hooks/report.d.ts +10 -0
- package/dist/hooks/report.d.ts.map +1 -1
- package/dist/hooks.js +70 -11
- package/dist/hooks.js.map +6 -5
- package/dist/repo-shape/dependabot.d.ts +11 -17
- package/dist/repo-shape/dependabot.d.ts.map +1 -1
- package/dist/repo-shape/render.d.ts.map +1 -1
- package/dist/repo-shape/shape.d.ts +1 -1
- package/dist/repo-shape/shape.d.ts.map +1 -1
- package/dist/repo-shape/yaml.d.ts +20 -3
- package/dist/repo-shape/yaml.d.ts.map +1 -1
- package/dist/repo-shape.d.ts +2 -4
- package/dist/repo-shape.d.ts.map +1 -1
- package/dist/repo-shape.js +106 -202
- package/dist/repo-shape.js.map +7 -8
- package/dist/versions.js +8 -2
- package/dist/versions.js.map +4 -4
- package/docs/hooks.md +19 -0
- package/docs/repo-shape-dependabot.md +31 -43
- package/package.json +1 -1
- package/src/hooks/gates.ts +70 -12
- package/src/hooks/oxfmt-config.ts +67 -0
- package/src/hooks/report.ts +15 -0
- package/src/repo-shape/dependabot.ts +23 -38
- package/src/repo-shape/render.ts +0 -3
- package/src/repo-shape/shape.ts +0 -1
- package/src/repo-shape/yaml.ts +26 -4
- package/src/repo-shape.ts +1 -4
- package/dist/repo-shape/automerge.d.ts +0 -17
- package/dist/repo-shape/automerge.d.ts.map +0 -1
- package/src/repo-shape/automerge.ts +0 -144
|
@@ -1,39 +1,42 @@
|
|
|
1
1
|
# Dependabot, and how a kit release reaches a consumer
|
|
2
2
|
|
|
3
|
-
`repo-shape` renders `.github/dependabot.yml
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
else. The branch ruleset's required checks decide whether it merges.
|
|
3
|
+
`repo-shape` renders `.github/dependabot.yml`. It watches everything EXCEPT the packages
|
|
4
|
+
this kit publishes: an `ignore` entry names `@homeflare/*` in the bun block, so Dependabot
|
|
5
|
+
never proposes them.
|
|
7
6
|
|
|
8
|
-
**
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
**Retired 2026-09-23 (kit auto-bumper design, Tim): the `homeflare` group and the
|
|
8
|
+
rendered `dependabot-automerge.yml`.** They used to be how a kit release reached a
|
|
9
|
+
consumer — grouped, checked daily, merged on green. `taslabs-net/homeflare-bumper` does
|
|
10
|
+
that job now, dispatched from this repo's own `release.yml` (`notify-consumers`) with a
|
|
11
|
+
schedule backstop. The `ignore` below exists **so the two never compete**: without it, a
|
|
12
|
+
Dependabot bump and a bumper bump could open two pull requests for the same version at
|
|
13
|
+
once.
|
|
14
|
+
|
|
15
|
+
**Status (2026-09-22): the Actions half works; the bun half is blocked upstream regardless
|
|
16
|
+
of the ignore.** See [the blocker](#the-blocker-bunlock-lockfileversion-2).
|
|
11
17
|
|
|
12
18
|
## Why
|
|
13
19
|
|
|
14
20
|
Merging and releasing a kit change are automated. Bumping a consumer was not, and it is the
|
|
15
21
|
leg that silently stops: measured 2026-09-22, `homeflare-proxmox` and `homeflare-mini`
|
|
16
22
|
pinned `@homeflare/alchemy` 0.13.0 and `@homeflare/config` 0.5.1 while the kit had published
|
|
17
|
-
0.19.1 and 0.8.0. Tim's decision (2026-09-23):
|
|
18
|
-
|
|
23
|
+
0.19.1 and 0.8.0. Tim's decision (2026-09-23): `homeflare-bumper` carries a kit release into
|
|
24
|
+
every consumer; Dependabot's job is everything else.
|
|
19
25
|
|
|
20
26
|
## What each rendered choice rests on
|
|
21
27
|
|
|
22
28
|
Read on 2026-09-22 from GitHub's docs and from dependabot-core's source at v0.397.0.
|
|
23
29
|
|
|
24
|
-
| Choice
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
| `cooldown.
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| Group recognised by branch name | `dependabot/bun/homeflare-<10 hex>`: prefix, package manager, directory (root collapses), then group name and the first 10 hex of an MD5 digest | dependabot-core `common/lib/dependabot/pull_request_creator/branch_namer/dependency_group_strategy.rb` |
|
|
35
|
-
| `contents: write` + `pull-requests: write` on the job only | A Dependabot-started run gets a read-only `GITHUB_TOKEN` unless the `permissions` key raises it; these two are what GitHub's own example grants | [Troubleshooting Dependabot on GitHub Actions][troubleshoot] |
|
|
36
|
-
| `--squash` | The house policy (`declareRepoPolicy`) allows squash merges only | `@homeflare/alchemy` `repo-policy-form.ts` |
|
|
30
|
+
| Choice | Why | Source |
|
|
31
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
32
|
+
| `ignore: [{dependency-name: '@homeflare/*'}]` | `dependency-name`, "optionally using `*` to match zero or more characters" — stops Dependabot proposing what the bumper owns | [options reference: `ignore`][options] |
|
|
33
|
+
| `cooldown.default-days: 7` | Third-party updates are proposed once a release is a week old, by age rather than by calendar | [options reference: `cooldown`][options] |
|
|
34
|
+
| One bun block, weekly | Two blocks for one ecosystem and target branch must have "no overlap in directories defined" — moot now, kept for the directories split below | [options reference: `directories`][options] |
|
|
35
|
+
|
|
36
|
+
The bumper's own citations — `gh pr merge --auto`'s CLEAN/UNSTABLE trap, the App's token
|
|
37
|
+
scoping, and everything else that used to live in this repo's now-deleted
|
|
38
|
+
`dependabot-automerge.yml` — moved with it, to `taslabs-net/homeflare-bumper` (not
|
|
39
|
+
measured here whether its docs have landed yet; check that repo directly).
|
|
37
40
|
|
|
38
41
|
## Where Dependabot runs, and the billing lock
|
|
39
42
|
|
|
@@ -82,27 +85,14 @@ and rejects unsafe git tags. Downgrading the lockfile to suit Dependabot gives b
|
|
|
82
85
|
|
|
83
86
|
## What still needs a person
|
|
84
87
|
|
|
85
|
-
- **A kit release
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
new workflow run" ([GITHUB_TOKEN][token]), so a refreshed commit pushed with it would
|
|
91
|
-
never get its checks.
|
|
92
|
-
- **A repository whose base branch requires no status check.** The arming job fails on
|
|
93
|
-
purpose there (measured 2026-09-22: `homeflare-builds`, whose ruleset is not deployed yet,
|
|
94
|
-
and `homeflare-desktop`). `gh pr merge --auto` would otherwise merge the bump at once,
|
|
95
|
-
before `check` ran. Deploy the repository's ruleset; the next Dependabot rebase arms it.
|
|
96
|
-
- **A release whose plan changes.** A merged bump deploys nothing, but the next deploy of
|
|
97
|
-
that consumer applies whatever the new kit plans. Read the plan before deploying.
|
|
98
|
-
|
|
99
|
-
⚠️ **The merge itself starts no workflow on `main`.** Auto-merge armed with `GITHUB_TOKEN`
|
|
100
|
-
merges as that token, and its push triggers nothing. The rendered `ci.yml` has no push
|
|
101
|
-
trigger anyway, and a bump carries no changeset, so nothing is lost — but a repository that
|
|
102
|
-
adds a `push: main` workflow should know it will not run for these merges.
|
|
88
|
+
- **A kit release whose plan changes.** A merged bump — from either path — deploys
|
|
89
|
+
nothing, but the next deploy of that consumer applies whatever the new kit plans. Read
|
|
90
|
+
the plan before deploying.
|
|
91
|
+
- **Whatever the bumper itself hands off.** Its own auto-merge refusals, App setup, and
|
|
92
|
+
key rotation are documented where it lives, `taslabs-net/homeflare-bumper` — not here.
|
|
103
93
|
|
|
104
94
|
⚠️ **`open-pull-requests-limit: 5` is per block.** Five stale third-party pull requests
|
|
105
|
-
could, in principle,
|
|
95
|
+
could, in principle, crowd out a new one. dependabot-core runs grouped updates before
|
|
106
96
|
ungrouped ones in each job (`group_update_all_versions.rb`), but the limit is enforced by
|
|
107
97
|
the service, whose code is not public — so this is reasoned, not measured.
|
|
108
98
|
|
|
@@ -119,8 +109,6 @@ which only says "The updater encountered one or more errors".
|
|
|
119
109
|
|
|
120
110
|
[options]: https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference
|
|
121
111
|
[automating]: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions
|
|
122
|
-
[troubleshoot]: https://docs.github.com/en/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-on-actions
|
|
123
112
|
[concepts]: https://docs.github.com/en/code-security/concepts/supply-chain-security/dependabot-on-actions
|
|
124
113
|
[reference]: https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-on-actions
|
|
125
114
|
[selfhosted]: https://docs.github.com/en/code-security/dependabot/maintain-dependencies/managing-dependabot-on-self-hosted-runners
|
|
126
|
-
[token]: https://docs.github.com/en/actions/concepts/security/github_token
|
package/package.json
CHANGED
package/src/hooks/gates.ts
CHANGED
|
@@ -10,12 +10,25 @@
|
|
|
10
10
|
* runs the whole `check`, every test, the build and the smoke test on every pull request.
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
|
+
import { resolveOxfmtConfig } from './oxfmt-config.ts';
|
|
13
14
|
import { type Lane, planLanes } from './push-plan.ts';
|
|
14
15
|
import { changesEverything, parsePushRefs, pushScope } from './push-range.ts';
|
|
15
|
-
import { fail, note, ok, run, runLane, tool } from './report.ts';
|
|
16
|
+
import { fail, note, ok, run, runCaptured, runLane, tool } from './report.ts';
|
|
16
17
|
import { scanStagedSecrets } from './secrets.ts';
|
|
17
18
|
import { fingerprints, staged } from './staged.ts';
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* oxfmt's own summary line ("Finished in 2ms on 0 files using 10 threads.") is how the hook
|
|
22
|
+
* learns whether `--no-error-on-unmatched-pattern` turned a real run into a no-op — oxfmt has
|
|
23
|
+
* no flag that answers this more directly. `null` (unparsable — a future oxfmt wording change)
|
|
24
|
+
* is treated as "something ran": that is the safe direction, since it only ever suppresses the
|
|
25
|
+
* new "no formattable staged files" message, never a real failure.
|
|
26
|
+
*/
|
|
27
|
+
function matchedFileCount(stdout: string): number | null {
|
|
28
|
+
const match = /\bon (\d+) files? using/.exec(stdout);
|
|
29
|
+
return match?.[1] === undefined ? null : Number(match[1]);
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
/**
|
|
20
33
|
* Format and lint what is staged, restaging only what the formatter rewrote.
|
|
21
34
|
*
|
|
@@ -43,9 +56,41 @@ export async function preCommit(root: string): Promise<void> {
|
|
|
43
56
|
// gitleaks binary, so it runs even in a worktree nobody has installed yet.
|
|
44
57
|
await scanStagedSecrets();
|
|
45
58
|
if (!installed(root, 'pre-commit')) return;
|
|
46
|
-
|
|
59
|
+
|
|
47
60
|
const { formattable, code, partial } = await staged();
|
|
48
61
|
|
|
62
|
+
// ⚠️ BEFORE RESOLVING A CONFIG: if oxfmt would never run (nothing formattable staged, not
|
|
63
|
+
// even a partially-staged one to `--check`), a config problem — even an unresolved
|
|
64
|
+
// ambiguous one — must not block the commit. That is the exact shape of BUG 1: don't
|
|
65
|
+
// fail on "there is nothing to do here".
|
|
66
|
+
if (formattable.length === 0 && partial.length === 0) {
|
|
67
|
+
ok('pre-commit: nothing staged to format');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const config = await resolveOxfmtConfig(root);
|
|
72
|
+
if (config.kind === 'ambiguous') {
|
|
73
|
+
fail(
|
|
74
|
+
'pre-commit',
|
|
75
|
+
`${String(config.files.length)} oxfmt configs found (${config.files.join(', ')}) and oxfmt auto-discovers none of them`,
|
|
76
|
+
'keep exactly one .oxfmtrc.* file at the repo root (.oxfmtrc.json is auto-discovered)',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
// ⛔ BUG, MEASURED 2026-09-23: bare `oxfmt` only finds `.oxfmtrc.json`/`.oxfmtrc.jsonc` on
|
|
80
|
+
// its own — see oxfmt-config.ts. `--config` is added only when the repo's config needs it.
|
|
81
|
+
const oxfmt = [
|
|
82
|
+
...tool(root, 'oxfmt'),
|
|
83
|
+
...(config.kind === 'explicit' ? ['--config', config.path] : []),
|
|
84
|
+
// ⛔ BUG, MEASURED 2026-09-23: a staged file wholly excluded by oxfmt's OWN
|
|
85
|
+
// `ignorePatterns` (house `packages/distilled-*/src/**`, say) made oxfmt exit non-zero
|
|
86
|
+
// with "Expected at least one target file" — `staged()` classifies by extension only,
|
|
87
|
+
// blind to the repo's ignore rules, so `formattable`/`code` can be 100% ignored files.
|
|
88
|
+
// This flag is oxfmt's own documented answer (`--help`, oxfmt 0.68.0): still fail on a
|
|
89
|
+
// real formatting problem, but not on "everything here was excluded".
|
|
90
|
+
'--no-error-on-unmatched-pattern',
|
|
91
|
+
];
|
|
92
|
+
const oxlint = [...tool(root, 'oxlint'), '--no-error-on-unmatched-pattern'];
|
|
93
|
+
|
|
49
94
|
// ⛔ Checked, never rewritten — see staged.ts for why `git add` here would be theft.
|
|
50
95
|
if (partial.length > 0) {
|
|
51
96
|
note(`${partial.length} staged file(s) also have unstaged edits; checking without rewriting`);
|
|
@@ -64,22 +109,28 @@ export async function preCommit(root: string): Promise<void> {
|
|
|
64
109
|
}
|
|
65
110
|
|
|
66
111
|
const before = await fingerprints(formattable);
|
|
67
|
-
|
|
112
|
+
const { code: fmtExit, stdout: fmtOut } = await runCaptured([...oxfmt, ...formattable]);
|
|
113
|
+
if (fmtExit !== 0) {
|
|
68
114
|
fail('pre-commit', 'oxfmt could not format the staged files', 'bun run format');
|
|
69
115
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
116
|
+
const matched = matchedFileCount(fmtOut);
|
|
117
|
+
|
|
118
|
+
if (matched !== 0) {
|
|
119
|
+
const after = await fingerprints(formattable);
|
|
120
|
+
const rewritten = formattable.filter((file) => before.get(file) !== after.get(file));
|
|
72
121
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
122
|
+
if (rewritten.length > 0) {
|
|
123
|
+
note(`oxfmt rewrote and restaged ${rewritten.length} file(s): ${rewritten.join(', ')}`);
|
|
124
|
+
if ((await run(['git', 'add', '--', ...rewritten])) !== 0) {
|
|
125
|
+
fail('pre-commit', 'could not restage the formatted files', 'git add the listed files');
|
|
126
|
+
}
|
|
77
127
|
}
|
|
78
128
|
}
|
|
79
129
|
|
|
80
130
|
// ⚠️ `--deny-warnings` matches what CI runs. A hook that is laxer than CI is worse
|
|
81
|
-
// than no hook: it certifies a change CI will reject.
|
|
82
|
-
|
|
131
|
+
// than no hook: it certifies a change CI will reject. Same ignore-rules shape as oxfmt
|
|
132
|
+
// above: `code` can be entirely excluded by oxlint's own `ignorePatterns`.
|
|
133
|
+
if (code.length > 0 && (await run([...oxlint, '--deny-warnings', ...code])) !== 0) {
|
|
83
134
|
fail(
|
|
84
135
|
'pre-commit',
|
|
85
136
|
`oxlint found problems in ${code.length} staged file(s)`,
|
|
@@ -87,7 +138,14 @@ export async function preCommit(root: string): Promise<void> {
|
|
|
87
138
|
);
|
|
88
139
|
}
|
|
89
140
|
|
|
90
|
-
|
|
141
|
+
if (matched === 0) {
|
|
142
|
+
note(
|
|
143
|
+
`${String(formattable.length)} staged file(s) matched a formattable extension, but all are excluded by ignore rules`,
|
|
144
|
+
);
|
|
145
|
+
ok('pre-commit: no formattable staged files');
|
|
146
|
+
} else {
|
|
147
|
+
ok(`pre-commit: ${formattable.length} staged file(s) formatted and linted`);
|
|
148
|
+
}
|
|
91
149
|
}
|
|
92
150
|
|
|
93
151
|
/** The one-line reason a lane is in the run, printed before it starts. */
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `.oxfmtrc.*` file governs this repo — so the hook can name it explicitly.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ BUG, MEASURED 2026-09-23 (homeflare-desktop docs agent): the shared pre-commit ran
|
|
5
|
+
* bare `oxfmt`, and bare `oxfmt` auto-discovers only `.oxfmtrc.json` and `.oxfmtrc.jsonc`.
|
|
6
|
+
* Checked against the pinned oxfmt 0.68.0 directly: a repo with only `.oxfmtrc.mjs` and
|
|
7
|
+
* no `-c` prints "No config found, using defaults" and formats with oxfmt's built-in
|
|
8
|
+
* style, silently — even though `oxfmt --help` lists `.ts/.mts/.cts/.js/.mjs/.cjs` as
|
|
9
|
+
* valid `--config` targets. Desktop's fix was renaming to `.oxfmtrc.json`; this fixes the
|
|
10
|
+
* hook so a repo does not have to.
|
|
11
|
+
* ★ WHY A FILE SCAN, NOT A PACKAGE.JSON READ. The repo's own `format`/`check` script names
|
|
12
|
+
* the config it wants (or relies on auto-discovery finding `.oxfmtrc.json`), but its exact
|
|
13
|
+
* invocation shape varies per repo. Finding the config file on disk and handing it to
|
|
14
|
+
* oxfmt directly gets the same result without parsing an arbitrary shell command.
|
|
15
|
+
* ⛔ EVERY `.oxfmtrc.*` LIVES AT THE REPO ROOT TODAY (checked: every repo in the estate has
|
|
16
|
+
* exactly one, at root — no nested per-package overrides). Explicit `-c` disables oxfmt's
|
|
17
|
+
* own nested-config search (measured: a nested `.oxfmtrc.json` was ignored once `-c` named
|
|
18
|
+
* the root one), so this only scans `root` itself — it would misfire on a repo that added a
|
|
19
|
+
* package-level override, which none currently do.
|
|
20
|
+
*/
|
|
21
|
+
import { readdir } from 'node:fs/promises';
|
|
22
|
+
|
|
23
|
+
/** Auto-discovered by bare `oxfmt` — nothing for the hook to pass. */
|
|
24
|
+
const AUTO_DISCOVERED = ['.oxfmtrc.json', '.oxfmtrc.jsonc'];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Accepted by `oxfmt -c/--config` (per `oxfmt --help`, oxfmt 0.68.0) but never found on its
|
|
28
|
+
* own — checked empirically for each extension. Listed in the order `--help` documents them.
|
|
29
|
+
*/
|
|
30
|
+
const NEEDS_EXPLICIT_CONFIG = [
|
|
31
|
+
'.oxfmtrc.ts',
|
|
32
|
+
'.oxfmtrc.mts',
|
|
33
|
+
'.oxfmtrc.cts',
|
|
34
|
+
'.oxfmtrc.js',
|
|
35
|
+
'.oxfmtrc.mjs',
|
|
36
|
+
'.oxfmtrc.cjs',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export type OxfmtConfigResolution =
|
|
40
|
+
/** `.oxfmtrc.json` or `.oxfmtrc.jsonc` exists — bare oxfmt already finds it. */
|
|
41
|
+
| { readonly kind: 'auto' }
|
|
42
|
+
/** No `.oxfmtrc.*` at all — let oxfmt use its built-in defaults, as today. */
|
|
43
|
+
| { readonly kind: 'none' }
|
|
44
|
+
/** Exactly one config oxfmt would not find unaided — pass it with `-c`. */
|
|
45
|
+
| { readonly kind: 'explicit'; readonly path: string }
|
|
46
|
+
/**
|
|
47
|
+
* Two or more configs, none of them auto-discovered — bare oxfmt (and a repo's own
|
|
48
|
+
* bare `bun run format`) would silently fall back to defaults too, honouring neither.
|
|
49
|
+
* Guessing which one the repo meant would just move the silent-defaults bug here.
|
|
50
|
+
*/
|
|
51
|
+
| { readonly kind: 'ambiguous'; readonly files: readonly string[] };
|
|
52
|
+
|
|
53
|
+
/** Every `.oxfmtrc.*` file actually present at `root`, in a fixed, deterministic order. */
|
|
54
|
+
async function present(root: string, names: readonly string[]): Promise<readonly string[]> {
|
|
55
|
+
const entries = new Set(await readdir(root).catch(() => []));
|
|
56
|
+
return names.filter((name) => entries.has(name));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function resolveOxfmtConfig(root: string): Promise<OxfmtConfigResolution> {
|
|
60
|
+
if ((await present(root, AUTO_DISCOVERED)).length > 0) return { kind: 'auto' };
|
|
61
|
+
|
|
62
|
+
const explicit = await present(root, NEEDS_EXPLICIT_CONFIG);
|
|
63
|
+
const [only, ...rest] = explicit;
|
|
64
|
+
if (only === undefined) return { kind: 'none' };
|
|
65
|
+
if (rest.length === 0) return { kind: 'explicit', path: `${root}/${only}` };
|
|
66
|
+
return { kind: 'ambiguous', files: explicit };
|
|
67
|
+
}
|
package/src/hooks/report.ts
CHANGED
|
@@ -52,6 +52,21 @@ export async function capture(cmd: readonly string[]): Promise<string> {
|
|
|
52
52
|
return (await probe(cmd)).stdout;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Run a command, relaying its stdout live-enough (after it exits) while also handing the
|
|
57
|
+
* caller the text — gates.ts reads oxfmt's own "on N files" summary from it. `stderr` stays
|
|
58
|
+
* `inherit`: diagnostics (a parse error, "no files matched") must show immediately, and nothing
|
|
59
|
+
* here needs to inspect them.
|
|
60
|
+
*/
|
|
61
|
+
export async function runCaptured(
|
|
62
|
+
cmd: readonly string[],
|
|
63
|
+
): Promise<{ readonly code: number; readonly stdout: string }> {
|
|
64
|
+
const proc = Bun.spawn([...cmd], { stdout: 'pipe', stderr: 'inherit' });
|
|
65
|
+
const stdout = await new Response(proc.stdout).text();
|
|
66
|
+
process.stdout.write(stdout);
|
|
67
|
+
return { code: await proc.exited, stdout };
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
/** Capture a command's stdout AND its exit code — git plumbing that answers by status. */
|
|
56
71
|
export async function probe(cmd: readonly string[]): Promise<{ code: number; stdout: string }> {
|
|
57
72
|
const proc = Bun.spawn([...cmd], { stdout: 'pipe', stderr: 'ignore' });
|
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `.github/dependabot.yml`, rendered
|
|
2
|
+
* `.github/dependabot.yml`, rendered.
|
|
3
3
|
*
|
|
4
4
|
* ★ MEASURED BEFORE IT WAS RENDERED (2026-09-22): 1 of 14 repositories had a Dependabot
|
|
5
5
|
* config. Twelve took no dependency or Action updates at all, and nothing said so.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* ⛔ `@homeflare/*` IS IGNORED HERE, NOT GROUPED (retired 2026-09-23, kit auto-bumper
|
|
8
|
+
* design, Tim). A `homeflare` group and a rendered `dependabot-automerge.yml`
|
|
9
|
+
* used to carry kit releases into a consumer; both are gone. `homeflare-bumper` does
|
|
10
|
+
* that job now, over `workflow_dispatch` from this repo's own release, with a schedule
|
|
11
|
+
* backstop — a Dependabot bump running at the same time would open a SECOND, competing
|
|
12
|
+
* pull request for the same version bump. The `ignore` entry below is what stops that:
|
|
13
|
+
* Dependabot never proposes `@homeflare/*` at all, so there is only ever one bump PR.
|
|
12
14
|
*
|
|
13
15
|
* Every Dependabot claim below was read from GitHub's docs or dependabot-core's source on
|
|
14
16
|
* 2026-09-22 (dependabot-core v0.397.0); `docs/repo-shape-dependabot.md` has the citations.
|
|
15
17
|
*/
|
|
16
18
|
import type { RepoShape } from './shape.ts';
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
* The group's identifier. ⛔ IT IS ALSO HALF OF A BRANCH NAME the auto-merge workflow
|
|
20
|
-
* matches — dependabot-core names a group's branch `dependabot/bun/<group>-<10 hex>`
|
|
21
|
-
* (branch_namer/dependency_group_strategy.rb) — so it is exported, not retyped there.
|
|
22
|
-
* Dependabot requires an identifier that starts and ends with a letter.
|
|
23
|
-
*/
|
|
24
|
-
export const HOMEFLARE_GROUP = 'homeflare';
|
|
25
|
-
|
|
26
20
|
/** The first-party scope. Every package the kit publishes is under it. */
|
|
27
21
|
export const HOMEFLARE_PATTERN = '@homeflare/*';
|
|
28
22
|
|
|
29
23
|
/**
|
|
30
|
-
* ★ SEVEN DAYS
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* is also the supply-chain half — a compromised release is usually yanked within days.
|
|
24
|
+
* ★ SEVEN DAYS. A third-party release is proposed once it is a week old rather than on
|
|
25
|
+
* Dependabot's undocumented-but-real 3-day default — that is also the supply-chain
|
|
26
|
+
* half, since a compromised release is usually yanked within days.
|
|
34
27
|
*/
|
|
35
28
|
export const THIRD_PARTY_COOLDOWN_DAYS = 7;
|
|
36
29
|
|
|
@@ -65,41 +58,33 @@ version: 2
|
|
|
65
58
|
|
|
66
59
|
updates:
|
|
67
60
|
# ── The toolchain (bun.lock) ────────────────────────────────────────────────
|
|
68
|
-
# ⚠️ BLOCKED UPSTREAM, 2026-09-22
|
|
69
|
-
# Dependabot's updater bundles bun 1.3.14, which reads
|
|
70
|
-
# every estate repository with "Unsupported
|
|
61
|
+
# ⚠️ STILL BLOCKED UPSTREAM, 2026-09-22 — unrelated to the ignore below. bun 1.4 writes
|
|
62
|
+
# bun.lock \`lockfileVersion\` 2 and Dependabot's updater bundles bun 1.3.14, which reads
|
|
63
|
+
# up to 1, so this whole block fails in every estate repository with "Unsupported
|
|
64
|
+
# bun.lock 'lockfileVersion' 2" before it reads a single manifest. The fix is
|
|
71
65
|
# dependabot/dependabot-core pull request 16071. The github-actions block is unaffected.
|
|
72
|
-
# ⛔ ONE BUN BLOCK, SO ONE SCHEDULE. Dependabot refuses two blocks for one ecosystem and
|
|
73
|
-
# target branch whose directories overlap, so the \`homeflare\` group cannot be daily
|
|
74
|
-
# while the rest stays weekly. The block is daily; \`cooldown\` slows the rest.
|
|
75
66
|
- package-ecosystem: bun
|
|
76
67
|
directories:
|
|
77
68
|
${bunDirs.map((dir) => ` - ${dir}`).join('\n')}
|
|
78
69
|
schedule:
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
interval: weekly
|
|
71
|
+
day: monday
|
|
81
72
|
time: '09:00'
|
|
82
73
|
timezone: America/New_York
|
|
83
|
-
# ⛔ THE EXCLUDE IS NOT OPTIONAL. Dependabot applies a 3-day cooldown to every version
|
|
84
|
-
# update even when this key is absent, so without it a kit release would wait three
|
|
85
|
-
# days before its bump opened — and "daily" would quietly mean "three days late".
|
|
86
74
|
cooldown:
|
|
87
75
|
default-days: ${THIRD_PARTY_COOLDOWN_DAYS}
|
|
88
|
-
|
|
76
|
+
# ⛔ NEVER PROPOSED HERE. \`homeflare-bumper\` opens the one pull request that bumps
|
|
77
|
+
# \`@homeflare/*\` (kit auto-bumper design, Tim 2026-09-23) — a Dependabot
|
|
78
|
+
# update for the same package would race it and, on the weeks they disagree, leave
|
|
79
|
+
# two open pull requests fighting over the same \`package.json\` line.
|
|
80
|
+
ignore:
|
|
81
|
+
- dependency-name: '${HOMEFLARE_PATTERN}'
|
|
89
82
|
open-pull-requests-limit: 5
|
|
90
83
|
commit-message:
|
|
91
84
|
prefix: 'chore'
|
|
92
85
|
include: scope
|
|
93
86
|
labels: [dependencies]
|
|
94
87
|
groups:
|
|
95
|
-
# ★ FIRST, AND EVERY UPDATE TYPE. A kit release is one set of packages built to work
|
|
96
|
-
# together, so they move as one pull request; \`bun run check\` is what reads it, and
|
|
97
|
-
# .github/workflows/dependabot-automerge.yml merges it when that is green.
|
|
98
|
-
# ⚠️ A release that changes what @homeflare/config renders fails the drift test here
|
|
99
|
-
# by design; \`bun run repo-shape:refresh\` on the branch is the one-command fix.
|
|
100
|
-
${HOMEFLARE_GROUP}:
|
|
101
|
-
patterns: ['${HOMEFLARE_PATTERN}']
|
|
102
|
-
|
|
103
88
|
# oxfmt and oxlint move together and only affect style. Minor and patch bumps are
|
|
104
89
|
# noise unless they fail CI, which is what CI is for.
|
|
105
90
|
lint-and-format:
|
package/src/repo-shape/render.ts
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
* test lives in `packages/alchemy/tests/repo-shape-policy.test.ts`, which is the one
|
|
21
21
|
* place that imports both.
|
|
22
22
|
*/
|
|
23
|
-
import { renderAutomerge } from './automerge.ts';
|
|
24
23
|
import { renderCi } from './ci.ts';
|
|
25
24
|
import { renderActionlintConfig, renderChangesetConfig } from './companions.ts';
|
|
26
25
|
import { renderDependabot } from './dependabot.ts';
|
|
@@ -68,7 +67,6 @@ export function renderRepoShape(shape: RepoShape): RenderedRepo {
|
|
|
68
67
|
'.changeset/config.json': renderChangesetConfig(shape),
|
|
69
68
|
'.github/dependabot.yml': renderDependabot(shape),
|
|
70
69
|
'.github/workflows/ci.yml': renderCi(shape),
|
|
71
|
-
'.github/workflows/dependabot-automerge.yml': renderAutomerge(shape),
|
|
72
70
|
'.github/workflows/security.yml': renderSecurity(shape),
|
|
73
71
|
};
|
|
74
72
|
|
|
@@ -87,6 +85,5 @@ export const RENDERED_PATHS: readonly RenderedPath[] = [
|
|
|
87
85
|
'.github/actionlint.yaml',
|
|
88
86
|
'.github/dependabot.yml',
|
|
89
87
|
'.github/workflows/ci.yml',
|
|
90
|
-
'.github/workflows/dependabot-automerge.yml',
|
|
91
88
|
'.github/workflows/security.yml',
|
|
92
89
|
];
|
package/src/repo-shape/shape.ts
CHANGED
package/src/repo-shape/yaml.ts
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
* this house are the product. The rendered workflows are written as text with holes;
|
|
7
7
|
* only the step lists, whose shape varies per repository, go through here.
|
|
8
8
|
*
|
|
9
|
-
* ⚠️ Bun
|
|
10
|
-
* against Bun 1.4.0 on 2026-09-
|
|
11
|
-
*
|
|
9
|
+
* ⚠️ Bun.YAML.stringify EXISTS (`Object.keys(Bun.YAML)` is `["parse", "stringify"]`,
|
|
10
|
+
* measured against Bun 1.4.0 on 2026-09-23) but its output does not fit these rules:
|
|
11
|
+
* (a) a multi-line `run:` comes out as a double-quoted string with `\n` escapes, e.g.
|
|
12
|
+
* `run: "echo a\necho b\n"`, not a `|` block scalar; (b) `09:00` comes out UNQUOTED,
|
|
13
|
+
* e.g. `cron: 09:00`, the YAML 1.1 sexagesimal trap the `scalar()` comment below guards
|
|
14
|
+
* against; (c) a mapping key is followed by a trailing space, e.g. `"steps: \n - ..."`;
|
|
15
|
+
* and (d) a plain JS object has nowhere to attach a comment, so it cannot carry one. The
|
|
16
|
+
* tests parse what this writes with `Bun.YAML.parse` and compare structures, so a
|
|
17
|
+
* malformed emission fails rather than shipping.
|
|
12
18
|
*/
|
|
13
19
|
import type { JobStep } from './shape.ts';
|
|
14
20
|
|
|
@@ -53,13 +59,29 @@ function renderMapping(
|
|
|
53
59
|
return Object.entries(entries).map(([key, value]) => `${indent(depth)}${key}: ${scalar(value)}`);
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Trim trailing `\n` characters the way `command.replace(/\n+$/, '')` used to, but
|
|
64
|
+
* linear instead of backtracking — the same pattern as `normalizeBaseUrl` in
|
|
65
|
+
* `packages/distilled-netbox/src/credentials.ts`. Exported so a test can compare it
|
|
66
|
+
* against the old regex directly.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ Linear on purpose: a `/\n+$/` regex backtracks polynomially on a long run of
|
|
69
|
+
* "\n" that is not at the end (CodeQL js/polynomial-redos), and `command` here is
|
|
70
|
+
* repository-configured step text.
|
|
71
|
+
*/
|
|
72
|
+
export function trimTrailingNewlines(value: string): string {
|
|
73
|
+
let end = value.length;
|
|
74
|
+
while (end > 0 && value.charCodeAt(end - 1) === 10) end--;
|
|
75
|
+
return value.slice(0, end);
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
/**
|
|
57
79
|
* ★ BLOCK SCALAR FOR EVERY MULTI-LINE `run:`. A folded or quoted form would join the
|
|
58
80
|
* lines, and a shell script whose `if` and `then` end up on one line is a syntax error
|
|
59
81
|
* at job time rather than at lint time. `|` keeps them exactly as written.
|
|
60
82
|
*/
|
|
61
83
|
function renderRun(command: string, depth: number): string[] {
|
|
62
|
-
const lines = command
|
|
84
|
+
const lines = trimTrailingNewlines(command).split('\n');
|
|
63
85
|
if (lines.length === 1) return [`${indent(depth)}run: ${scalar(lines[0] ?? '')}`];
|
|
64
86
|
return [`${indent(depth)}run: |`, ...lines.map((line) => `${indent(depth + 1)}${line}`)];
|
|
65
87
|
}
|
package/src/repo-shape.ts
CHANGED
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
* That one file gives the repository:
|
|
18
18
|
*
|
|
19
19
|
* · its FILES — `bun run repo-shape:refresh` writes ci.yml, security.yml,
|
|
20
|
-
*
|
|
21
|
-
* the changeset config;
|
|
20
|
+
* actionlint.yaml, dependabot.yml and the changeset config;
|
|
22
21
|
* · its DRIFT GATE — a `bun:test` calling `driftInRepoShape` fails on a hand edit;
|
|
23
22
|
* · its SETTINGS — `renderRepoShape(shape).policy` is the options object
|
|
24
23
|
* `@homeflare/alchemy`'s `declareRepoPolicy` takes, so the ruleset
|
|
@@ -34,11 +33,9 @@
|
|
|
34
33
|
* including the ones with no Alchemy stack, so it belongs to the package they all
|
|
35
34
|
* already have — and it takes no dependency on Alchemy or Effect to get there.
|
|
36
35
|
*/
|
|
37
|
-
export { GROUP_BRANCH, GROUP_BRANCH_PREFIX, renderAutomerge } from './repo-shape/automerge.ts';
|
|
38
36
|
export { renderCi, ACTIONLINT_VERSION, BUN_VERSION } from './repo-shape/ci.ts';
|
|
39
37
|
export { renderActionlintConfig, renderChangesetConfig } from './repo-shape/companions.ts';
|
|
40
38
|
export {
|
|
41
|
-
HOMEFLARE_GROUP,
|
|
42
39
|
HOMEFLARE_PATTERN,
|
|
43
40
|
renderDependabot,
|
|
44
41
|
THIRD_PARTY_COOLDOWN_DAYS,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { RepoShape } from './shape.ts';
|
|
2
|
-
/**
|
|
3
|
-
* The branch prefix a job-level `if:` can test with `startsWith`. GitHub's expression
|
|
4
|
-
* language has no regex, so this is the cheap filter that keeps every other pull request
|
|
5
|
-
* from taking a runner slot; `GROUP_BRANCH` is the exact test inside the step.
|
|
6
|
-
*/
|
|
7
|
-
export declare const GROUP_BRANCH_PREFIX: string;
|
|
8
|
-
/**
|
|
9
|
-
* ⛔ EXACT, AND FAILS CLOSED. A solo update of `@homeflare/config` is
|
|
10
|
-
* `dependabot/bun/homeflare/config-0.9.0` and a package named `homeflare-x` would be
|
|
11
|
-
* `dependabot/bun/homeflare-x-1.2.3`; neither matches. If Dependabot ever changes its
|
|
12
|
-
* format, the symptom is a bump that waits for a person — never one merged by mistake.
|
|
13
|
-
*/
|
|
14
|
-
export declare const GROUP_BRANCH: string;
|
|
15
|
-
/** The whole `dependabot-automerge.yml` for a shape. */
|
|
16
|
-
export declare function renderAutomerge(shape: RepoShape): string;
|
|
17
|
-
//# sourceMappingURL=automerge.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"automerge.d.ts","sourceRoot":"","sources":["../../src/repo-shape/automerge.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAA6C,CAAC;AAEhF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,MAA+C,CAAC;AA0D3E,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAyCxD"}
|