@homeflare/config 0.11.1 → 0.12.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.
@@ -12,9 +12,17 @@
12
12
  * ⚠️ IT NEVER WRITES AN EXCEPTED FILE. Refreshing a file the repository declared it owns
13
13
  * would overwrite the deviation the reason was written for — the one destructive thing
14
14
  * this could do, and the one it must not.
15
+ *
16
+ * ⚠️ IT DELETES A RETIRED FILE ONLY WHEN IT CAN PROVE IT RENDERED IT. `RETIRED_FILES`
17
+ * (`retired.ts`) names every path this package used to render; `wasRenderedByUs` is the
18
+ * proof — the file still carries the generated-file header. A retired path present
19
+ * without that header is left alone and reported as refused, on the same reasoning as
20
+ * never overwriting an excepted file: this writer only ever removes what it is sure is
21
+ * its own.
15
22
  */
16
23
  import { REFRESH_COMMAND, driftInRepoShape, exceptionSummary } from './drift.ts';
17
24
  import { renderRepoShape } from './render.ts';
25
+ import { RETIRED_FILES, wasRenderedByUs } from './retired.ts';
18
26
  import { type RepoShape, isExcepted } from './shape.ts';
19
27
  import type { RenderedPath } from './shape.ts';
20
28
 
@@ -25,9 +33,13 @@ export interface RefreshResult {
25
33
  readonly unchanged: readonly string[];
26
34
  /** Paths skipped because the shape declares an exception for them. */
27
35
  readonly skipped: readonly string[];
36
+ /** Retired paths deleted because they still carried the generated-file header. */
37
+ readonly removed: readonly string[];
38
+ /** Retired paths left alone because they did not — provably hand-written, not ours. */
39
+ readonly refused: readonly string[];
28
40
  }
29
41
 
30
- /** Write a repository's rendered files into `projectDir`. */
42
+ /** Write a repository's rendered files into `projectDir`, and clear its rendered fossils. */
31
43
  export async function refreshRepoShape(
32
44
  projectDir: string,
33
45
  shape: RepoShape,
@@ -52,7 +64,36 @@ export async function refreshRepoShape(
52
64
  written.push(path);
53
65
  }
54
66
 
55
- return { skipped, unchanged, written };
67
+ const { refused, removed } = await clearRetiredFiles(projectDir);
68
+
69
+ return { refused, removed, skipped, unchanged, written };
70
+ }
71
+
72
+ /**
73
+ * Delete every retired path this package can prove it rendered; leave every other one, and
74
+ * say so. A path absent from `projectDir` is neither removed nor refused — there is
75
+ * nothing there to have an opinion about.
76
+ */
77
+ async function clearRetiredFiles(
78
+ projectDir: string,
79
+ ): Promise<{ removed: string[]; refused: string[] }> {
80
+ const removed: string[] = [];
81
+ const refused: string[] = [];
82
+
83
+ for (const retiredFile of RETIRED_FILES) {
84
+ const target = `${projectDir}/${retiredFile.path}`;
85
+ const file = Bun.file(target);
86
+ if (!(await file.exists())) continue;
87
+
88
+ if (wasRenderedByUs(await file.text())) {
89
+ await file.delete();
90
+ removed.push(retiredFile.path);
91
+ } else {
92
+ refused.push(retiredFile.path);
93
+ }
94
+ }
95
+
96
+ return { refused, removed };
56
97
  }
57
98
 
