@acme-skunkworks/eslint-config 1.0.3 → 1.0.4
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/dist/infrastructure/scripts/add-links-changelog.d.ts +12 -0
- package/dist/infrastructure/scripts/add-links-changelog.d.ts.map +1 -0
- package/dist/infrastructure/scripts/add-links-changelog.js +56 -0
- package/dist/infrastructure/scripts/enrich-changelog.d.ts +26 -0
- package/dist/infrastructure/scripts/enrich-changelog.d.ts.map +1 -0
- package/dist/infrastructure/scripts/enrich-changelog.js +60 -0
- package/dist/infrastructure/scripts/finalise-changelog.d.ts +26 -0
- package/dist/infrastructure/scripts/finalise-changelog.d.ts.map +1 -0
- package/dist/infrastructure/scripts/finalise-changelog.js +152 -0
- package/dist/infrastructure/scripts/stamp-changelog-version.d.ts +10 -0
- package/dist/infrastructure/scripts/stamp-changelog-version.d.ts.map +1 -0
- package/dist/infrastructure/scripts/stamp-changelog-version.js +35 -0
- package/dist/infrastructure/scripts/validate-changelog.d.ts +7 -0
- package/dist/infrastructure/scripts/validate-changelog.d.ts.map +1 -0
- package/dist/infrastructure/scripts/validate-changelog.js +216 -0
- package/dist/rules/sanity.d.ts +1 -1
- package/dist/rules/sanity.js +7 -7
- package/package.json +11 -9
- package/dist/infrastructure/scripts/retitle-release-pr.d.ts +0 -11
- package/dist/infrastructure/scripts/retitle-release-pr.d.ts.map +0 -1
- package/dist/infrastructure/scripts/retitle-release-pr.js +0 -50
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite bare Linear IDs in a markdown body to links, masking code/links.
|
|
3
|
+
*/
|
|
4
|
+
export declare function rewriteBody(body: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Split leading YAML frontmatter from the body, preserving the fence bytes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function splitFrontmatter(raw: string): {
|
|
9
|
+
body: string;
|
|
10
|
+
fm: string;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=add-links-changelog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-links-changelog.d.ts","sourceRoot":"","sources":["../../../infrastructure/scripts/add-links-changelog.ts"],"names":[],"mappings":"AAwBA;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBhD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAe1E"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Pure helper: rewrite bare Linear issue IDs (e.g. ASW-123) in changelog entry
|
|
2
|
+
// bodies into markdown links, masking code fences / inline code / already-linked
|
|
3
|
+
// IDs first so they're left untouched. Ported from octavo's add-links.mjs,
|
|
4
|
+
// adapted to this workspace (acme-skunkworks).
|
|
5
|
+
//
|
|
6
|
+
// Library module (no CLI): the release-time orchestrator finalise-changelog.ts
|
|
7
|
+
// applies it. Kept pure so it's trivially unit-testable.
|
|
8
|
+
const WORKSPACE = "acme-skunkworks";
|
|
9
|
+
const TEAM_KEYS = ["ASW", "AKW"];
|
|
10
|
+
const ISSUE_RE = new RegExp(`\\b(?:${TEAM_KEYS.join("|")})-\\d+\\b`, "g");
|
|
11
|
+
const FENCE_RE = /```[\s\S]*?```/g;
|
|
12
|
+
const INLINE_CODE_RE = /`[^`]*`/g;
|
|
13
|
+
const ALREADY_LINKED_RE = /\[[^\]]*\]\([^)]*\)/g;
|
|
14
|
+
// Private-Use-Area sentinel wrapping each masked span, so a placeholder can
|
|
15
|
+
// never collide with literal body text (e.g. a code sample containing the
|
|
16
|
+
// string "FENCE0"). U+E000 cannot appear in a markdown source file.
|
|
17
|
+
const SENTINEL = "\u{E000}";
|
|
18
|
+
const PLACEHOLDER_RE = /\u{E000}(?:FENCE|INLINE|LINK)(\d+)\u{E000}/gu;
|
|
19
|
+
function buildUrl(id) {
|
|
20
|
+
return `https://linear.app/${WORKSPACE}/issue/${id}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite bare Linear IDs in a markdown body to links, masking code/links.
|
|
24
|
+
*/
|
|
25
|
+
export function rewriteBody(body) {
|
|
26
|
+
const masks = [];
|
|
27
|
+
function mask(label) {
|
|
28
|
+
return (matched) => {
|
|
29
|
+
masks.push(matched);
|
|
30
|
+
return `${SENTINEL}${label}${masks.length - 1}${SENTINEL}`;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const masked = body
|
|
34
|
+
.replaceAll(FENCE_RE, mask("FENCE"))
|
|
35
|
+
.replaceAll(INLINE_CODE_RE, mask("INLINE"))
|
|
36
|
+
.replaceAll(ALREADY_LINKED_RE, mask("LINK"))
|
|
37
|
+
.replaceAll(ISSUE_RE, (id) => `[${id}](${buildUrl(id)})`);
|
|
38
|
+
return masked.replaceAll(PLACEHOLDER_RE, (_, index) => masks[Number(index)]);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Split leading YAML frontmatter from the body, preserving the fence bytes.
|
|
42
|
+
*/
|
|
43
|
+
export function splitFrontmatter(raw) {
|
|
44
|
+
if (!raw.startsWith("---\n")) {
|
|
45
|
+
return { body: raw, fm: "" };
|
|
46
|
+
}
|
|
47
|
+
// Search from index 3: the opening "---\n" is exactly 4 bytes, so the
|
|
48
|
+
// earliest a closing "\n---\n" can start is index 3 (the newline ending the
|
|
49
|
+
// opening fence). Starting at 4 would miss the close of an empty frontmatter
|
|
50
|
+
// ("---\n---\n").
|
|
51
|
+
const end = raw.indexOf("\n---\n", 3);
|
|
52
|
+
if (end === -1) {
|
|
53
|
+
return { body: raw, fm: "" };
|
|
54
|
+
}
|
|
55
|
+
return { body: raw.slice(end + 5), fm: raw.slice(0, end + 5) };
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type EnrichInput = {
|
|
2
|
+
additions?: null | string;
|
|
3
|
+
/**
|
|
4
|
+
* Feature branch name — the stable lookup key.
|
|
5
|
+
*/
|
|
6
|
+
branch: string;
|
|
7
|
+
changedFiles?: null | string;
|
|
8
|
+
deletions?: null | string;
|
|
9
|
+
/**
|
|
10
|
+
* PR merged_at timestamp (ISO 8601 UTC).
|
|
11
|
+
*/
|
|
12
|
+
mergedAt: string;
|
|
13
|
+
/**
|
|
14
|
+
* Merge commit SHA (full or short); only the first 7 chars are stored.
|
|
15
|
+
*/
|
|
16
|
+
mergeSha: string;
|
|
17
|
+
mergeStrategy?: null | string;
|
|
18
|
+
prNumber?: null | string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Apply enrichment to a single entry's raw markdown and return the rewritten
|
|
22
|
+
* markdown. Fill-once for merged_at/commit/merge_strategy/pr; authoritative
|
|
23
|
+
* overwrite for stats. created_at is never touched.
|
|
24
|
+
*/
|
|
25
|
+
export declare function enrichFrontmatter(raw: string, input: EnrichInput): string;
|
|
26
|
+
//# sourceMappingURL=enrich-changelog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrich-changelog.d.ts","sourceRoot":"","sources":["../../../infrastructure/scripts/enrich-changelog.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC1B;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC1B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CAC1B,CAAC;AASF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAkDzE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Pure enrichment of a changelog entry's frontmatter — fills the fields that
|
|
2
|
+
// are only knowable once the PR has merged (merged_at / commit / merge_strategy
|
|
3
|
+
// / pr) plus authoritative stats. `version` is filled separately by
|
|
4
|
+
// stamp-changelog-version. created_at is never touched.
|
|
5
|
+
//
|
|
6
|
+
// This is a library module (no CLI): the release-time orchestrator
|
|
7
|
+
// finalise-changelog.ts composes it with the PR data it resolves from `gh`.
|
|
8
|
+
// Ported from octavo's enrich-changelog.mjs, minus affected_packages (single
|
|
9
|
+
// package). Kept pure so it's trivially unit-testable.
|
|
10
|
+
import matter from "gray-matter";
|
|
11
|
+
/**
|
|
12
|
+
* True when a value is unset (null/undefined/"").
|
|
13
|
+
*/
|
|
14
|
+
function blank(value) {
|
|
15
|
+
return value === null || value === undefined || value === "";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Apply enrichment to a single entry's raw markdown and return the rewritten
|
|
19
|
+
* markdown. Fill-once for merged_at/commit/merge_strategy/pr; authoritative
|
|
20
|
+
* overwrite for stats. created_at is never touched.
|
|
21
|
+
*/
|
|
22
|
+
export function enrichFrontmatter(raw, input) {
|
|
23
|
+
const parsed = matter(raw);
|
|
24
|
+
const fm = { ...parsed.data };
|
|
25
|
+
if (!fm.created_at) {
|
|
26
|
+
throw new Error("entry has no created_at; refusing to enrich");
|
|
27
|
+
}
|
|
28
|
+
const shortSha = input.mergeSha.slice(0, 7);
|
|
29
|
+
if (blank(fm.merged_at)) {
|
|
30
|
+
fm.merged_at = input.mergedAt;
|
|
31
|
+
}
|
|
32
|
+
if (blank(fm.commit)) {
|
|
33
|
+
fm.commit = shortSha;
|
|
34
|
+
}
|
|
35
|
+
if (blank(fm.merge_strategy) && input.mergeStrategy) {
|
|
36
|
+
fm.merge_strategy = input.mergeStrategy;
|
|
37
|
+
}
|
|
38
|
+
if (blank(fm.pr) && input.prNumber) {
|
|
39
|
+
fm.pr = Number.parseInt(input.prNumber, 10);
|
|
40
|
+
}
|
|
41
|
+
// Authoritative overwrites from the GH API, always under stats: { ... }.
|
|
42
|
+
const stats = typeof fm.stats === "object" &&
|
|
43
|
+
fm.stats !== null &&
|
|
44
|
+
!Array.isArray(fm.stats)
|
|
45
|
+
? { ...fm.stats }
|
|
46
|
+
: {};
|
|
47
|
+
// Guard with blank() (not just null/undefined): an empty string would slip
|
|
48
|
+
// through and Number.parseInt("", 10) is NaN, which the validator rejects.
|
|
49
|
+
if (!blank(input.additions)) {
|
|
50
|
+
stats.loc_added = Number.parseInt(input.additions, 10);
|
|
51
|
+
}
|
|
52
|
+
if (!blank(input.deletions)) {
|
|
53
|
+
stats.loc_removed = Number.parseInt(input.deletions, 10);
|
|
54
|
+
}
|
|
55
|
+
if (!blank(input.changedFiles)) {
|
|
56
|
+
stats.files_changed = Number.parseInt(input.changedFiles, 10);
|
|
57
|
+
}
|
|
58
|
+
fm.stats = stats;
|
|
59
|
+
return matter.stringify(parsed.content, fm);
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
export declare const CHANGELOG_DIR = "changelog";
|
|
3
|
+
export type ResolvedPr = {
|
|
4
|
+
additions: null | string;
|
|
5
|
+
changedFiles: null | string;
|
|
6
|
+
deletions: null | string;
|
|
7
|
+
mergedAt: string;
|
|
8
|
+
mergeSha: string;
|
|
9
|
+
mergeStrategy: null | string;
|
|
10
|
+
prNumber: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the merged PR for a branch, or null when none is found.
|
|
14
|
+
*/
|
|
15
|
+
export type PrResolver = (branch: string) => null | ResolvedPr;
|
|
16
|
+
export type Runner = (cmd: string, args: readonly string[]) => string;
|
|
17
|
+
/**
|
|
18
|
+
* Finalise one entry's raw markdown for release. Returns the rewritten markdown,
|
|
19
|
+
* or null when nothing changed (already finalised).
|
|
20
|
+
*/
|
|
21
|
+
export declare function finaliseEntry(raw: string, version: string, resolvePr: PrResolver): null | string;
|
|
22
|
+
/**
|
|
23
|
+
* Build a PR resolver backed by `gh` + `git` (injectable runner for tests).
|
|
24
|
+
*/
|
|
25
|
+
export declare function makeResolver(run: Runner): PrResolver;
|
|
26
|
+
//# sourceMappingURL=finalise-changelog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finalise-changelog.d.ts","sourceRoot":"","sources":["../../../infrastructure/scripts/finalise-changelog.ts"],"names":[],"mappings":";AAuBA,eAAO,MAAM,aAAa,cAAc,CAAC;AAEzC,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,YAAY,EAAE,IAAI,GAAG,MAAM,CAAC;IAC5B,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,IAAI,GAAG,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,UAAU,CAAC;AAE/D,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAMtE;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,UAAU,GACpB,IAAI,GAAG,MAAM,CAgCf;AAaD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CA4EpD"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
// Release-time finalisation of changelog entries — run by changesets/action's
|
|
3
|
+
// `version:` command, right after `changeset version`, so the result is
|
|
4
|
+
// committed into the "release: version packages" PR (no separate workflow, no
|
|
5
|
+
// bot push to main).
|
|
6
|
+
//
|
|
7
|
+
// For every entry that isn't finalised yet (empty `version`):
|
|
8
|
+
// 1. resolve its merged PR from the `branch` field via `gh` and enrich
|
|
9
|
+
// (merged_at / commit / pr / merge_strategy / stats);
|
|
10
|
+
// 2. stamp `version` with the just-bumped package.json version;
|
|
11
|
+
// 3. rewrite bare Linear IDs to links.
|
|
12
|
+
//
|
|
13
|
+
// The pure `finaliseEntry(raw, version, resolvePr)` is unit-testable with a fake
|
|
14
|
+
// resolver; main() wires the real `gh`/`git` resolver and walks the directory.
|
|
15
|
+
import { rewriteBody, splitFrontmatter } from "./add-links-changelog.js";
|
|
16
|
+
import { enrichFrontmatter } from "./enrich-changelog.js";
|
|
17
|
+
import { readPackageVersion, stampVersion } from "./stamp-changelog-version.js";
|
|
18
|
+
import matter from "gray-matter";
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
export const CHANGELOG_DIR = "changelog";
|
|
23
|
+
function blank(value) {
|
|
24
|
+
return value === null || value === undefined || value === "";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Finalise one entry's raw markdown for release. Returns the rewritten markdown,
|
|
28
|
+
* or null when nothing changed (already finalised).
|
|
29
|
+
*/
|
|
30
|
+
export function finaliseEntry(raw, version, resolvePr) {
|
|
31
|
+
const fm = matter(raw).data;
|
|
32
|
+
if (!blank(fm.version)) {
|
|
33
|
+
return null; // already shipped in a release
|
|
34
|
+
}
|
|
35
|
+
let next = raw;
|
|
36
|
+
const branch = typeof fm.branch === "string" ? fm.branch : "";
|
|
37
|
+
const needsEnrich = blank(fm.merged_at) || blank(fm.commit) || blank(fm.pr);
|
|
38
|
+
if (branch && needsEnrich) {
|
|
39
|
+
const pr = resolvePr(branch);
|
|
40
|
+
if (pr) {
|
|
41
|
+
next = enrichFrontmatter(next, {
|
|
42
|
+
additions: pr.additions,
|
|
43
|
+
branch,
|
|
44
|
+
changedFiles: pr.changedFiles,
|
|
45
|
+
deletions: pr.deletions,
|
|
46
|
+
mergedAt: pr.mergedAt,
|
|
47
|
+
mergeSha: pr.mergeSha,
|
|
48
|
+
mergeStrategy: pr.mergeStrategy,
|
|
49
|
+
prNumber: pr.prNumber,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
next = stampVersion(next, version) ?? next;
|
|
54
|
+
const { body, fm: fmText } = splitFrontmatter(next);
|
|
55
|
+
next = fmText + rewriteBody(body);
|
|
56
|
+
return next === raw ? null : next;
|
|
57
|
+
}
|
|
58
|
+
function realRunner(cmd, args) {
|
|
59
|
+
return execFileSync(cmd, args, {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
62
|
+
// Fail fast if gh/git stalls (network/auth). Enrichment is best-effort, so
|
|
63
|
+
// a timeout throws → makeResolver's try/catch falls back to null rather
|
|
64
|
+
// than hanging the release until the whole job times out.
|
|
65
|
+
timeout: 30_000,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a PR resolver backed by `gh` + `git` (injectable runner for tests).
|
|
70
|
+
*/
|
|
71
|
+
export function makeResolver(run) {
|
|
72
|
+
function resolve(branch) {
|
|
73
|
+
const json = run("gh", [
|
|
74
|
+
"pr",
|
|
75
|
+
"list",
|
|
76
|
+
"--head",
|
|
77
|
+
branch,
|
|
78
|
+
"--state",
|
|
79
|
+
"merged",
|
|
80
|
+
"--limit",
|
|
81
|
+
"1",
|
|
82
|
+
"--json",
|
|
83
|
+
"number,mergedAt,additions,deletions,changedFiles,mergeCommit,headRefOid",
|
|
84
|
+
]);
|
|
85
|
+
const list = JSON.parse(json);
|
|
86
|
+
if (list.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const pr = list[0];
|
|
90
|
+
const mergeSha = pr.mergeCommit?.oid ?? "";
|
|
91
|
+
// Infer merge strategy from the merge commit shape (GitHub doesn't expose
|
|
92
|
+
// it directly): 2+ parents -> merge; otherwise squash.
|
|
93
|
+
// NOTE: rebase merges are also reported as "squash" — GitHub replays them
|
|
94
|
+
// with fresh SHAs, so mergeCommit.oid never equals headRefOid and the
|
|
95
|
+
// "rebase" branch below is effectively unreachable. This repo squash-merges
|
|
96
|
+
// anyway, and merge_strategy is only record-keeping metadata, so the
|
|
97
|
+
// imprecision is harmless.
|
|
98
|
+
let mergeStrategy = null;
|
|
99
|
+
if (mergeSha) {
|
|
100
|
+
const parents = (run("git", ["cat-file", "-p", mergeSha]).match(/^parent /gm) ?? []).length;
|
|
101
|
+
if (parents >= 2) {
|
|
102
|
+
mergeStrategy = "merge";
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
mergeStrategy = mergeSha === pr.headRefOid ? "rebase" : "squash";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Absent numeric fields stay null (not ""), so the enrich guard skips them
|
|
109
|
+
// rather than parsing "" into NaN.
|
|
110
|
+
return {
|
|
111
|
+
additions: pr.additions === undefined ? null : String(pr.additions),
|
|
112
|
+
changedFiles: pr.changedFiles === undefined ? null : String(pr.changedFiles),
|
|
113
|
+
deletions: pr.deletions === undefined ? null : String(pr.deletions),
|
|
114
|
+
mergedAt: pr.mergedAt ?? "",
|
|
115
|
+
mergeSha,
|
|
116
|
+
mergeStrategy,
|
|
117
|
+
prNumber: String(pr.number ?? ""),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return (branch) => {
|
|
121
|
+
// Enrichment is best-effort metadata: a gh/git failure here must NOT abort
|
|
122
|
+
// `changeset version` and block the release. On any error, warn and return
|
|
123
|
+
// null — the entry still gets version-stamped, just without PR metadata.
|
|
124
|
+
try {
|
|
125
|
+
return resolve(branch);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.warn(`⚠️ Could not resolve PR for branch ${branch}: ${error.message}`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function main() {
|
|
134
|
+
const version = readPackageVersion(readFileSync("package.json", "utf8"));
|
|
135
|
+
const resolvePr = makeResolver(realRunner);
|
|
136
|
+
const files = readdirSync(CHANGELOG_DIR)
|
|
137
|
+
.filter((name) => name.endsWith(".md") && name !== "README.md")
|
|
138
|
+
.map((name) => join(CHANGELOG_DIR, name));
|
|
139
|
+
let finalised = 0;
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const next = finaliseEntry(readFileSync(file, "utf8"), version, resolvePr);
|
|
142
|
+
if (next !== null) {
|
|
143
|
+
writeFileSync(file, next);
|
|
144
|
+
finalised++;
|
|
145
|
+
console.log(`finalised ${version}: ${file}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
console.log(`Changelog finalisation complete. ${finalised} entr${finalised === 1 ? "y" : "ies"} finalised with ${version}.`);
|
|
149
|
+
}
|
|
150
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
151
|
+
main();
|
|
152
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamp `version` onto an entry if it has none. Returns the rewritten markdown,
|
|
3
|
+
* or null when the entry already has a version (no write needed).
|
|
4
|
+
*/
|
|
5
|
+
export declare function stampVersion(raw: string, version: string): null | string;
|
|
6
|
+
/**
|
|
7
|
+
* Read the `version` field from a package.json string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readPackageVersion(packageJsonRaw: string): string;
|
|
10
|
+
//# sourceMappingURL=stamp-changelog-version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stamp-changelog-version.d.ts","sourceRoot":"","sources":["../../../infrastructure/scripts/stamp-changelog-version.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CASxE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAOjE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Pure helpers for release-time version stamping: set `version` on an entry
|
|
2
|
+
// that doesn't have one, and read the version from package.json.
|
|
3
|
+
//
|
|
4
|
+
// Library module (no CLI): the release-time orchestrator finalise-changelog.ts
|
|
5
|
+
// composes these. Kept pure so they're trivially unit-testable.
|
|
6
|
+
import matter from "gray-matter";
|
|
7
|
+
/**
|
|
8
|
+
* True when a value is unset (null/undefined/"").
|
|
9
|
+
*/
|
|
10
|
+
function blank(value) {
|
|
11
|
+
return value === null || value === undefined || value === "";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Stamp `version` onto an entry if it has none. Returns the rewritten markdown,
|
|
15
|
+
* or null when the entry already has a version (no write needed).
|
|
16
|
+
*/
|
|
17
|
+
export function stampVersion(raw, version) {
|
|
18
|
+
const parsed = matter(raw);
|
|
19
|
+
const fm = { ...parsed.data };
|
|
20
|
+
if (!blank(fm.version)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
fm.version = version;
|
|
24
|
+
return matter.stringify(parsed.content, fm);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read the `version` field from a package.json string.
|
|
28
|
+
*/
|
|
29
|
+
export function readPackageVersion(packageJsonRaw) {
|
|
30
|
+
const pkg = JSON.parse(packageJsonRaw);
|
|
31
|
+
if (typeof pkg.version !== "string" || pkg.version.length === 0) {
|
|
32
|
+
throw new Error("package.json is missing a string `version`");
|
|
33
|
+
}
|
|
34
|
+
return pkg.version;
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
export declare const CHANGELOG_DIR = "changelog";
|
|
3
|
+
/**
|
|
4
|
+
* Validate one entry. Returns an array of human-readable error strings.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateEntry(name: string, raw: string): string[];
|
|
7
|
+
//# sourceMappingURL=validate-changelog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-changelog.d.ts","sourceRoot":"","sources":["../../../infrastructure/scripts/validate-changelog.ts"],"names":[],"mappings":";AAoBA,eAAO,MAAM,aAAa,cAAc,CAAC;AA0DzC;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CA0JjE"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
// Validates the individual dated changelog entries under `changelog/`.
|
|
3
|
+
//
|
|
4
|
+
// Ported from octavo's scripts/validate-changelog.mjs and adapted for this
|
|
5
|
+
// repo (single, semver'd npm package):
|
|
6
|
+
// - `version` is accepted (typed-when-present semver string); octavo has none.
|
|
7
|
+
// - `affected_packages` is dropped (one package, not a monorepo).
|
|
8
|
+
// - the REQUIRED set is relaxed to title/created_at/category/breaking so that
|
|
9
|
+
// both backfilled historical entries (no branch/author/stats) and in-flight
|
|
10
|
+
// entries (no version/merged_at/pr/commit/stats until enriched) validate.
|
|
11
|
+
// /send-it is the guarantee that new entries get branch/author/co_authors;
|
|
12
|
+
// validation is the safety net, not the sole guard.
|
|
13
|
+
//
|
|
14
|
+
// The pure `validateEntry(name, raw)` returns an array of error strings (empty
|
|
15
|
+
// means valid), so it's trivially unit-testable; main() walks the directory.
|
|
16
|
+
import matter from "gray-matter";
|
|
17
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
|
+
export const CHANGELOG_DIR = "changelog";
|
|
20
|
+
const FILENAME_RE = /^(\d{8})-(\d{6})-([a-z0-9-]+)\.md$/;
|
|
21
|
+
const ISO_UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
22
|
+
// SemVer 2.0.0: prerelease and build identifiers are dot-separated and may
|
|
23
|
+
// contain ASCII alphanumerics and hyphens (e.g. 1.2.3-rc-1, 1.2.3+build-45).
|
|
24
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
25
|
+
const SHA7_RE = /^[0-9a-f]{7}$/;
|
|
26
|
+
const ISSUE_RE = /^[A-Z]{2,}-\d+$/;
|
|
27
|
+
const CATEGORIES = new Set([
|
|
28
|
+
"chore",
|
|
29
|
+
"docs",
|
|
30
|
+
"feature",
|
|
31
|
+
"fix",
|
|
32
|
+
"perf",
|
|
33
|
+
"refactor",
|
|
34
|
+
]);
|
|
35
|
+
const MERGE_STRATEGIES = new Set(["merge", "rebase", "squash"]);
|
|
36
|
+
const SECTION_RE = /^##\s+(Breaking|Added|Changed|Fixed)\b/m;
|
|
37
|
+
const REQUIRED = ["title", "created_at", "category", "breaking"];
|
|
38
|
+
/**
|
|
39
|
+
* True when a value is set to something meaningful (not null/undefined/"").
|
|
40
|
+
*/
|
|
41
|
+
function present(value) {
|
|
42
|
+
return value !== null && value !== undefined && value !== "";
|
|
43
|
+
}
|
|
44
|
+
function isInt(value) {
|
|
45
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
46
|
+
}
|
|
47
|
+
function isNonNegInt(value) {
|
|
48
|
+
return isInt(value) && value >= 0;
|
|
49
|
+
}
|
|
50
|
+
function isStringArray(value) {
|
|
51
|
+
return (Array.isArray(value) && value.every((item) => typeof item === "string"));
|
|
52
|
+
}
|
|
53
|
+
function asIso(value) {
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (value instanceof Date) {
|
|
58
|
+
return value.toISOString();
|
|
59
|
+
}
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate one entry. Returns an array of human-readable error strings.
|
|
64
|
+
*/
|
|
65
|
+
export function validateEntry(name, raw) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
function fail(message) {
|
|
68
|
+
errors.push(`${name}: ${message}`);
|
|
69
|
+
}
|
|
70
|
+
if (!FILENAME_RE.test(name)) {
|
|
71
|
+
fail("filename must match YYYYMMDD-HHMMSS-<slug>.md (slug: [a-z0-9-]+)");
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = matter(raw);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
fail(`frontmatter unparseable: ${error.message}`);
|
|
80
|
+
return errors;
|
|
81
|
+
}
|
|
82
|
+
const fm = (parsed.data ?? {});
|
|
83
|
+
const body = parsed.content ?? "";
|
|
84
|
+
for (const key of REQUIRED) {
|
|
85
|
+
if (!(key in fm)) {
|
|
86
|
+
fail(`missing required field: ${key}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if ("title" in fm &&
|
|
90
|
+
(typeof fm.title !== "string" || fm.title.trim() === "")) {
|
|
91
|
+
fail("title must be a non-empty string");
|
|
92
|
+
}
|
|
93
|
+
if ("release_note" in fm &&
|
|
94
|
+
fm.release_note !== null &&
|
|
95
|
+
typeof fm.release_note !== "string") {
|
|
96
|
+
fail("release_note must be a string or null when present");
|
|
97
|
+
}
|
|
98
|
+
if (present(fm.version) &&
|
|
99
|
+
(typeof fm.version !== "string" || !SEMVER_RE.test(fm.version))) {
|
|
100
|
+
fail(`version must be a semver string when set (got ${JSON.stringify(fm.version)})`);
|
|
101
|
+
}
|
|
102
|
+
if ("created_at" in fm && !ISO_UTC_RE.test(asIso(fm.created_at))) {
|
|
103
|
+
fail(`created_at must be ISO 8601 UTC with Z suffix (got ${JSON.stringify(fm.created_at)})`);
|
|
104
|
+
}
|
|
105
|
+
if (present(fm.merged_at) && !ISO_UTC_RE.test(asIso(fm.merged_at))) {
|
|
106
|
+
fail("merged_at must be ISO 8601 UTC with Z suffix when set");
|
|
107
|
+
}
|
|
108
|
+
if ("branch" in fm &&
|
|
109
|
+
(typeof fm.branch !== "string" || fm.branch.trim() === "")) {
|
|
110
|
+
fail("branch must be a non-empty string when present");
|
|
111
|
+
}
|
|
112
|
+
if (present(fm.pr) && !isInt(fm.pr)) {
|
|
113
|
+
fail("pr must be an integer when set");
|
|
114
|
+
}
|
|
115
|
+
if (present(fm.commit) && !SHA7_RE.test(String(fm.commit))) {
|
|
116
|
+
fail("commit must be a 7-char hex SHA when set");
|
|
117
|
+
}
|
|
118
|
+
if (present(fm.merge_strategy) &&
|
|
119
|
+
!MERGE_STRATEGIES.has(String(fm.merge_strategy))) {
|
|
120
|
+
fail(`merge_strategy must be one of: ${[...MERGE_STRATEGIES].join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
if ("author" in fm &&
|
|
123
|
+
(typeof fm.author !== "string" || fm.author.trim() === "")) {
|
|
124
|
+
fail("author must be a non-empty string when present");
|
|
125
|
+
}
|
|
126
|
+
if ("co_authors" in fm && !isStringArray(fm.co_authors)) {
|
|
127
|
+
fail("co_authors must be an array of strings (use [] when none)");
|
|
128
|
+
}
|
|
129
|
+
if ("category" in fm && !CATEGORIES.has(String(fm.category))) {
|
|
130
|
+
fail(`category must be one of: ${[...CATEGORIES].join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
if ("breaking" in fm && typeof fm.breaking !== "boolean") {
|
|
133
|
+
fail("breaking must be a boolean");
|
|
134
|
+
}
|
|
135
|
+
if ("issues" in fm) {
|
|
136
|
+
if (isStringArray(fm.issues)) {
|
|
137
|
+
for (const id of fm.issues) {
|
|
138
|
+
if (!ISSUE_RE.test(id)) {
|
|
139
|
+
fail(`issues entry ${JSON.stringify(id)} must match [A-Z]{2,}-\\d+`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
fail("issues must be an array of strings when present");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// PR stats live under stats: { files_changed, loc_added, loc_removed }.
|
|
148
|
+
const statKeys = ["files_changed", "loc_added", "loc_removed"];
|
|
149
|
+
for (const key of statKeys) {
|
|
150
|
+
if (key in fm) {
|
|
151
|
+
fail(`${key} must be under stats, not top-level`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// stats is optional (filled by enrichment), but must be a well-formed object
|
|
155
|
+
// with non-negative integer values when present.
|
|
156
|
+
if (present(fm.stats)) {
|
|
157
|
+
if (typeof fm.stats !== "object" || Array.isArray(fm.stats)) {
|
|
158
|
+
fail("stats must be an object");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const stats = fm.stats;
|
|
162
|
+
for (const key of statKeys) {
|
|
163
|
+
if (key in stats && present(stats[key]) && !isNonNegInt(stats[key])) {
|
|
164
|
+
fail(`stats.${key} must be a non-negative integer when set`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// The schema (changelog/README.md) requires "## Breaking" to be the FIRST
|
|
170
|
+
// body section when breaking: true — not merely present somewhere.
|
|
171
|
+
if (fm.breaking === true) {
|
|
172
|
+
const firstSection = body.match(/^##\s+([A-Za-z]+)\b/m)?.[1];
|
|
173
|
+
if (firstSection !== "Breaking") {
|
|
174
|
+
fail('breaking: true requires "## Breaking" as the first body section');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!SECTION_RE.test(body)) {
|
|
178
|
+
fail("body must contain at least one of: ## Breaking | ## Added | ## Changed | ## Fixed");
|
|
179
|
+
}
|
|
180
|
+
return errors;
|
|
181
|
+
}
|
|
182
|
+
function listEntries(directory) {
|
|
183
|
+
let stat;
|
|
184
|
+
try {
|
|
185
|
+
stat = statSync(directory);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
console.error(`changelog directory not found: ${directory}`);
|
|
189
|
+
process.exit(2);
|
|
190
|
+
}
|
|
191
|
+
if (!stat.isDirectory()) {
|
|
192
|
+
console.error(`${directory} is not a directory`);
|
|
193
|
+
process.exit(2);
|
|
194
|
+
}
|
|
195
|
+
return readdirSync(directory)
|
|
196
|
+
.filter((name) => name.endsWith(".md") && name !== "README.md")
|
|
197
|
+
.map((name) => join(directory, name));
|
|
198
|
+
}
|
|
199
|
+
function main() {
|
|
200
|
+
const files = listEntries(CHANGELOG_DIR);
|
|
201
|
+
const errors = [];
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
errors.push(...validateEntry(basename(file), readFileSync(file, "utf8")));
|
|
204
|
+
}
|
|
205
|
+
if (errors.length > 0) {
|
|
206
|
+
console.error(`Changelog validation failed with ${errors.length} error(s):\n`);
|
|
207
|
+
for (const message of errors) {
|
|
208
|
+
console.error(` - ${message}`);
|
|
209
|
+
}
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
console.log(`Changelog validation passed (${files.length} entr${files.length === 1 ? "y" : "ies"} checked).`);
|
|
213
|
+
}
|
|
214
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
215
|
+
main();
|
|
216
|
+
}
|
package/dist/rules/sanity.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Combined Sanity ESLint configuration. Exports an array of two configs
|
|
7
7
|
* documented inline above: schema property ordering for `*.schema.ts` (a
|
|
8
8
|
* perfectionist `sort-objects` rule with custom groups so identity → fields
|
|
9
|
-
* →
|
|
9
|
+
* → behaviour → validation appear in a deterministic, readable order) and
|
|
10
10
|
* structure-file exceptions (allows the `S => S.list()` arrow-function
|
|
11
11
|
* pattern and the canonical single-letter `S` parameter that Sanity's docs
|
|
12
12
|
* universally use for the StructureBuilder).
|
package/dist/rules/sanity.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Property ordering follows a logical grouping:
|
|
8
8
|
* 1. Identity: name, title, type, icon
|
|
9
9
|
* 2. Fields: fields (placed early so document content stays visually prominent)
|
|
10
|
-
* 3.
|
|
11
|
-
* 4.
|
|
10
|
+
* 3. Organisation: fieldset, group, groups, fieldsets
|
|
11
|
+
* 4. Behaviour: hidden, readOnly
|
|
12
12
|
* 5. Type-specific: options, rows, to, of, marks, styles
|
|
13
13
|
* 6. Content defaults: initialValue, description
|
|
14
14
|
* 7. Validation: validation
|
|
@@ -33,12 +33,12 @@ const sanitySchemaPropertyOrdering = {
|
|
|
33
33
|
{ elementNamePattern: "^icon$", groupName: "icon" },
|
|
34
34
|
// 2. Fields array (placed early so the schema's content stays visually prominent)
|
|
35
35
|
{ elementNamePattern: "^fields$", groupName: "fields" },
|
|
36
|
-
// 3.
|
|
36
|
+
// 3. Organisation - where does it go?
|
|
37
37
|
{ elementNamePattern: "^fieldset$", groupName: "fieldset" },
|
|
38
38
|
{ elementNamePattern: "^group$", groupName: "group" },
|
|
39
39
|
{ elementNamePattern: "^groups$", groupName: "groups" },
|
|
40
40
|
{ elementNamePattern: "^fieldsets$", groupName: "fieldsets" },
|
|
41
|
-
// 4.
|
|
41
|
+
// 4. Behaviour - how does it behave?
|
|
42
42
|
{ elementNamePattern: "^hidden$", groupName: "hidden" },
|
|
43
43
|
{ elementNamePattern: "^readOnly$", groupName: "readOnly" },
|
|
44
44
|
// 5. Type-specific options
|
|
@@ -65,12 +65,12 @@ const sanitySchemaPropertyOrdering = {
|
|
|
65
65
|
"icon",
|
|
66
66
|
// Fields
|
|
67
67
|
"fields",
|
|
68
|
-
//
|
|
68
|
+
// Organisation
|
|
69
69
|
"fieldset",
|
|
70
70
|
"group",
|
|
71
71
|
"groups",
|
|
72
72
|
"fieldsets",
|
|
73
|
-
//
|
|
73
|
+
// Behaviour
|
|
74
74
|
"hidden",
|
|
75
75
|
"readOnly",
|
|
76
76
|
// Type-specific
|
|
@@ -135,7 +135,7 @@ const sanityStructure = {
|
|
|
135
135
|
* Combined Sanity ESLint configuration. Exports an array of two configs
|
|
136
136
|
* documented inline above: schema property ordering for `*.schema.ts` (a
|
|
137
137
|
* perfectionist `sort-objects` rule with custom groups so identity → fields
|
|
138
|
-
* →
|
|
138
|
+
* → behaviour → validation appear in a deterministic, readable order) and
|
|
139
139
|
* structure-file exceptions (allows the `S => S.list()` arrow-function
|
|
140
140
|
* pattern and the canonical single-letter `S` parameter that Sanity's docs
|
|
141
141
|
* universally use for the StructureBuilder).
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acme-skunkworks/eslint-config",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Shared ESLint
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Shared ESLint flat-config preset with TypeScript, React, Astro, Sanity, and Storybook support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
7
7
|
"eslint-config",
|
|
8
8
|
"linting",
|
|
9
9
|
"typescript",
|
|
10
|
+
"react",
|
|
10
11
|
"astro",
|
|
12
|
+
"sanity",
|
|
13
|
+
"storybook",
|
|
11
14
|
"code-quality"
|
|
12
15
|
],
|
|
13
16
|
"homepage": "https://github.com/acme-skunkworks/eslint-config#readme",
|
|
@@ -42,6 +45,8 @@
|
|
|
42
45
|
"act:list": "act --list",
|
|
43
46
|
"act:release:dry": "act push -W .github/workflows/release.yml",
|
|
44
47
|
"build": "tsc",
|
|
48
|
+
"changelog:finalise": "tsx infrastructure/scripts/finalise-changelog.ts",
|
|
49
|
+
"changeset:version": "changeset version && pnpm changelog:finalise",
|
|
45
50
|
"ci:list": "gh run list --limit 10",
|
|
46
51
|
"ci:view": "gh run view",
|
|
47
52
|
"ci:watch": "gh run watch $(gh run list -L 1 --json databaseId -q '.[0].databaseId // empty')",
|
|
@@ -49,8 +54,8 @@
|
|
|
49
54
|
"format": "npx prettier --write .",
|
|
50
55
|
"lint": "eslint 'index.ts' 'rules/**/*.ts' 'infrastructure/scripts/**/*.ts' 'infrastructure/send-it/**/*.ts' --cache --cache-location ./.eslintcache",
|
|
51
56
|
"lint:fix": "eslint 'index.ts' 'rules/**/*.ts' 'infrastructure/scripts/**/*.ts' 'infrastructure/send-it/**/*.ts' --fix --cache --cache-location ./.eslintcache",
|
|
52
|
-
"lint:md": "markdownlint-cli2 '**/*.{md,mdx}' '!**/node_modules/**' '!**/dist/**' '!**/.turbo/**' '!**/.astro/**'",
|
|
53
|
-
"lint:md:fix": "markdownlint-cli2 --fix '**/*.{md,mdx}' '!**/node_modules/**' '!**/dist/**' '!**/.turbo/**' '!**/.astro/**'",
|
|
57
|
+
"lint:md": "markdownlint-cli2 '**/*.{md,mdx}' '!**/node_modules/**' '!**/dist/**' '!**/.turbo/**' '!**/.astro/**' '!**/CHANGELOG.md'",
|
|
58
|
+
"lint:md:fix": "markdownlint-cli2 --fix '**/*.{md,mdx}' '!**/node_modules/**' '!**/dist/**' '!**/.turbo/**' '!**/.astro/**' '!**/CHANGELOG.md'",
|
|
54
59
|
"lint:sh": "bash -c 'if command -v shellcheck >/dev/null 2>&1; then shellcheck infrastructure/scripts/*.sh .husky/pre-commit .husky/pre-push .husky/commit-msg; elif [ \"$(uname -s)\" = \"Darwin\" ]; then echo \"⚠️ shellcheck not installed — skipping. Install: brew install shellcheck\"; else echo \"⚠️ shellcheck not installed — skipping. Install: apt-get install shellcheck\"; fi'",
|
|
55
60
|
"lint:workflows": "actionlint",
|
|
56
61
|
"lint:yaml": "yamllint .",
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
"test:sh": "bash -c 'if command -v bats >/dev/null 2>&1; then bats infrastructure/tests/*.bats; elif [ \"$(uname -s)\" = \"Darwin\" ]; then echo \"⚠️ bats not installed — skipping. Install: brew install bats-core\"; else echo \"⚠️ bats not installed — skipping. Install: apt-get install bats\"; fi'",
|
|
65
70
|
"test:watch": "vitest",
|
|
66
71
|
"tsc": "tsc --noEmit",
|
|
72
|
+
"validate:changelog": "tsx infrastructure/scripts/validate-changelog.ts",
|
|
67
73
|
"version": "changeset version"
|
|
68
74
|
},
|
|
69
75
|
"lint-staged": {
|
|
@@ -111,6 +117,7 @@
|
|
|
111
117
|
"@changesets/cli": "^2.31.0",
|
|
112
118
|
"@types/eslint": "^9.6.1",
|
|
113
119
|
"@types/node": "^25.6.0",
|
|
120
|
+
"gray-matter": "^4.0.3",
|
|
114
121
|
"husky": "^9.1.7",
|
|
115
122
|
"lint-staged": "^16.3.2",
|
|
116
123
|
"markdownlint-cli2": "^0.18.1",
|
|
@@ -131,10 +138,5 @@
|
|
|
131
138
|
"publishConfig": {
|
|
132
139
|
"access": "public",
|
|
133
140
|
"provenance": true
|
|
134
|
-
},
|
|
135
|
-
"pnpm": {
|
|
136
|
-
"onlyBuiltDependencies": [
|
|
137
|
-
"unrs-resolver"
|
|
138
|
-
]
|
|
139
141
|
}
|
|
140
142
|
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S npx tsx
|
|
2
|
-
export type Runner = (cmd: string, args: readonly string[]) => string;
|
|
3
|
-
export type RetitleResult = {
|
|
4
|
-
reason: string;
|
|
5
|
-
status: "skipped";
|
|
6
|
-
} | {
|
|
7
|
-
status: "ok";
|
|
8
|
-
title: string;
|
|
9
|
-
};
|
|
10
|
-
export declare function retitleReleasePr(environment: Record<string, string | undefined>, run: Runner): RetitleResult;
|
|
11
|
-
//# sourceMappingURL=retitle-release-pr.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"retitle-release-pr.d.ts","sourceRoot":"","sources":["../../../infrastructure/scripts/retitle-release-pr.ts"],"names":[],"mappings":";AAgBA,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAEtE,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AASpC,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAC/C,GAAG,EAAE,MAAM,GACV,aAAa,CAwBf"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S npx tsx
|
|
2
|
-
// Rewrites the Changesets-opened "Version Packages" PR title from the static
|
|
3
|
-
// `release: version packages` (set in release.yml) to `<name>@<version>` after
|
|
4
|
-
// the action has bumped package.json on the changeset-release/main branch.
|
|
5
|
-
//
|
|
6
|
-
// Inputs are read from env, not argv, so the script is trivially mockable in
|
|
7
|
-
// tests and we don't have to think about shell quoting in YAML:
|
|
8
|
-
//
|
|
9
|
-
// PR_NUMBER — the changesets/action output `pullRequestNumber`. Empty means
|
|
10
|
-
// the action either published (no PR) or had nothing to do; in
|
|
11
|
-
// that case we exit cleanly. The workflow's `if:` should gate
|
|
12
|
-
// this already, but the guard is here for direct invocations.
|
|
13
|
-
// GH_TOKEN — passed through to `gh` via the subprocess env.
|
|
14
|
-
import { execFileSync } from "node:child_process";
|
|
15
|
-
function realRunner(cmd, args) {
|
|
16
|
-
return execFileSync(cmd, args, {
|
|
17
|
-
encoding: "utf8",
|
|
18
|
-
stdio: ["ignore", "pipe", "inherit"],
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
export function retitleReleasePr(environment, run) {
|
|
22
|
-
const prNumber = environment.PR_NUMBER?.trim();
|
|
23
|
-
if (!prNumber) {
|
|
24
|
-
return { reason: "PR_NUMBER is empty", status: "skipped" };
|
|
25
|
-
}
|
|
26
|
-
run("git", ["fetch", "origin", "changeset-release/main"]);
|
|
27
|
-
const packageJsonRaw = run("git", ["show", "FETCH_HEAD:package.json"]);
|
|
28
|
-
const pkg = JSON.parse(packageJsonRaw);
|
|
29
|
-
if (typeof pkg.name !== "string" || pkg.name.length === 0) {
|
|
30
|
-
throw new Error("package.json is missing a string `name`");
|
|
31
|
-
}
|
|
32
|
-
if (typeof pkg.version !== "string" || pkg.version.length === 0) {
|
|
33
|
-
throw new Error("package.json is missing a string `version`");
|
|
34
|
-
}
|
|
35
|
-
const title = `${pkg.name}@${pkg.version}`;
|
|
36
|
-
run("gh", ["pr", "edit", prNumber, "--title", title]);
|
|
37
|
-
return { status: "ok", title };
|
|
38
|
-
}
|
|
39
|
-
function main() {
|
|
40
|
-
const result = retitleReleasePr(process.env, realRunner);
|
|
41
|
-
if (result.status === "skipped") {
|
|
42
|
-
console.log(`retitle-release-pr: skipped (${result.reason})`);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
console.log(`retitle-release-pr: set title to ${result.title}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
49
|
-
main();
|
|
50
|
-
}
|