@homeflare/config 0.5.1 → 0.8.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 +46 -0
- package/bin/hooks.ts +19 -0
- package/dist/hooks/gates.d.ts +19 -0
- package/dist/hooks/gates.d.ts.map +1 -0
- package/dist/hooks/install.d.ts +20 -0
- package/dist/hooks/install.d.ts.map +1 -0
- package/dist/hooks/report.d.ts +33 -0
- package/dist/hooks/report.d.ts.map +1 -0
- package/dist/hooks/staged.d.ts +19 -0
- package/dist/hooks/staged.d.ts.map +1 -0
- package/dist/hooks.d.ts +15 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +202 -0
- package/dist/hooks.js.map +14 -0
- package/dist/repo-shape/ci.d.ts +20 -0
- package/dist/repo-shape/ci.d.ts.map +1 -0
- package/dist/repo-shape/companions.d.ts +39 -0
- package/dist/repo-shape/companions.d.ts.map +1 -0
- package/dist/repo-shape/drift.d.ts +29 -0
- package/dist/repo-shape/drift.d.ts.map +1 -0
- package/dist/repo-shape/refresh.d.ts +18 -0
- package/dist/repo-shape/refresh.d.ts.map +1 -0
- package/dist/repo-shape/render.d.ts +39 -0
- package/dist/repo-shape/render.d.ts.map +1 -0
- package/dist/repo-shape/security.d.ts +22 -0
- package/dist/repo-shape/security.d.ts.map +1 -0
- package/dist/repo-shape/shape.d.ts +153 -0
- package/dist/repo-shape/shape.d.ts.map +1 -0
- package/dist/repo-shape/yaml.d.ts +18 -0
- package/dist/repo-shape/yaml.d.ts.map +1 -0
- package/dist/repo-shape.d.ts +43 -0
- package/dist/repo-shape.d.ts.map +1 -0
- package/dist/repo-shape.js +615 -0
- package/dist/repo-shape.js.map +17 -0
- package/docs/repo-shape.md +199 -0
- package/package.json +11 -1
- package/src/hooks/gates.ts +104 -0
- package/src/hooks/install.ts +95 -0
- package/src/hooks/report.ts +96 -0
- package/src/hooks/staged.ts +68 -0
- package/src/hooks.ts +48 -0
- package/src/repo-shape/ci.ts +215 -0
- package/src/repo-shape/companions.ts +151 -0
- package/src/repo-shape/drift.ts +130 -0
- package/src/repo-shape/refresh.ts +113 -0
- package/src/repo-shape/render.ts +88 -0
- package/src/repo-shape/security.ts +115 -0
- package/src/repo-shape/shape.ts +229 -0
- package/src/repo-shape/yaml.ts +100 -0
- package/src/repo-shape.ts +69 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest YAML writer that renders a job's steps, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* ★ NOT A YAML LIBRARY, DELIBERATELY. A general serializer would take a dependency every
|
|
5
|
+
* consumer of `@homeflare/config` inherits, and it would strip the comments — which in
|
|
6
|
+
* this house are the product. The rendered workflows are written as text with holes;
|
|
7
|
+
* only the step lists, whose shape varies per repository, go through here.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ Bun can PARSE YAML natively (`Bun.YAML.parse`) but does not stringify it, measured
|
|
10
|
+
* against Bun 1.4.0 on 2026-09-22. The tests parse what this writes with `Bun.YAML.parse`
|
|
11
|
+
* and compare structures, so a malformed emission fails rather than shipping.
|
|
12
|
+
*/
|
|
13
|
+
import type { JobStep } from './shape.ts';
|
|
14
|
+
|
|
15
|
+
/** Two spaces per level, the house indent and GitHub's own. */
|
|
16
|
+
const INDENT = ' ';
|
|
17
|
+
|
|
18
|
+
function indent(depth: number): string {
|
|
19
|
+
return INDENT.repeat(depth);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ⛔ QUOTE ANYTHING YAML WOULD RE-READ AS SOMETHING ELSE. `09:00` is a sexagesimal number
|
|
24
|
+
* in YAML 1.1 and `on`/`no` are booleans; an unquoted value that looks like either
|
|
25
|
+
* reaches GitHub as the wrong type, and the symptom is a schedule that never fires
|
|
26
|
+
* rather than an error.
|
|
27
|
+
*/
|
|
28
|
+
const PLAIN = /^[A-Za-z0-9_./][A-Za-z0-9_ ./@:+-]*$/;
|
|
29
|
+
const YAML_KEYWORD =
|
|
30
|
+
/^(y|Y|n|N|on|On|ON|no|No|NO|yes|Yes|YES|true|True|TRUE|false|False|FALSE|null|Null|NULL|~)$/;
|
|
31
|
+
|
|
32
|
+
function scalar(value: string | number): string {
|
|
33
|
+
if (typeof value === 'number') return String(value);
|
|
34
|
+
if (value === '') return "''";
|
|
35
|
+
const plain =
|
|
36
|
+
PLAIN.test(value) &&
|
|
37
|
+
!YAML_KEYWORD.test(value) &&
|
|
38
|
+
!value.includes(': ') &&
|
|
39
|
+
!value.includes(' #') &&
|
|
40
|
+
// ⚠️ A LEADING DIGIT PLUS A COLON IS A YAML 1.1 SEXAGESIMAL NUMBER, not a string:
|
|
41
|
+
// `09:00` would reach GitHub as an integer, and a schedule set to an integer
|
|
42
|
+
// simply never fires. `bun run build:web` is safe because it does not start with
|
|
43
|
+
// a digit, which is why this is narrower than "contains a colon".
|
|
44
|
+
!/^\d.*:/.test(value) &&
|
|
45
|
+
value.trimEnd() === value;
|
|
46
|
+
return plain ? value : `'${value.replaceAll("'", "''")}'`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderMapping(
|
|
50
|
+
entries: Readonly<Record<string, string | number>>,
|
|
51
|
+
depth: number,
|
|
52
|
+
): string[] {
|
|
53
|
+
return Object.entries(entries).map(([key, value]) => `${indent(depth)}${key}: ${scalar(value)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* ★ BLOCK SCALAR FOR EVERY MULTI-LINE `run:`. A folded or quoted form would join the
|
|
58
|
+
* lines, and a shell script whose `if` and `then` end up on one line is a syntax error
|
|
59
|
+
* at job time rather than at lint time. `|` keeps them exactly as written.
|
|
60
|
+
*/
|
|
61
|
+
function renderRun(command: string, depth: number): string[] {
|
|
62
|
+
const lines = command.replace(/\n+$/, '').split('\n');
|
|
63
|
+
if (lines.length === 1) return [`${indent(depth)}run: ${scalar(lines[0] ?? '')}`];
|
|
64
|
+
return [`${indent(depth)}run: |`, ...lines.map((line) => `${indent(depth + 1)}${line}`)];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One step, as the lines of a `steps:` list item at `depth`. */
|
|
68
|
+
export function renderStep(step: JobStep, depth: number): string[] {
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
const body =
|
|
71
|
+
step.uses === undefined
|
|
72
|
+
? renderRun(step.run ?? '', depth + 1)
|
|
73
|
+
: [`${indent(depth + 1)}uses: ${step.uses}`];
|
|
74
|
+
|
|
75
|
+
// ★ `name:` then `if:` then the body, because that is the order a reader scans: what
|
|
76
|
+
// this step is, whether it runs, what it does. All three are ordinary mapping keys
|
|
77
|
+
// to GitHub, so the order is for the person reading the diff, not the parser.
|
|
78
|
+
const head: string[] = [];
|
|
79
|
+
if (step.name !== undefined) head.push(`${indent(depth + 1)}name: ${scalar(step.name)}`);
|
|
80
|
+
if (step.if !== undefined) head.push(`${indent(depth + 1)}if: ${scalar(step.if)}`);
|
|
81
|
+
|
|
82
|
+
// ⚠️ The first line of the item carries the dash, whichever key it turns out to be —
|
|
83
|
+
// an unnamed, unconditional step still inlines its `run:` or `uses:` after the dash
|
|
84
|
+
// and lets any block body follow at its own indent.
|
|
85
|
+
const [first = '', ...rest] = [...head, ...body];
|
|
86
|
+
lines.push(`${indent(depth)}- ${first.trimStart()}`, ...rest);
|
|
87
|
+
|
|
88
|
+
if (step.with !== undefined && Object.keys(step.with).length > 0) {
|
|
89
|
+
lines.push(`${indent(depth + 1)}with:`, ...renderMapping(step.with, depth + 2));
|
|
90
|
+
}
|
|
91
|
+
if (step.env !== undefined && Object.keys(step.env).length > 0) {
|
|
92
|
+
lines.push(`${indent(depth + 1)}env:`, ...renderMapping(step.env, depth + 2));
|
|
93
|
+
}
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A whole `steps:` list, already indented for a job at `depth`. */
|
|
98
|
+
export function renderSteps(steps: readonly JobStep[], depth: number): string {
|
|
99
|
+
return steps.flatMap((step) => renderStep(step, depth)).join('\n');
|
|
100
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@homeflare/config/repo-shape` — the standard HomeFlare repository, as one declaration.
|
|
3
|
+
*
|
|
4
|
+
* A repository keeps a `repo-shape.ts` at its root:
|
|
5
|
+
*
|
|
6
|
+
* import { except, repoShapeCli, type RepoShape } from '@homeflare/config/repo-shape';
|
|
7
|
+
*
|
|
8
|
+
* export const shape: RepoShape = {
|
|
9
|
+
* owner: 'taslabs-net',
|
|
10
|
+
* repository: 'homeflare-proxmox',
|
|
11
|
+
* runner: 'mini',
|
|
12
|
+
* publishes: false,
|
|
13
|
+
* };
|
|
14
|
+
*
|
|
15
|
+
* if (import.meta.main) process.exit(await repoShapeCli(import.meta.dir, shape, Bun.argv.slice(2)));
|
|
16
|
+
*
|
|
17
|
+
* That one file gives the repository:
|
|
18
|
+
*
|
|
19
|
+
* · its FILES — `bun run repo-shape:refresh` writes ci.yml, security.yml,
|
|
20
|
+
* actionlint.yaml, dependabot.yml and the changeset config;
|
|
21
|
+
* · its DRIFT GATE — a `bun:test` calling `driftInRepoShape` fails on a hand edit;
|
|
22
|
+
* · its SETTINGS — `renderRepoShape(shape).policy` is the options object
|
|
23
|
+
* `@homeflare/alchemy`'s `declareRepoPolicy` takes, so the ruleset
|
|
24
|
+
* requires exactly the checks these workflows report.
|
|
25
|
+
*
|
|
26
|
+
* ⛔ A DEVIATION IS DECLARED OR IT FAILS. `except({ file, reason, since })` refuses an
|
|
27
|
+
* empty reason and refuses a reason read from a variable — the text has to be written
|
|
28
|
+
* at the exception, in the diff, where someone will read it. Same for `extraJob()`.
|
|
29
|
+
*
|
|
30
|
+
* ★ WHY IT LIVES IN `@homeflare/config` RATHER THAN `@homeflare/alchemy`. Measured
|
|
31
|
+
* 2026-09-22: `@homeflare/config` is a dependency of 13 of 14 estate repositories and
|
|
32
|
+
* `@homeflare/alchemy` of 4. The drift check has to run in every repository's own CI,
|
|
33
|
+
* including the ones with no Alchemy stack, so it belongs to the package they all
|
|
34
|
+
* already have — and it takes no dependency on Alchemy or Effect to get there.
|
|
35
|
+
*/
|
|
36
|
+
export { renderCi, ACTIONLINT_VERSION, BUN_VERSION } from './repo-shape/ci.ts';
|
|
37
|
+
export {
|
|
38
|
+
renderActionlintConfig,
|
|
39
|
+
renderChangesetConfig,
|
|
40
|
+
renderDependabot,
|
|
41
|
+
} from './repo-shape/companions.ts';
|
|
42
|
+
export {
|
|
43
|
+
type DriftReport,
|
|
44
|
+
type Problem,
|
|
45
|
+
driftInRepoShape,
|
|
46
|
+
exceptionSummary,
|
|
47
|
+
REFRESH_COMMAND,
|
|
48
|
+
} from './repo-shape/drift.ts';
|
|
49
|
+
export { type RefreshResult, refreshRepoShape, repoShapeCli } from './repo-shape/refresh.ts';
|
|
50
|
+
export {
|
|
51
|
+
type RenderedRepo,
|
|
52
|
+
type RepoShapePolicy,
|
|
53
|
+
RENDERED_PATHS,
|
|
54
|
+
renderRepoShape,
|
|
55
|
+
} from './repo-shape/render.ts';
|
|
56
|
+
export { renderSecurity } from './repo-shape/security.ts';
|
|
57
|
+
export {
|
|
58
|
+
type ExtraJob,
|
|
59
|
+
type JobStep,
|
|
60
|
+
type RenderedPath,
|
|
61
|
+
type RepoRunner,
|
|
62
|
+
type RepoShape,
|
|
63
|
+
type RepoShapeException,
|
|
64
|
+
type Stated,
|
|
65
|
+
except,
|
|
66
|
+
extraJob,
|
|
67
|
+
isExcepted,
|
|
68
|
+
runsOn,
|
|
69
|
+
} from './repo-shape/shape.ts';
|