@homeflare/config 0.8.0 → 0.10.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 +10 -9
- package/dist/repo-shape/automerge.d.ts +17 -0
- package/dist/repo-shape/automerge.d.ts.map +1 -0
- package/dist/repo-shape/ci.d.ts.map +1 -1
- package/dist/repo-shape/companions.d.ts +2 -6
- package/dist/repo-shape/companions.d.ts.map +1 -1
- package/dist/repo-shape/dependabot.d.ts +35 -0
- package/dist/repo-shape/dependabot.d.ts.map +1 -0
- package/dist/repo-shape/guards.d.ts +25 -0
- package/dist/repo-shape/guards.d.ts.map +1 -0
- package/dist/repo-shape/render.d.ts.map +1 -1
- package/dist/repo-shape/shape.d.ts +40 -2
- package/dist/repo-shape/shape.d.ts.map +1 -1
- package/dist/repo-shape.d.ts +5 -2
- package/dist/repo-shape.d.ts.map +1 -1
- package/dist/repo-shape.js +269 -105
- package/dist/repo-shape.js.map +10 -7
- package/dist/versions.d.ts +8 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +334 -0
- package/dist/versions.js.map +14 -0
- package/docs/repo-shape-dependabot.md +126 -0
- package/docs/repo-shape-inputs.md +83 -0
- package/docs/repo-shape.md +5 -23
- package/docs/versions.md +50 -0
- package/package.json +5 -1
- package/src/repo-shape/automerge.ts +144 -0
- package/src/repo-shape/ci.ts +35 -16
- package/src/repo-shape/companions.ts +2 -77
- package/src/repo-shape/dependabot.ts +136 -0
- package/src/repo-shape/guards.ts +68 -0
- package/src/repo-shape/render.ts +5 -1
- package/src/repo-shape/shape.ts +51 -35
- package/src/repo-shape.ts +8 -4
- package/src/versions.ts +61 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Every input a repository declares, and what it renders
|
|
2
|
+
|
|
3
|
+
`RepoShape` is the whole declaration. This is the field reference; the model —
|
|
4
|
+
why a difference is an input or an exception and never a hand edit — is in
|
|
5
|
+
[repo-shape.md](./repo-shape.md).
|
|
6
|
+
|
|
7
|
+
## The fields
|
|
8
|
+
|
|
9
|
+
| Field | Renders | Measured reason it is an input |
|
|
10
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| `owner` | The `repo:` in `.changeset/config.json` | The only text that varied in that file across 11 of 13 repositories |
|
|
12
|
+
| `repository` | The same, plus the Dependabot target | As above |
|
|
13
|
+
| `runner` | `runs-on:` in every job, whether `.github/actionlint.yaml` exists, and the header's billing note | 12 repositories on the mini, 1 (`homeflare-kit`) on `ubuntu-latest`. One variable, three files |
|
|
14
|
+
| `publishes` | `access: public\|restricted`, whether `privatePackages` is written, whether Dependabot watches `/packages/*` | `@changesets/cli` versions **nothing** when `privatePackages.version` is absent — a silent no-op |
|
|
15
|
+
| `node` | `actions/setup-node@v6` before `setup-bun`, in `check` and every bun extra job | See below |
|
|
16
|
+
| `extraJobs` | One job per entry, its stated reason above it, and its id in the `ci` aggregate's `needs` | 4 genuinely different jobs across 3 repositories |
|
|
17
|
+
| `exceptions` | Nothing — it removes a file from the drift comparison | The escape hatch, priced so it is used last |
|
|
18
|
+
|
|
19
|
+
## `node` — the input added 2026-09-22
|
|
20
|
+
|
|
21
|
+
⛔ **The mini's job image carries no node at all.** `ubuntu-latest` always did, so a
|
|
22
|
+
Bun-only prologue is right for twelve repositories and _silently wrong_ for two:
|
|
23
|
+
|
|
24
|
+
- **homeflare-alerts** — `tests/alchemy-import.test.ts` spawns `node` to prove the modules
|
|
25
|
+
load the way the Alchemy CLI (`node …/cli.js`) loads them. Without a real node, three
|
|
26
|
+
tests fail with `Executable not found in $PATH: "node"`, measured on the runner.
|
|
27
|
+
- **homeflare-blog** — Payload requires Node >= 24.15, so every lane needs it.
|
|
28
|
+
|
|
29
|
+
Both had written the identical `actions/setup-node@v6` block by hand before this existed.
|
|
30
|
+
The alternative was `except({ file: '.github/workflows/ci.yml', … })` in both — which
|
|
31
|
+
would have handed the estate's two most complicated CI files back to hand-editing, in the
|
|
32
|
+
round whose whole purpose was to stop that.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export const shape: RepoShape = {
|
|
36
|
+
owner: 'taslabs-net',
|
|
37
|
+
repository: 'homeflare-alerts',
|
|
38
|
+
runner: 'mini',
|
|
39
|
+
publishes: false,
|
|
40
|
+
node: 24, // a major, not a range: setup-node resolves the newest 24.x
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
⚠️ `package-manager-cache: false` is rendered with it. Bun does the installing, so priming
|
|
45
|
+
npm's cache costs time and caches nothing anything in the job reads.
|
|
46
|
+
|
|
47
|
+
★ **`workflow lint` never gets it.** That job installs nothing — actionlint is preloaded
|
|
48
|
+
on the image — so adding node there would pay for a toolchain no step uses.
|
|
49
|
+
|
|
50
|
+
## `timeout` on an extra job
|
|
51
|
+
|
|
52
|
+
`extraJob({ …, timeout: 15 })` renders `timeout-minutes: 15`.
|
|
53
|
+
|
|
54
|
+
⚠️ **A hung job is not a failed job.** homeflare-blog's `runtime` job drives Playwright
|
|
55
|
+
against a local workerd; a browser that never reaches its first paint holds a self-hosted
|
|
56
|
+
slot for GitHub's default 360 minutes rather than reporting red, and on a 3-slot pool that
|
|
57
|
+
is the whole pool. Only a job that starts something with its own wait — a browser, a
|
|
58
|
+
server, a container — needs one. `check` does not: `bun run check` exits.
|
|
59
|
+
|
|
60
|
+
## What the renderer owns, and what it does not
|
|
61
|
+
|
|
62
|
+
The rendered `check` job runs one step: `bun run check`, the repository's own gate.
|
|
63
|
+
|
|
64
|
+
That is deliberate. A workflow that re-lists `lint`, `types` and `test` is a second copy
|
|
65
|
+
of the gate, and a copy can check _less_ than the original. Measured 2026-09-22:
|
|
66
|
+
`bun run check` in `homeflare-kit` is `lint && types && build && test`, and its
|
|
67
|
+
`tests/dist.test.ts` skips itself when `dist/` is absent — so a workflow that ran the
|
|
68
|
+
three lanes without the build would drop that test silently and still report green.
|
|
69
|
+
`homeflare-alerts` runs `check:types` and `build:web` in its check; `homeflare-subnet-calc`
|
|
70
|
+
delegates to `verify`. All fourteen have a `check` script, and all fourteen are local-only.
|
|
71
|
+
|
|
72
|
+
So the split is:
|
|
73
|
+
|
|
74
|
+
| Owned by the renderer | Owned by the repository |
|
|
75
|
+
| ------------------------------------------------ | ------------------------------------------ |
|
|
76
|
+
| Triggers, permissions, concurrency, runner label | What `bun run check` runs |
|
|
77
|
+
| Action versions and their pins | Which extra jobs exist, each with a reason |
|
|
78
|
+
| The aggregate `ci` job and its `needs` | Its `package.json` scripts |
|
|
79
|
+
| The actionlint, Dependabot and changeset configs | — |
|
|
80
|
+
|
|
81
|
+
The cost, said plainly: a red X says `check` rather than naming the lane. `bun run check`
|
|
82
|
+
short-circuits on the first failure and names the lane in its output, which is the same
|
|
83
|
+
signal a person gets locally.
|
package/docs/repo-shape.md
CHANGED
|
@@ -150,28 +150,10 @@ Three further things the check refuses, each because it hides a reason:
|
|
|
150
150
|
|
|
151
151
|
## What the renderer owns, and what it does not
|
|
152
152
|
|
|
153
|
-
The rendered `check` job runs one step: `bun run check`, the repository's own gate
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`bun run check` in `homeflare-kit` is `lint && types && build && test`, and its
|
|
158
|
-
`tests/dist.test.ts` skips itself when `dist/` is absent — so a workflow that ran the
|
|
159
|
-
three lanes without the build would drop that test silently and still report green.
|
|
160
|
-
`homeflare-alerts` runs `check:types` and `build:web` in its check; `homeflare-subnet-calc`
|
|
161
|
-
delegates to `verify`. All fourteen have a `check` script, and all fourteen are local-only.
|
|
162
|
-
|
|
163
|
-
So the split is:
|
|
164
|
-
|
|
165
|
-
| Owned by the renderer | Owned by the repository |
|
|
166
|
-
| ------------------------------------------------ | ------------------------------------------ |
|
|
167
|
-
| Triggers, permissions, concurrency, runner label | What `bun run check` runs |
|
|
168
|
-
| Action versions and their pins | Which extra jobs exist, each with a reason |
|
|
169
|
-
| The aggregate `ci` job and its `needs` | Its `package.json` scripts |
|
|
170
|
-
| The actionlint, Dependabot and changeset configs | — |
|
|
171
|
-
|
|
172
|
-
The cost, said plainly: a red X says `check` rather than naming the lane. `bun run check`
|
|
173
|
-
short-circuits on the first failure and names the lane in its output, which is the same
|
|
174
|
-
signal a person gets locally.
|
|
153
|
+
The rendered `check` job runs one step: `bun run check`, the repository's own gate — so
|
|
154
|
+
the renderer owns the plumbing and the repository owns what its gate runs. That split,
|
|
155
|
+
and every field of `RepoShape` with the measurement behind it, is in
|
|
156
|
+
[repo-shape-inputs.md](./repo-shape-inputs.md).
|
|
175
157
|
|
|
176
158
|
## Bumping `@homeflare/config` will go red before it goes green
|
|
177
159
|
|
|
@@ -179,7 +161,7 @@ signal a person gets locally.
|
|
|
179
161
|
renderer changes — a new action version, a fixed permission — every repository that bumps
|
|
180
162
|
`@homeflare/config` fails `bun run check` on the next run, because its committed files are
|
|
181
163
|
now the _old_ render. That includes the Dependabot pull request that does the bumping,
|
|
182
|
-
which cannot fix itself.
|
|
164
|
+
which cannot fix itself ([repo-shape-dependabot.md](repo-shape-dependabot.md) says why).
|
|
183
165
|
|
|
184
166
|
The fix is one command, and the failure names it. The rule that follows:
|
|
185
167
|
|
package/docs/versions.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# The estate version set: `@homeflare/config/versions`
|
|
2
|
+
|
|
3
|
+
Status: published source. Not yet enforced anywhere outside the kit. Verified 2026-09-22.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { ESTATE_VERSIONS } from '@homeflare/config/versions';
|
|
7
|
+
|
|
8
|
+
ESTATE_VERSIONS.alchemy; // '2.0.0-beta.79'
|
|
9
|
+
ESTATE_VERSIONS.bun; // '1.4.0', the same value as BUN_VERSION in ./repo-shape
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| package | version | why this one |
|
|
13
|
+
| ----------------------------- | ------------------- | ---------------------------------------------------------- |
|
|
14
|
+
| `bun` | `1.4.0` | the value every rendered CI installs (`BUN_VERSION`) |
|
|
15
|
+
| `alchemy` | `2.0.0-beta.79` | the newest release on npm when this was written |
|
|
16
|
+
| `effect` | `4.0.0-rc.115` | what that alchemy release overrides its whole workspace to |
|
|
17
|
+
| `@distilled.cloud/cloudflare` | `1.0.0-rc.12` | what that alchemy release depends on, exactly |
|
|
18
|
+
| `typescript` | `7.0.2` | the kit catalog |
|
|
19
|
+
| `oxfmt` / `oxlint` | `0.68.0` / `1.83.0` | the kit catalog; ahead of upstream's `^0.66.0` / `^1.82.0` |
|
|
20
|
+
| `@types/bun` | `1.4.2` | matches `bun` |
|
|
21
|
+
|
|
22
|
+
## Where the truth lives
|
|
23
|
+
|
|
24
|
+
⛔ **The kit's root `catalog` is the source, and this export is its published copy.** Before
|
|
25
|
+
this export existed, the pins were written in two places. Bun was `BUN_VERSION` in
|
|
26
|
+
`./repo-shape`. Everything else sat in the kit's root `catalog`, which is never published.
|
|
27
|
+
So no other repository had anything to compare its lockfile against.
|
|
28
|
+
`tests/estate-versions.test.ts` in the kit fails whenever a value here differs from the
|
|
29
|
+
catalog, from the root `overrides`, from `packageManager`, or from what the installed
|
|
30
|
+
`alchemy` itself depends on. A bump therefore changes both in one PR, or it goes red.
|
|
31
|
+
|
|
32
|
+
★ **Runtime pins follow `alchemy`.** `effect` and `@distilled.cloud/*` are whatever the
|
|
33
|
+
pinned `alchemy` release was built and tested against. They move in the PR that bumps
|
|
34
|
+
`alchemy`, never ahead of it. Upstream `main` already runs `effect` rc.117. That is not a
|
|
35
|
+
reason to move until a release carries it.
|
|
36
|
+
|
|
37
|
+
## What drifts today
|
|
38
|
+
|
|
39
|
+
The kit's `alchemy-provider-standard` audit measured each estate repository's lockfile
|
|
40
|
+
read-only on 2026-09-22:
|
|
41
|
+
|
|
42
|
+
- Seven app repositories resolve `alchemy` 2.0.0-beta.78.
|
|
43
|
+
- The monorepo resolves beta.77, with `effect` rc.112, distilled rc.9, oxfmt 0.66.0 and
|
|
44
|
+
oxlint 1.81.0, and installs with pnpm.
|
|
45
|
+
- One app resolves TypeScript 5.9.3.
|
|
46
|
+
- Every consumer resolves an older `@homeflare/config` than the one published.
|
|
47
|
+
|
|
48
|
+
⚠️ **This export compares nothing on its own.** Adding a lockfile check to `checkProject`,
|
|
49
|
+
so that every repository fails on drift, is a separate estate-wide rollout. It has to land
|
|
50
|
+
together with the bumps it would demand. Otherwise every CI goes red on the same day.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Shared tsconfig, oxlint, oxfmt, and non-npm release helpers for HomeFlare projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Timothy Schneider",
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
"types": "./dist/require-release-config.d.ts",
|
|
54
54
|
"default": "./dist/require-release-config.js"
|
|
55
55
|
},
|
|
56
|
+
"./versions": {
|
|
57
|
+
"types": "./dist/versions.d.ts",
|
|
58
|
+
"default": "./dist/versions.js"
|
|
59
|
+
},
|
|
56
60
|
"./package.json": "./package.json"
|
|
57
61
|
},
|
|
58
62
|
"publishConfig": {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.github/workflows/dependabot-automerge.yml`, rendered.
|
|
3
|
+
*
|
|
4
|
+
* ★ THE OTHER HALF OF THE `homeflare` GROUP. Dependabot opens the bump; this arms GitHub's
|
|
5
|
+
* own auto-merge on it; the branch ruleset's required checks (`ci`, `secret scan`)
|
|
6
|
+
* decide whether it ever merges. Nothing here merges anything itself, and a merge
|
|
7
|
+
* deploys nothing — every stack in the estate is deployed by hand.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ `gh pr merge --auto` IS NOT ALWAYS "ARM". When the pull request is already CLEAN,
|
|
10
|
+
* UNSTABLE or HAS_HOOKS it merges AT ONCE instead (cli/cli `pkg/cmd/pr/merge/merge.go`,
|
|
11
|
+
* `isImmediatelyMergeable`, read at v2.101.0 — the version the mini's job image ships).
|
|
12
|
+
* UNSTABLE is "mergeable, with a non-passing status", which is every pull request on a
|
|
13
|
+
* branch whose rules require no check: before `check` has run, and even after it failed.
|
|
14
|
+
* So the step reads the base branch's active rules first and refuses — a red check, never
|
|
15
|
+
* a merge — when none requires a status check. Measured 2026-09-22: homeflare-builds (its
|
|
16
|
+
* ruleset not deployed yet) and homeflare-desktop have none.
|
|
17
|
+
*
|
|
18
|
+
* ⛔ FIRST-PARTY ONLY: A `run:` STEP AND THE GitHub CLI, NO `uses:` AT ALL. GitHub's own
|
|
19
|
+
* example ("Automating Dependabot with GitHub Actions") identifies the update with
|
|
20
|
+
* `dependabot/fetch-metadata`, which the same page marks as "not certified by GitHub".
|
|
21
|
+
* The group is identified by the branch Dependabot gives it instead, whose format is
|
|
22
|
+
* dependabot-core's (`branch_namer/dependency_group_strategy.rb`, read at v0.397.0):
|
|
23
|
+
* `<prefix>/<package manager>/<directory>/<group>-<10 hex MD5 digest>`, with the root
|
|
24
|
+
* directory collapsing to nothing.
|
|
25
|
+
*/
|
|
26
|
+
import { HOMEFLARE_GROUP } from './dependabot.ts';
|
|
27
|
+
import type { RepoShape } from './shape.ts';
|
|
28
|
+
import { runsOn } from './shape.ts';
|
|
29
|
+
import { renderSteps } from './yaml.ts';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The branch prefix a job-level `if:` can test with `startsWith`. GitHub's expression
|
|
33
|
+
* language has no regex, so this is the cheap filter that keeps every other pull request
|
|
34
|
+
* from taking a runner slot; `GROUP_BRANCH` is the exact test inside the step.
|
|
35
|
+
*/
|
|
36
|
+
export const GROUP_BRANCH_PREFIX: string = `dependabot/bun/${HOMEFLARE_GROUP}-`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ⛔ EXACT, AND FAILS CLOSED. A solo update of `@homeflare/config` is
|
|
40
|
+
* `dependabot/bun/homeflare/config-0.9.0` and a package named `homeflare-x` would be
|
|
41
|
+
* `dependabot/bun/homeflare-x-1.2.3`; neither matches. If Dependabot ever changes its
|
|
42
|
+
* format, the symptom is a bump that waits for a person — never one merged by mistake.
|
|
43
|
+
*/
|
|
44
|
+
export const GROUP_BRANCH: string = `^${GROUP_BRANCH_PREFIX}[0-9a-f]{10}$`;
|
|
45
|
+
|
|
46
|
+
const HEADER = `# Arms auto-merge on Dependabot's @homeflare/* group, and on nothing else.
|
|
47
|
+
#
|
|
48
|
+
# 🤖 RENDERED BY @homeflare/config — DO NOT EDIT THIS FILE BY HAND.
|
|
49
|
+
# Its input is this repository's \`repo-shape.ts\`; refresh with \`bun run repo-shape:refresh\`.
|
|
50
|
+
#
|
|
51
|
+
# ★ HOW A KIT RELEASE ARRIVES: the \`${HOMEFLARE_GROUP}\` group in .github/dependabot.yml opens one
|
|
52
|
+
# pull request; this arms GitHub's auto-merge on it; the ruleset's required checks decide.
|
|
53
|
+
# Nothing here merges anything itself, and merging deploys nothing.
|
|
54
|
+
# ⛔ NO THIRD-PARTY ACTION. GitHub's own example uses dependabot/fetch-metadata, which its
|
|
55
|
+
# docs mark "not certified by GitHub"; the group is recognised by its branch name instead.
|
|
56
|
+
# ⛔ NO REQUIRED CHECK ON THE BASE BRANCH, NO ARMING: there \`gh pr merge --auto\` would merge
|
|
57
|
+
# at once, unchecked. The job fails instead, so the pull request waits for a person.
|
|
58
|
+
# ⚠️ A RENDERER CHANGE STILL NEEDS A PERSON. When a kit release changes what @homeflare/config
|
|
59
|
+
# renders, the bump fails \`check\` (the drift test) and never goes green. Run
|
|
60
|
+
# \`bun run repo-shape:refresh\` on Dependabot's branch and push. ⛔ This workflow does not do
|
|
61
|
+
# it for you: a push made with GITHUB_TOKEN starts no workflow run, so a refreshed commit
|
|
62
|
+
# would never get the required checks and the pull request would wait forever.
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const TRIGGER = `name: dependabot auto-merge
|
|
66
|
+
|
|
67
|
+
on:
|
|
68
|
+
pull_request:
|
|
69
|
+
|
|
70
|
+
# ⛔ NOTHING AT THE TOP; the one job asks for exactly what \`gh pr merge --auto\` needs.
|
|
71
|
+
permissions: {}
|
|
72
|
+
|
|
73
|
+
concurrency:
|
|
74
|
+
group: automerge-\${{ github.ref }}
|
|
75
|
+
cancel-in-progress: true
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const JOB_NOTE = ` # ⛔ BOTH LOGINS, NOT EITHER. \`user.login\` is who opened the pull request; \`github.actor\` is
|
|
79
|
+
# who started this run. A person pushing to Dependabot's branch changes the actor, so
|
|
80
|
+
# their commit never arms anything; only Dependabot's own rebases do.`;
|
|
81
|
+
|
|
82
|
+
const PERMISSIONS_NOTE = ` # ★ WHAT GitHub'S OWN EXAMPLE GRANTS FOR THIS STEP, AND NO MORE. A run Dependabot starts gets
|
|
83
|
+
# a read-only GITHUB_TOKEN unless the workflow raises it ("Troubleshooting Dependabot on
|
|
84
|
+
# GitHub Actions" → "Changing GITHUB_TOKEN permissions").`;
|
|
85
|
+
|
|
86
|
+
const ARM = `set -euo pipefail
|
|
87
|
+
if [[ ! "$HEAD_REF" =~ ${GROUP_BRANCH} ]]; then
|
|
88
|
+
echo "::notice::$HEAD_REF is not the ${HOMEFLARE_GROUP} group's branch; auto-merge not armed"
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
# ⛔ gh merges a CLEAN or UNSTABLE pull request at once rather than arming it. Only a branch
|
|
92
|
+
# rule that requires a status check keeps it BLOCKED until the checks have passed.
|
|
93
|
+
required=$(gh api "repos/$GITHUB_REPOSITORY/rules/branches/$BASE_REF" \\
|
|
94
|
+
--jq '[.[] | select(.type == "required_status_checks")] | length')
|
|
95
|
+
if [ "$required" = 0 ]; then
|
|
96
|
+
echo "::error::$BASE_REF requires no status check, so gh would merge at once; auto-merge not armed"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
# ★ --squash: the house repositories allow squash merges only (declareRepoPolicy).
|
|
100
|
+
gh pr merge --auto --squash "$PR_URL"`;
|
|
101
|
+
|
|
102
|
+
/** The whole `dependabot-automerge.yml` for a shape. */
|
|
103
|
+
export function renderAutomerge(shape: RepoShape): string {
|
|
104
|
+
// ⚠️ GitHub ACTIONS EXPRESSIONS, NOT TEMPLATE LITERALS: the runner interpolates `${{ … }}`
|
|
105
|
+
// at job time, so they must reach the file verbatim (see security.ts for the same note).
|
|
106
|
+
// oxlint-disable-next-line no-template-curly-in-string
|
|
107
|
+
const headRef = '${{ github.head_ref }}';
|
|
108
|
+
// oxlint-disable-next-line no-template-curly-in-string
|
|
109
|
+
const baseRef = '${{ github.base_ref }}';
|
|
110
|
+
// oxlint-disable-next-line no-template-curly-in-string
|
|
111
|
+
const prUrl = '${{ github.event.pull_request.html_url }}';
|
|
112
|
+
// oxlint-disable-next-line no-template-curly-in-string
|
|
113
|
+
const token = '${{ secrets.GITHUB_TOKEN }}';
|
|
114
|
+
const condition = [
|
|
115
|
+
"github.event.pull_request.user.login == 'dependabot[bot]'",
|
|
116
|
+
"github.actor == 'dependabot[bot]'",
|
|
117
|
+
`startsWith(github.head_ref, '${GROUP_BRANCH_PREFIX}')`,
|
|
118
|
+
].join(' && ');
|
|
119
|
+
|
|
120
|
+
return `${HEADER}
|
|
121
|
+
${TRIGGER}
|
|
122
|
+
jobs:
|
|
123
|
+
${JOB_NOTE}
|
|
124
|
+
arm:
|
|
125
|
+
name: arm auto-merge
|
|
126
|
+
if: ${condition}
|
|
127
|
+
runs-on: ${runsOn(shape.runner)}
|
|
128
|
+
${PERMISSIONS_NOTE}
|
|
129
|
+
permissions:
|
|
130
|
+
contents: write
|
|
131
|
+
pull-requests: write
|
|
132
|
+
steps:
|
|
133
|
+
${renderSteps(
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
env: { BASE_REF: baseRef, GH_TOKEN: token, HEAD_REF: headRef, PR_URL: prUrl },
|
|
137
|
+
name: 'Arm auto-merge on the homeflare group',
|
|
138
|
+
run: ARM,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
3,
|
|
142
|
+
)}
|
|
143
|
+
`;
|
|
144
|
+
}
|
package/src/repo-shape/ci.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* `needs`, so adding a job never means editing a branch ruleset. `repoShapeChecks()`
|
|
11
11
|
* returns exactly `['ci', 'secret scan']`, which is what `declareRepoPolicy` requires.
|
|
12
12
|
*/
|
|
13
|
-
import type { ExtraJob, RepoShape } from './shape.ts';
|
|
14
|
-
import { runsOn } from './shape.ts';
|
|
13
|
+
import type { ExtraJob, JobStep, RepoShape } from './shape.ts';
|
|
14
|
+
import { nodeMajor, runsOn } from './shape.ts';
|
|
15
15
|
import { renderSteps } from './yaml.ts';
|
|
16
16
|
|
|
17
17
|
/** Bun the whole estate is pinned to. One line, one place. */
|
|
@@ -21,6 +21,7 @@ export const ACTIONLINT_VERSION = '1.7.12';
|
|
|
21
21
|
|
|
22
22
|
const CHECKOUT = 'actions/checkout@v7';
|
|
23
23
|
const SETUP_BUN = 'oven-sh/setup-bun@v2';
|
|
24
|
+
const SETUP_NODE = 'actions/setup-node@v6';
|
|
24
25
|
|
|
25
26
|
function runnerNote(shape: RepoShape): string {
|
|
26
27
|
if (shape.runner !== 'mini') {
|
|
@@ -148,31 +149,49 @@ const VERIFY = `if [ "\${{ contains(needs.*.result, 'failure') }}" = "true" ] ||
|
|
|
148
149
|
exit 1
|
|
149
150
|
fi`;
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
const NODE_NOTE = ` # ⛔ REAL NODE, NOT BUN'S SHIM, AND ONLY WHERE THE GATE NEEDS IT. The mini's job
|
|
153
|
+
# image has no node on PATH (ubuntu-latest always did), so a repository whose own
|
|
154
|
+
# \`check\` spawns \`node\` — or whose framework demands a Node runtime — fails with
|
|
155
|
+
# \`Executable not found in $PATH: "node"\` without this. Declared as \`node:\` in
|
|
156
|
+
# repo-shape.ts, so it is one input rather than a hand-edited block per repository.
|
|
157
|
+
# ⚠️ NO PACKAGE-MANAGER CACHE: bun does the installing, so priming npm's cache costs
|
|
158
|
+
# time and caches nothing anything here reads.`;
|
|
159
|
+
|
|
160
|
+
function prologue(shape: RepoShape): string {
|
|
161
|
+
const node = nodeMajor(shape);
|
|
162
|
+
const bun: JobStep[] = [
|
|
163
|
+
{ uses: SETUP_BUN, with: { 'bun-version': BUN_VERSION } },
|
|
164
|
+
{ run: 'bun install --frozen-lockfile' },
|
|
165
|
+
];
|
|
166
|
+
if (node === undefined) return renderSteps([{ uses: CHECKOUT }, ...bun], 3);
|
|
167
|
+
return [
|
|
168
|
+
renderSteps([{ uses: CHECKOUT }], 3),
|
|
169
|
+
NODE_NOTE,
|
|
170
|
+
renderSteps(
|
|
171
|
+
[{ uses: SETUP_NODE, with: { 'node-version': node, 'package-manager-cache': 'false' } }],
|
|
172
|
+
3,
|
|
173
|
+
),
|
|
174
|
+
renderSteps(bun, 3),
|
|
175
|
+
].join('\n');
|
|
160
176
|
}
|
|
161
177
|
|
|
162
|
-
function renderExtraJob(job: ExtraJob, on: string): string {
|
|
178
|
+
function renderExtraJob(job: ExtraJob, shape: RepoShape, on: string): string {
|
|
163
179
|
const needs =
|
|
164
180
|
(job.needs ?? []).length === 0 ? '' : ` needs: [${(job.needs ?? []).join(', ')}]\n`;
|
|
181
|
+
// ★ A TIMEOUT IS THE JOB'S, NOT THE SHAPE'S. Only a job that starts something with its
|
|
182
|
+
// own wait needs one, and it is rendered where a reader looks for it.
|
|
183
|
+
const timeout = job.timeout === undefined ? '' : ` timeout-minutes: ${job.timeout}\n`;
|
|
165
184
|
const steps =
|
|
166
185
|
job.bun === false
|
|
167
186
|
? renderSteps([{ uses: CHECKOUT }, ...job.steps], 3)
|
|
168
|
-
: [prologue(), renderSteps(job.steps, 3)].join('\n');
|
|
187
|
+
: [prologue(shape), renderSteps(job.steps, 3)].join('\n');
|
|
169
188
|
// ★ The stated reason is rendered into the file. A job nobody can explain is a job
|
|
170
189
|
// nobody dares delete, so the explanation travels with it.
|
|
171
190
|
return ` # ★ NOT PART OF THE STANDARD SHAPE — ${job.reason}
|
|
172
191
|
${job.id}:
|
|
173
192
|
name: ${job.name}
|
|
174
193
|
${needs} runs-on: ${on}
|
|
175
|
-
steps:
|
|
194
|
+
${timeout} steps:
|
|
176
195
|
${steps}
|
|
177
196
|
`;
|
|
178
197
|
}
|
|
@@ -190,7 +209,7 @@ ${CHECK_NOTE}
|
|
|
190
209
|
name: check
|
|
191
210
|
runs-on: ${on}
|
|
192
211
|
steps:
|
|
193
|
-
${prologue()}
|
|
212
|
+
${prologue(shape)}
|
|
194
213
|
${renderSteps([{ run: 'bun run check' }], 3)}
|
|
195
214
|
|
|
196
215
|
${WORKFLOWS_NOTE}
|
|
@@ -203,7 +222,7 @@ ${ACTIONLINT_NOTE}
|
|
|
203
222
|
${renderSteps([{ name: 'Install actionlint (checksum-verified)', run: INSTALL_ACTIONLINT }], 3)}
|
|
204
223
|
${renderSteps([{ name: 'Lint workflows', run: './actionlint -color' }], 3)}
|
|
205
224
|
|
|
206
|
-
${extras.map((job) => `${renderExtraJob(job, on)}\n`).join('')}${AGGREGATE_NOTE}
|
|
225
|
+
${extras.map((job) => `${renderExtraJob(job, shape, on)}\n`).join('')}${AGGREGATE_NOTE}
|
|
207
226
|
ci:
|
|
208
227
|
name: ci
|
|
209
228
|
if: always()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* Dependabot.
|
|
2
|
+
* The two smaller rendered files: actionlint's config and the changeset config.
|
|
3
|
+
* (Dependabot was the third; it grew a group and a schedule rule, so it is `dependabot.ts`.)
|
|
4
4
|
*
|
|
5
5
|
* ★ EACH ONE WAS MEASURED BEFORE IT WAS RENDERED (2026-09-22, across 13 repositories):
|
|
6
6
|
* · `.changeset/config.json` — byte-identical apart from the repository name in 11 of
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
* · `.github/actionlint.yaml` — 12 of 13 had it, with three wordings of one comment.
|
|
11
11
|
* The one without it is the one repository still on `ubuntu-latest`, which is the
|
|
12
12
|
* only case where it is genuinely not needed. That is an input, not an exception.
|
|
13
|
-
* · `.github/dependabot.yml` — 1 of 14. Twelve repositories take no dependency or
|
|
14
|
-
* Action updates at all, and nothing said so. Rendering it is the fix.
|
|
15
13
|
*/
|
|
16
14
|
import type { RepoShape } from './shape.ts';
|
|
17
15
|
|
|
@@ -76,76 +74,3 @@ self-hosted-runner:
|
|
|
76
74
|
- homeflare-mini
|
|
77
75
|
`;
|
|
78
76
|
}
|
|
79
|
-
|
|
80
|
-
/** `.github/dependabot.yml`. */
|
|
81
|
-
export function renderDependabot(shape: RepoShape): string {
|
|
82
|
-
// ⚠️ `directories`, not `directory`, for bun: a workspace keeps a dependency in the
|
|
83
|
-
// package that declares it, so pointing only at `/` leaves every `packages/*`
|
|
84
|
-
// manifest unwatched — and the symptom is silence, not an error.
|
|
85
|
-
const bunDirs = shape.publishes ? ['/', '/packages/*'] : ['/'];
|
|
86
|
-
return `# Dependabot for ${shape.repository}.
|
|
87
|
-
#
|
|
88
|
-
# 🤖 RENDERED BY @homeflare/config — DO NOT EDIT THIS FILE BY HAND.
|
|
89
|
-
# Refresh with \`bun run repo-shape:refresh\`.
|
|
90
|
-
#
|
|
91
|
-
# ⛔ THIS FILE LIVES AT .github/dependabot.yml, NOT IN .github/workflows/. Dependabot is a
|
|
92
|
-
# platform feature, not an Action — a config placed among the workflows is silently
|
|
93
|
-
# ignored, and the symptom is simply that no pull requests ever arrive. Measured
|
|
94
|
-
# 2026-09-22: 13 of 14 HomeFlare repositories had no dependabot config at all, so their
|
|
95
|
-
# Actions and their toolchain went stale invisibly, which is exactly how nothing fails.
|
|
96
|
-
#
|
|
97
|
-
# ★ WHY GROUPED RATHER THAN ONE PR PER DEPENDENCY. The default opens a pull request per
|
|
98
|
-
# outdated package; that is a trickle nobody reviews properly. Each group below is a set
|
|
99
|
-
# that is either safe to take together or needs deciding together.
|
|
100
|
-
version: 2
|
|
101
|
-
|
|
102
|
-
updates:
|
|
103
|
-
# ── The toolchain (bun.lock) ────────────────────────────────────────────────
|
|
104
|
-
- package-ecosystem: bun
|
|
105
|
-
directories:
|
|
106
|
-
${bunDirs.map((dir) => ` - ${dir}`).join('\n')}
|
|
107
|
-
schedule:
|
|
108
|
-
interval: weekly
|
|
109
|
-
day: monday
|
|
110
|
-
time: '09:00'
|
|
111
|
-
timezone: America/New_York
|
|
112
|
-
open-pull-requests-limit: 5
|
|
113
|
-
commit-message:
|
|
114
|
-
prefix: 'chore'
|
|
115
|
-
include: scope
|
|
116
|
-
labels: [dependencies]
|
|
117
|
-
groups:
|
|
118
|
-
# oxfmt and oxlint move together and only affect style. Minor and patch bumps are
|
|
119
|
-
# noise unless they fail CI, which is what CI is for.
|
|
120
|
-
lint-and-format:
|
|
121
|
-
patterns: ['oxfmt', 'oxlint']
|
|
122
|
-
update-types: [minor, patch]
|
|
123
|
-
|
|
124
|
-
# ⚠️ MAJORS EXCLUDED DELIBERATELY. TypeScript majors change what typechecks;
|
|
125
|
-
# changesets majors have renamed inputs and dropped compatibility (the action's v2
|
|
126
|
-
# did both). These want reading, not merging on green.
|
|
127
|
-
build-tooling:
|
|
128
|
-
patterns: ['typescript', '@changesets/*', '@types/bun']
|
|
129
|
-
update-types: [minor, patch]
|
|
130
|
-
|
|
131
|
-
# ── The workflows themselves ────────────────────────────────────────────────
|
|
132
|
-
# ★ Actions go stale invisibly: nothing fails, they just keep running old code. The
|
|
133
|
-
# estate's first workflows pinned checkout@v5 (current: v7) and changesets/action@v1
|
|
134
|
-
# (current: v2, with every input renamed) — measured 2026-09-15.
|
|
135
|
-
- package-ecosystem: github-actions
|
|
136
|
-
directory: /
|
|
137
|
-
schedule:
|
|
138
|
-
interval: weekly
|
|
139
|
-
day: monday
|
|
140
|
-
time: '09:00'
|
|
141
|
-
timezone: America/New_York
|
|
142
|
-
open-pull-requests-limit: 5
|
|
143
|
-
commit-message:
|
|
144
|
-
prefix: 'ci'
|
|
145
|
-
labels: [dependencies, github-actions]
|
|
146
|
-
groups:
|
|
147
|
-
actions:
|
|
148
|
-
patterns: ['*']
|
|
149
|
-
update-types: [minor, patch]
|
|
150
|
-
`;
|
|
151
|
-
}
|