58
99
  /**
@@ -108,6 +149,18 @@ export async function repoShapeCli(
108
149
  ...result.written.map((path) => `wrote ${path}`),
109
150
  ...result.unchanged.map((path) => `ok ${path}`),
110
151
  ...result.skipped.map((path) => `excepted ${path}`),
152
+ ...result.removed.map((path) => `removed ${path} (retired; carried our header)`),
111
153
  ]);
154
+ // ★ ON ITS OWN LINE, TO STDERR: a refused retired file is the one outcome here that
155
+ // still needs a person. `--check` (above) keeps failing on it — it reports any retired
156
+ // path that is present, proof or not — so this is not the only place it is said, but
157
+ // it is the only place that says WHY refresh did not just fix it.
158
+ await say(
159
+ Bun.stderr,
160
+ result.refused.map(
161
+ (path) =>
162
+ `refused ${path} — present but not provably ours; not deleted. See docs/repo-shape-retired.md.`,
163
+ ),
164
+ );
112
165
  return 0;
113
166
  }
@@ -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
  ];
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Rendered paths this package stopped rendering — the "no fossils" list.
3
+ *
4
+ * ★ WHY THIS EXISTS. `renderRepoShape` only emits the files a shape asks for today; it
5
+ * never says what it used to emit, and `refreshRepoShape` only writes what it renders —
6
+ * so a file retired from the renderer was never deleted by a refresh. It just stopped
7
+ * being updated, silently, forever, in every repository that had already taken it.
8
+ *
9
+ * MEASURED 2026-09-24: `@homeflare/config` 0.12.0 (kit PR 199) stopped rendering
10
+ * `.github/workflows/dependabot-automerge.yml`. Every consumer that had refreshed to
11
+ * 0.12.0 before this module existed (homeflare-wiki bump PR 19, homeflare-mini bump
12
+ * PR 47) kept the dead file, and `driftInRepoShape` never said so — it only compares
13
+ * paths the CURRENT shape renders, and a retired path is not one of those.
14
+ *
15
+ * ⛔ RETIRING A PATH IS A ONE-WAY DOOR, NOT A RENAME. Once a path is here it can never
16
+ * become a `RenderedPath` again: `render.ts` would start writing a file this module is
17
+ * also trying to delete, and every refresh would fight itself. Give the replacement a
18
+ * new path instead. `repo-shape-retired.test.ts` asserts the two lists never overlap.
19
+ *
20
+ * ⚠️ DELETION IS GATED ON PROOF, NOT ON THE PATH ALONE. A repository can have a
21
+ * hand-written file sitting at the exact path a retired renderer used to own — a fork of
22
+ * the old rendered file, kept on purpose, or unrelated content that just landed there.
23
+ * `wasRenderedByUs` is the proof: every renderer in this directory writes the
24
+ * `🤖 RENDERED BY @homeflare/config` line into its header (grep the directory for it),
25
+ * and that line is the one thing a hand-written file has no reason to contain.
26
+ * `refreshRepoShape` deletes a retired path only when the line is present; otherwise it
27
+ * refuses and reports the path, the same way it refuses to overwrite an excepted file.
28
+ */
29
+ import { requireSemver, requireSentence } from './guards.ts';
30
+
31
+ /**
32
+ * A path `@homeflare/config` no longer renders, but may still find committed in a
33
+ * repository that refreshed before the retirement. Extend this union at the same time a
34
+ * path is added to `RETIRED_FILES` below — never reuse one already there.
35
+ */
36
+ export type RetiredPath = '.github/workflows/dependabot-automerge.yml';
37
+
38
+ export interface RetiredFile {
39
+ /** The path this package rendered, once. */
40
+ readonly path: RetiredPath;
41
+ /** The `@homeflare/config` version whose release stopped rendering it. */
42
+ readonly retiredIn: string;
43
+ /** Why, and which kit change did it — written at the retirement, like `except()`'s reason. */
44
+ readonly reason: string;
45
+ }
46
+
47
+ function retire(file: RetiredFile): RetiredFile {
48
+ return {
49
+ path: file.path,
50
+ reason: requireSentence('retired reason', file.reason),
51
+ retiredIn: requireSemver(file.retiredIn),
52
+ };
53
+ }
54
+
55
+ /** Every path `@homeflare/config` used to render, oldest retirement first. */
56
+ export const RETIRED_FILES: readonly RetiredFile[] = [
57
+ retire({
58
+ path: '.github/workflows/dependabot-automerge.yml',
59
+ reason:
60
+ 'kit PR 199 retired the Dependabot @homeflare group; taslabs-net/homeflare-bumper ' +
61
+ "carries a kit release into each consumer now, over the kit's own release workflow",
62
+ retiredIn: '0.12.0',
63
+ }),
64
+ ];
65
+
66
+ /**
67
+ * The substring every generated file's header has carried since this package's first
68
+ * renderer. Kept independent of the four current renderers' own copies of this text
69
+ * (`ci.ts`, `security.ts`, `dependabot.ts`, `companions.ts`) on purpose: a future wording
70
+ * change to the live header must not stop this module recognising a file rendered under
71
+ * the old one.
72
+ */
73
+ export const GENERATED_FILE_MARKER = '🤖 RENDERED BY @homeflare/config';
74
+
75
+ /**
76
+ * Whether `content` is provably a file this package once wrote.
77
+ *
78
+ * ⚠️ THE MARKER HAS TO START A LINE OF ITS OWN, NOT MERELY OCCUR SOMEWHERE IN THE TEXT.
79
+ * Every real header carries it as a whole comment line — `# 🤖 RENDERED BY
80
+ * @homeflare/config — DO NOT EDIT…`, third line in every renderer in this directory, so
81
+ * this does not require it to be the FIRST line — but a bare `content.includes(...)`
82
+ * would also match a hand-written file that merely *talks about* the marker, e.g. a
83
+ * comment reading "this file used to be 🤖 RENDERED BY @homeflare/config before it was
84
+ * retired; keeping it by hand now" — adversarial review, 2026-09-24, caught this exact
85
+ * case before it shipped. Requiring the marker at the start of a trimmed line is what a
86
+ * sentence built around it, rather than a header line consisting of it, cannot satisfy.
87
+ * `repo-shape-retired.test.ts` asserts the mid-sentence form stays refused.
88
+ *
89
+ * ⚠️ STILL A PREFIX CHECK ON THAT LINE, NOT A FULL-LINE EXACT MATCH. The retired renderer
90
+ * varied its trailing header prose by `shape.runner` and by repository name, and a
91
+ * future wording change to what follows the marker on a live renderer's header must not
92
+ * stop this recognising a file rendered under the old wording — only the marker itself,
93
+ * `🤖 RENDERED BY @homeflare/config`, has been constant across every renderer this
94
+ * package has ever shipped.
95
+ */
96
+ export function wasRenderedByUs(content: string): boolean {
97
+ return content
98
+ .split('\n')
99
+ .some((line) => line.trimStart().startsWith(`# ${GENERATED_FILE_MARKER}`));
100
+ }
101
+
102
+ /**
103
+ * The message `driftInRepoShape` reports for a retired path that is still present, and
104
+ * `repoShapeCli --check` prints verbatim. Names the fix command either way — whether that
105
+ * command can actually remove the file depends on `wasRenderedByUs`, which only
106
+ * `refreshRepoShape` (holding the file's contents) can decide.
107
+ */
108
+ export function retiredFileProblem(file: RetiredFile): string {
109
+ return (
110
+ `${file.path}: retired in @homeflare/config@${file.retiredIn} (${file.reason}) ` +
111
+ 'but still present — run `bun run repo-shape:refresh` to remove it'
112
+ );
113
+ }
@@ -86,7 +86,6 @@ export type RenderedPath =
86
86
  | '.github/actionlint.yaml'
87
87
  | '.github/dependabot.yml'
88
88
  | '.github/workflows/ci.yml'
89
- | '.github/workflows/dependabot-automerge.yml'
90
89
  | '.github/workflows/security.yml';
91
90
 
92
91
  export interface RepoShapeException {
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
- * dependabot-automerge.yml, actionlint.yaml, dependabot.yml and
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,
@@ -57,6 +54,14 @@ export {
57
54
  RENDERED_PATHS,
58
55
  renderRepoShape,
59
56
  } from './repo-shape/render.ts';
57
+ export {
58
+ type RetiredFile,
59
+ type RetiredPath,
60
+ GENERATED_FILE_MARKER,
61
+ RETIRED_FILES,
62
+ retiredFileProblem,
63
+ wasRenderedByUs,
64
+ } from './repo-shape/retired.ts';
60
65
  export { renderSecurity } from './repo-shape/security.ts';
61
66
  export {
62
67
  type ExtraJob,
@@ -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"}
@@ -1,144 +0,0 @@
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
- }