@acme-skunkworks/eslint-config 1.0.3 → 1.0.5
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 +49 -109
- 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/check-changelog-completeness.d.ts +9 -0
- package/dist/infrastructure/scripts/check-changelog-completeness.d.ts.map +1 -0
- package/dist/infrastructure/scripts/check-changelog-completeness.js +68 -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 +153 -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/infrastructure/send-it/derive-changeset.d.ts.map +1 -1
- package/dist/infrastructure/send-it/derive-changeset.js +7 -3
- package/dist/rules/sanity.d.ts +1 -1
- package/dist/rules/sanity.js +7 -7
- package/package.json +8 -10
- 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
package/README.md
CHANGED
|
@@ -1,142 +1,82 @@
|
|
|
1
1
|
# @acme-skunkworks/eslint-config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> A shared ESLint v9 flat-config **preset composer** for TypeScript and React projects — import named-export presets and compose only the ones you need.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@acme-skunkworks/eslint-config)
|
|
6
|
+
[](https://www.npmjs.com/package/@acme-skunkworks/eslint-config#provenance)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://www.npmjs.com/package/@acme-skunkworks/eslint-config)
|
|
9
|
+
|
|
10
|
+
Every release is published to npm via OIDC Trusted Publishing with a **provenance attestation** — the artefact is built and signed on GitHub Actions, so consumers can verify exactly which commit and workflow produced it.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
6
13
|
|
|
7
14
|
```bash
|
|
8
15
|
pnpm add -D @acme-skunkworks/eslint-config eslint prettier
|
|
9
16
|
```
|
|
10
17
|
|
|
11
|
-
Every ESLint plugin
|
|
18
|
+
`eslint` (`^8.57.0 || ^9.0.0`) and `prettier` (`^3.0.0`) are **required peer dependencies** — install them alongside. Every ESLint plugin the config uses ships as a regular dependency, so you don't install plugins separately. Node 22+ is required.
|
|
19
|
+
|
|
20
|
+
> **ESLint v8 users:** flat config isn't the default in v8 — set `ESLINT_USE_FLAT_CONFIG=1` so ESLint reads your `eslint.config.js`. ESLint v9 uses flat config by default, so no flag is needed.
|
|
12
21
|
|
|
13
|
-
##
|
|
22
|
+
## Quick start
|
|
14
23
|
|
|
15
|
-
The package
|
|
24
|
+
The package exposes each preset as a named export. Compose them in your `eslint.config.js`:
|
|
16
25
|
|
|
17
26
|
```js
|
|
18
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
base,
|
|
29
|
+
typescript,
|
|
30
|
+
frameworkRouting,
|
|
31
|
+
} from "@acme-skunkworks/eslint-config";
|
|
19
32
|
|
|
20
33
|
export default [
|
|
21
34
|
...base,
|
|
22
35
|
typescript,
|
|
23
36
|
...frameworkRouting,
|
|
24
37
|
// your overrides last so they win
|
|
25
|
-
{
|
|
26
|
-
rules: {
|
|
27
|
-
"@typescript-eslint/no-explicit-any": "warn",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
38
|
+
{ rules: { "@typescript-eslint/no-explicit-any": "warn" } },
|
|
30
39
|
];
|
|
31
40
|
```
|
|
32
41
|
|
|
33
|
-
Pull in the presets
|
|
34
|
-
|
|
35
|
-
| Export | What it covers |
|
|
36
|
-
|---|---|
|
|
37
|
-
| `base` | Plugin-alias hack + global ignores + the canonical baseline + packageJson lint config + commonjs file overrides + the big preferences block (top-level type imports, `func-style: declaration`, prettier integration, import resolver, `no-console` warn, etc.). The "you almost always want this" stack. |
|
|
38
|
-
| `typescript` | Overrides for `**/*.{ts,tsx}` (disables `react/no-unused-prop-types` and `react/prop-types`). |
|
|
39
|
-
| `frameworkRouting` | Disables `canonical/filename-match-exported` for routing dirs (`routes/**`, `app/**`, `pages/**`, `src/routes/**`, `src/pages/**`); re-allows arrow functions on `root.tsx` / `*.route.tsx`. Order matters — must spread **after** `base`. |
|
|
40
|
-
| `astro` | `eslint-plugin-astro/flat/recommended` + Astro-specific overrides. Pull in for Astro projects. |
|
|
41
|
-
| `sanity` | Schema property ordering for `*.schema.ts` and structure-file exceptions. Pull in for projects using Sanity Studio. |
|
|
42
|
-
| `testing` | Relaxes strict TypeScript rules and devDependencies imports for `**/*.{test,spec}.*`, `__tests__/**`, and setup files. |
|
|
43
|
-
| `storybook` | Overrides for `**/*.stories.{ts,tsx}`. |
|
|
44
|
-
| `complexity` | Raises cyclomatic complexity threshold to 40 for `**/scripts/**` (orchestration scripts run linearly). Opt-in. |
|
|
45
|
-
| `e2e` | Disables `react-hooks/rules-of-hooks` for `**/e2e/**` (Playwright `test.extend` callbacks are false positives). Opt-in. |
|
|
46
|
-
| `tableComponents` | Disables `react/no-unstable-nested-components` for `**/*Table.tsx` (TanStack Table / Refine column-cell renderers). Opt-in. |
|
|
47
|
-
|
|
48
|
-
> **Note:** Requires ESLint v9+ with flat config. Node 22+.
|
|
49
|
-
|
|
50
|
-
## 🔄 Migrating from `@robeasthope/eslint-config`
|
|
51
|
-
|
|
52
|
-
This package was previously published as `@robeasthope/eslint-config` from the [`RobEasthope/protomolecule`](https://github.com/RobEasthope/protomolecule) monorepo (versions up to and including v6.2.1). It now ships from this standalone repo under the `@acme-skunkworks` scope, with a named-export composition pattern.
|
|
53
|
-
|
|
54
|
-
### Step 1: rename the dep
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
pnpm remove @robeasthope/eslint-config
|
|
58
|
-
pnpm add -D @acme-skunkworks/eslint-config
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Step 2: switch to named exports
|
|
62
|
-
|
|
63
|
-
The default export still works during the migration window, but is **deprecated** and will be removed in a future major:
|
|
64
|
-
|
|
65
|
-
```js
|
|
66
|
-
// Old (still works in v1, deprecated)
|
|
67
|
-
import eslintConfig from "@acme-skunkworks/eslint-config";
|
|
68
|
-
export default [...eslintConfig];
|
|
69
|
-
|
|
70
|
-
// New (preferred — pull in only what you need)
|
|
71
|
-
import { base, typescript, frameworkRouting } from "@acme-skunkworks/eslint-config";
|
|
72
|
-
export default [...base, typescript, ...frameworkRouting];
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
If your project was a Sanity / Storybook / Astro consumer, opt-in to those presets explicitly. The default export bundled all of them; the new shape makes the dependencies explicit.
|
|
42
|
+
Pull in only the presets your project needs. Array presets are spread with `...`; single-config presets are added as-is — the table below notes which is which.
|
|
76
43
|
|
|
77
|
-
|
|
44
|
+
## Presets
|
|
78
45
|
|
|
79
|
-
|
|
80
|
-
- **Named-export composition.** Each preset is independently importable; consumers compose what they need.
|
|
81
|
-
- **`prettier` is now a `peerDependency`.** Was already a transitive dep via `eslint-plugin-prettier`; this just makes the contract explicit.
|
|
46
|
+
**Core** — the everyday stack:
|
|
82
47
|
|
|
83
|
-
|
|
48
|
+
| Preset | Shape | What it covers |
|
|
49
|
+
| ------------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `base` | array (`...`) | Plugin-alias hack, global ignores, the canonical baseline, `package.json` lint config, CommonJS file overrides, and the `preferences` block (top-level type imports, `func-style: declaration`, Prettier integration, import resolver, `no-console` warn). The "you almost always want this" stack. |
|
|
51
|
+
| `typescript` | single | Overrides for `**/*.{ts,tsx}` — disables `react/no-unused-prop-types` and `react/prop-types`, which are redundant under TypeScript. |
|
|
52
|
+
| `frameworkRouting` | array (`...`) | Turns off `canonical/filename-match-exported` for routing dirs — `routes/**`, `app/**`, `pages/**` (Next.js), `src/routes/**` (SvelteKit), `src/pages/**` (Astro) — and re-allows arrow functions on `root.tsx` / `*.route.tsx`. Spread **after** `base` (see Gotchas). |
|
|
53
|
+
| `testing` | single | Relaxes strict TypeScript rules and `import/no-extraneous-dependencies` for `**/*.{test,spec}.*`, `__tests__/**`, and setup files. |
|
|
84
54
|
|
|
85
|
-
|
|
55
|
+
**Opt-in** — pull in per project:
|
|
86
56
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
-
|
|
57
|
+
| Preset | Shape | What it covers |
|
|
58
|
+
| ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `astro` | array (`...`) | `eslint-plugin-astro/flat/recommended` plus Astro-specific overrides. |
|
|
60
|
+
| `sanity` | array (`...`) | Schema property ordering for `*.schema.ts` and structure-file exceptions, for Sanity Studio projects. |
|
|
61
|
+
| `storybook` | single | Overrides for `**/*.stories.{ts,tsx}`. |
|
|
62
|
+
| `complexity` | array (`...`) | Raises the cyclomatic-complexity threshold to 40 for `**/scripts/**`, where orchestration scripts run linearly. |
|
|
63
|
+
| `e2e` | single | Disables `react-hooks/rules-of-hooks` for `**/e2e/**` — Playwright `test.extend` callbacks are false positives. |
|
|
64
|
+
| `tableComponents` | single | Disables `react/no-unstable-nested-components` for `**/*Table.tsx` — TanStack Table / Refine column-cell renderers. |
|
|
93
65
|
|
|
94
|
-
|
|
66
|
+
> A deprecated **default export** still bundles the v6.x composition for back-compat during migration. New code should use the named exports above. See the [migration guide](MIGRATION_FROM_PROTOMOLECULE.md).
|
|
95
67
|
|
|
96
|
-
|
|
68
|
+
## Gotchas
|
|
97
69
|
|
|
98
|
-
|
|
99
|
-
import { base, typescript } from "@acme-skunkworks/eslint-config";
|
|
100
|
-
|
|
101
|
-
export default [
|
|
102
|
-
...base,
|
|
103
|
-
typescript,
|
|
104
|
-
{
|
|
105
|
-
rules: {
|
|
106
|
-
"@typescript-eslint/no-explicit-any": "warn",
|
|
107
|
-
"react/prop-types": "off",
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Add additional plugins by configuring them directly:
|
|
114
|
-
|
|
115
|
-
```js
|
|
116
|
-
import { base, typescript } from "@acme-skunkworks/eslint-config";
|
|
117
|
-
import pluginReact from "eslint-plugin-react";
|
|
70
|
+
Two ordering rules are easy to get wrong and fail **silently** — the config still loads, but the intended override never wins:
|
|
118
71
|
|
|
119
|
-
export
|
|
120
|
-
|
|
121
|
-
typescript,
|
|
122
|
-
{
|
|
123
|
-
plugins: { react: pluginReact },
|
|
124
|
-
rules: { "react/jsx-uses-react": "error" },
|
|
125
|
-
},
|
|
126
|
-
];
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## 🔧 Development
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
pnpm install # install deps
|
|
133
|
-
pnpm run build # tsc → dist/
|
|
134
|
-
pnpm lint # lint this package's own source
|
|
135
|
-
pnpm lint:fix # auto-fix
|
|
136
|
-
```
|
|
72
|
+
- **Spread `frameworkRouting` _after_ `...base`.** Its `func-style` override must come later in the array than the `preferences` block (which `base` includes), or `base` wins and the React Router 7 typed-export pattern breaks.
|
|
73
|
+
- **`frameworkRouting` is an ordered pair of configs** for the same reason — its second element (the React Router exceptions) must stay after its first, which in turn must follow `preferences`. If you ever expand the spread into individual elements, preserve their order.
|
|
137
74
|
|
|
138
|
-
|
|
75
|
+
Full rationale (and the protomolecule issues behind it) is in [`CLAUDE.md`](CLAUDE.md).
|
|
139
76
|
|
|
140
|
-
|
|
77
|
+
## Links
|
|
141
78
|
|
|
142
|
-
|
|
79
|
+
- [Changelog](CHANGELOG.md)
|
|
80
|
+
- [Migration guide](MIGRATION_FROM_PROTOMOLECULE.md) — moving from `@robeasthope/eslint-config`
|
|
81
|
+
- [Repository](https://github.com/acme-skunkworks/eslint-config)
|
|
82
|
+
- [Licence](LICENSE) — MIT
|
|
@@ -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,9 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
export declare function isReleaseTriggering(prTitle: string): boolean;
|
|
3
|
+
export declare function hasChangelogEntry(changedFiles: readonly string[]): boolean;
|
|
4
|
+
export type CompletenessResult = {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
reason: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function checkCompleteness(prTitle: string, changedFiles: readonly string[]): CompletenessResult;
|
|
9
|
+
//# sourceMappingURL=check-changelog-completeness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-changelog-completeness.d.ts","sourceRoot":"","sources":["../../../infrastructure/scripts/check-changelog-completeness.ts"],"names":[],"mappings":";AAuBA,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAI1E;AAED,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B,kBAAkB,CAmBpB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
// Changelog-completeness gate (SK-371). A release-triggering PR title
|
|
3
|
+
// (`feat`/`fix`/breaking) MUST carry a dated `changelog/` entry. This restores
|
|
4
|
+
// the coupling Changesets gave for free — no changeset → no release — now that
|
|
5
|
+
// release-please infers the bump from the Conventional-Commit PR title rather
|
|
6
|
+
// than an explicit file. Wired into ci.yml's build-and-lint job.
|
|
7
|
+
//
|
|
8
|
+
// "Release-triggering" mirrors release-please's default node bump table exactly:
|
|
9
|
+
// only `feat` (minor), `fix` (patch), and a `!` breaking marker (major) cut a
|
|
10
|
+
// release; `docs`/`chore`/`ci`/`refactor`/`perf`/`test`/`build`/`style` do not.
|
|
11
|
+
//
|
|
12
|
+
// Inputs (env, set by the workflow):
|
|
13
|
+
// PR_TITLE — the pull request title (github.event.pull_request.title)
|
|
14
|
+
// BASE_REF — the base branch name (github.base_ref); defaults to "main"
|
|
15
|
+
// Reads changed files from `git diff --name-only origin/<BASE_REF>...HEAD`.
|
|
16
|
+
// Pure functions live exported for vitest.
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
const RELEASE_TRIGGERING_TYPE = /^(feat|fix)(\([^)]+\))?:/;
|
|
19
|
+
const BREAKING_SUBJECT = /^[a-z]+(\([^)]+\))?!:/;
|
|
20
|
+
const CHANGELOG_ENTRY = /^changelog\/.+\.md$/;
|
|
21
|
+
export function isReleaseTriggering(prTitle) {
|
|
22
|
+
const title = prTitle.trim();
|
|
23
|
+
return BREAKING_SUBJECT.test(title) || RELEASE_TRIGGERING_TYPE.test(title);
|
|
24
|
+
}
|
|
25
|
+
export function hasChangelogEntry(changedFiles) {
|
|
26
|
+
return changedFiles.some((file) => CHANGELOG_ENTRY.test(file) && file !== "changelog/README.md");
|
|
27
|
+
}
|
|
28
|
+
export function checkCompleteness(prTitle, changedFiles) {
|
|
29
|
+
if (!isReleaseTriggering(prTitle)) {
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
reason: `PR title "${prTitle}" is not release-triggering — no changelog entry required.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (hasChangelogEntry(changedFiles)) {
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
reason: "Release-triggering PR title with a changelog/ entry present.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: `PR title "${prTitle}" triggers a release (feat/fix/breaking) but no changelog/*.md entry is present in the diff vs the base branch. Run /send-it (or add a dated changelog/ entry) so the release carries notes.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function readChangedFiles(baseRef) {
|
|
47
|
+
const out = execFileSync("git", ["diff", "--name-only", `origin/${baseRef}...HEAD`], { encoding: "utf8" });
|
|
48
|
+
return out
|
|
49
|
+
.split("\n")
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
function main() {
|
|
54
|
+
const prTitle = process.env.PR_TITLE ?? "";
|
|
55
|
+
const baseRef = process.env.BASE_REF || "main";
|
|
56
|
+
if (!prTitle) {
|
|
57
|
+
console.error("PR_TITLE is not set — cannot run the changelog-completeness gate.");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const result = checkCompleteness(prTitle, readChangedFiles(baseRef));
|
|
61
|
+
console.log(result.reason);
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
67
|
+
main();
|
|
68
|
+
}
|
|
@@ -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,CA6EpD"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
// Release-time finalisation of changelog entries — run by the orchestrator
|
|
3
|
+
// right after `release-please release-pr` (SK-371/SK-376), so the result is
|
|
4
|
+
// committed into the release PR (no separate workflow, no bot push to main).
|
|
5
|
+
// Reads the just-bumped version from package.json, which release-please updated.
|
|
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
|
+
// the release-please release-PR build and block the release. On any error,
|
|
123
|
+
// warn and return null — the entry still gets version-stamped, just without
|
|
124
|
+
// PR metadata.
|
|
125
|
+
try {
|
|
126
|
+
return resolve(branch);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn(`⚠️ Could not resolve PR for branch ${branch}: ${error.message}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function main() {
|
|
135
|
+
const version = readPackageVersion(readFileSync("package.json", "utf8"));
|
|
136
|
+
const resolvePr = makeResolver(realRunner);
|
|
137
|
+
const files = readdirSync(CHANGELOG_DIR)
|
|
138
|
+
.filter((name) => name.endsWith(".md") && name !== "README.md")
|
|
139
|
+
.map((name) => join(CHANGELOG_DIR, name));
|
|
140
|
+
let finalised = 0;
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
const next = finaliseEntry(readFileSync(file, "utf8"), version, resolvePr);
|
|
143
|
+
if (next !== null) {
|
|
144
|
+
writeFileSync(file, next);
|
|
145
|
+
finalised++;
|
|
146
|
+
console.log(`finalised ${version}: ${file}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
console.log(`Changelog finalisation complete. ${finalised} entr${finalised === 1 ? "y" : "ies"} finalised with ${version}.`);
|
|
150
|
+
}
|
|
151
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
152
|
+
main();
|
|
153
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derive-changeset.d.ts","sourceRoot":"","sources":["../../../infrastructure/send-it/derive-changeset.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"derive-changeset.d.ts","sourceRoot":"","sources":["../../../infrastructure/send-it/derive-changeset.ts"],"names":[],"mappings":";AAoBA,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAYjD;AAED,MAAM,MAAM,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AACtE,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAK/C,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAmB3D;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAO7D"}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
|
-
// Derives the deterministic bits
|
|
2
|
+
// Derives the deterministic bits /send-it needs from the branch commits.
|
|
3
3
|
// Run: pnpm tsx infrastructure/send-it/derive-changeset.ts
|
|
4
4
|
//
|
|
5
|
+
// Since SK-371 there is no changeset file; /send-it uses these to name the dated
|
|
6
|
+
// changelog/ entry and to compose the Conventional Commits PR title (the
|
|
7
|
+
// release-please bump signal). The filename is retained for git history.
|
|
8
|
+
//
|
|
5
9
|
// Fields:
|
|
6
|
-
// slug : branch-name-derived
|
|
7
|
-
// bump : major | minor | patch (
|
|
10
|
+
// slug : branch-name-derived slug (changelog/<ts>-<slug>.md filename)
|
|
11
|
+
// bump : major | minor | patch (drives the PR-title prefix: feat!/feat/fix)
|
|
8
12
|
// body : a one-line draft summary (the slash command may rewrite this)
|
|
9
13
|
//
|
|
10
14
|
// Reads from git via `git branch --show-current` and `git log origin/main..HEAD`
|
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.5",
|
|
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,7 @@
|
|
|
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",
|
|
45
49
|
"ci:list": "gh run list --limit 10",
|
|
46
50
|
"ci:view": "gh run view",
|
|
47
51
|
"ci:watch": "gh run watch $(gh run list -L 1 --json databaseId -q '.[0].databaseId // empty')",
|
|
@@ -56,7 +60,6 @@
|
|
|
56
60
|
"lint:yaml": "yamllint .",
|
|
57
61
|
"prepare": "husky",
|
|
58
62
|
"prepublishOnly": "pnpm run build",
|
|
59
|
-
"release": "pnpm run build && changeset publish",
|
|
60
63
|
"release:manual": "pnpm run build && npm publish --access public --provenance=false",
|
|
61
64
|
"release:manual:dry": "pnpm run build && npm publish --access public --provenance=false --dry-run",
|
|
62
65
|
"sort-pkg-json": "sort-package-json",
|
|
@@ -64,7 +67,7 @@
|
|
|
64
67
|
"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
68
|
"test:watch": "vitest",
|
|
66
69
|
"tsc": "tsc --noEmit",
|
|
67
|
-
"
|
|
70
|
+
"validate:changelog": "tsx infrastructure/scripts/validate-changelog.ts"
|
|
68
71
|
},
|
|
69
72
|
"lint-staged": {
|
|
70
73
|
"**/*": [
|
|
@@ -108,9 +111,9 @@
|
|
|
108
111
|
},
|
|
109
112
|
"devDependencies": {
|
|
110
113
|
"@acme-skunkworks/markdownlint-config": "^2.0.0",
|
|
111
|
-
"@changesets/cli": "^2.31.0",
|
|
112
114
|
"@types/eslint": "^9.6.1",
|
|
113
115
|
"@types/node": "^25.6.0",
|
|
116
|
+
"gray-matter": "^4.0.3",
|
|
114
117
|
"husky": "^9.1.7",
|
|
115
118
|
"lint-staged": "^16.3.2",
|
|
116
119
|
"markdownlint-cli2": "^0.18.1",
|
|
@@ -131,10 +134,5 @@
|
|
|
131
134
|
"publishConfig": {
|
|
132
135
|
"access": "public",
|
|
133
136
|
"provenance": true
|
|
134
|
-
},
|
|
135
|
-
"pnpm": {
|
|
136
|
-
"onlyBuiltDependencies": [
|
|
137
|
-
"unrs-resolver"
|
|
138
|
-
]
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -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
|
-
}
|