@avi2dg/checks 0.24.0 → 0.24.2
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/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +2 -1
- package/docs/gates/checks-changelog.md +6 -1
- package/package.json +1 -1
- package/scripts/changelog-write.ts +37 -5
- package/scripts/changelog.ts +11 -1
- package/scripts/doc-templates.ts +1 -1
- package/templates/changelog.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.24.2
|
|
6
|
+
|
|
7
|
+
Released 2026-09-27.
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
|
|
11
|
+
- **scripts:** list commits merged in after a bump under that release [#82](https://github.com/avi2d/checks/pull/82)
|
|
12
|
+
- **scripts:** leave versions with no listable commits out of the changelog [#81](https://github.com/avi2d/checks/pull/81)
|
|
13
|
+
|
|
14
|
+
## 0.24.1
|
|
15
|
+
|
|
16
|
+
Released 2026-09-27.
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
- **scripts:** let checks-changelog release a repository that gains its version late [#79](https://github.com/avi2d/checks/pull/79)
|
|
21
|
+
|
|
5
22
|
## 0.24.0
|
|
6
23
|
|
|
7
24
|
Released 2026-09-27.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -41,7 +41,8 @@ To release a version:
|
|
|
41
41
|
The build writes the version's section into `CHANGELOG.md` from the conventional commits since the last release, so the changelog is never edited by hand.
|
|
42
42
|
1. Commit both as `chore: release <version>` and title the pull request the same.
|
|
43
43
|
The squash merge lands the title as the commit's subject, and a `feat` or `fix` title would add an entry the committed changelog lacks.
|
|
44
|
-
1.
|
|
44
|
+
1. Merge `main` into the release branch and run `bun run build` again right before it merges, committing what it rewrote, since the build lists a commit merged in after the bump under the release.
|
|
45
|
+
Never rebase the release branch.
|
|
45
46
|
1. Once it merges, tag that commit on `main` with the version and push the tag, since the version bump commit closes the release:
|
|
46
47
|
|
|
47
48
|
```sh
|
|
@@ -10,16 +10,21 @@ audience: consumers
|
|
|
10
10
|
|
|
11
11
|
It reads the version from `package.json` and treats that version as the release being prepared.
|
|
12
12
|
It lists every conventional commit the release closes, grouped as Features, Fixes, Performance, Reverts and Breaking changes.
|
|
13
|
+
It lists the commits a branch merged in after its bump under that bump, since the squash merge releases them there.
|
|
14
|
+
It leaves out what the branch added past its bump, since the squash merge folds it into the release commit.
|
|
15
|
+
Only a branch carrying its own unlanded bump lists the commits of an unlanded branch it merges, so such a branch takes in only main.
|
|
16
|
+
CI must build the pull request head commit, the `actions/checkout` ref `github.event.pull_request.head.sha`, and not the GitHub merge ref, since the merge ref puts main first and the branch's own bump off the first-parent chain.
|
|
13
17
|
It links each entry to its pull request under the repository address `package.json` names.
|
|
14
18
|
It keeps the date a released section already carries and dates a new section today.
|
|
15
19
|
It writes the whole file newest first, so the changelog is never edited by hand.
|
|
16
20
|
A repository with no tag yet releases from its first commit.
|
|
17
|
-
A
|
|
21
|
+
A version with no conventional commit worth listing writes no section.
|
|
18
22
|
|
|
19
23
|
## What it reads
|
|
20
24
|
|
|
21
25
|
It reads `package.json`, `CHANGELOG.md` and the git history from the repository root.
|
|
22
26
|
It finds releases in the version bumps of `package.json` across all of `HEAD` ancestry, so a checkout without tags writes the same file.
|
|
27
|
+
It reads a commit whose `package.json` is missing or has no version as unversioned, so the commit that adds the version is the first release and a repository that adopts a version late still writes its changelog.
|
|
23
28
|
It refuses a shallow checkout, since the releases reach back past its history.
|
|
24
29
|
It refuses a `package.json` with no repository address, since each entry links its pull request under it.
|
|
25
30
|
It refuses a repository address that is no `https` address once `git+`, a trailing slash and `.git` are dropped, since a pull request link needs one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, DateTime, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
-
import { cuts, releaseDates, renderChangelog, type Bump, type Cut, type Release } from "./changelog.ts";
|
|
3
|
+
import { cuts, hasEntries, namesPullRequest, releaseDates, renderChangelog, type Bump, type Cut, type Release } from "./changelog.ts";
|
|
4
4
|
import { git } from "./git.ts";
|
|
5
5
|
import { runMain } from "./main.ts";
|
|
6
6
|
|
|
@@ -24,9 +24,14 @@ const decodeManifestJson = Schema.decodeUnknownEffect(
|
|
|
24
24
|
),
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
+
const decodeVersionJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ version: Schema.optional(Schema.String) })));
|
|
28
|
+
|
|
27
29
|
const decodeManifest = (text: string, source: string) =>
|
|
28
30
|
decodeManifestJson(text).pipe(Effect.mapError((cause) => new ChangelogUnreadable({ message: `${source}: ${cause.message}` })));
|
|
29
31
|
|
|
32
|
+
const decodeVersion = (text: string, source: string) =>
|
|
33
|
+
decodeVersionJson(text).pipe(Effect.mapError((cause) => new ChangelogUnreadable({ message: `${source}: ${cause.message}` })));
|
|
34
|
+
|
|
30
35
|
const today = DateTime.nowInCurrentZone.pipe(DateTime.withCurrentZoneLocal, Effect.map(DateTime.formatIsoDate));
|
|
31
36
|
|
|
32
37
|
function repositoryWebUrl(repository: string): string | undefined {
|
|
@@ -38,7 +43,9 @@ function repositoryWebUrl(repository: string): string | undefined {
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
const versionAt = Effect.fn("versionAt")(function* (root: string, sha: string) {
|
|
41
|
-
|
|
46
|
+
const blob = (yield* git(["ls-tree", "--object-only", sha, "--", MANIFEST], root)).trim();
|
|
47
|
+
if (blob === "") return undefined;
|
|
48
|
+
return (yield* decodeVersion(yield* git(["cat-file", "blob", blob], root), `${MANIFEST} at ${sha}`)).version;
|
|
42
49
|
});
|
|
43
50
|
|
|
44
51
|
const readBumps = Effect.fn("readBumps")(function* (root: string) {
|
|
@@ -51,6 +58,7 @@ const readBumps = Effect.fn("readBumps")(function* (root: string) {
|
|
|
51
58
|
const [sha, date, parent] = line.split(FIELD);
|
|
52
59
|
if (sha === undefined || date === undefined || parent === undefined) continue;
|
|
53
60
|
const version = yield* versionAt(root, sha);
|
|
61
|
+
if (version === undefined) continue;
|
|
54
62
|
if (parent === "" || (yield* versionAt(root, parent)) !== version) bumps.push({ sha, version, date });
|
|
55
63
|
}
|
|
56
64
|
return bumps;
|
|
@@ -61,8 +69,28 @@ const readPublished = Effect.fn("readPublished")(function* (root: string) {
|
|
|
61
69
|
return new Set(tags.split("\n").flatMap((tag) => (tag.startsWith(TAG_PREFIX) ? [tag.slice(TAG_PREFIX.length)] : [])));
|
|
62
70
|
});
|
|
63
71
|
|
|
64
|
-
const
|
|
65
|
-
|
|
72
|
+
const mergedTips = Effect.fn("mergedTips")(function* (root: string, through: string) {
|
|
73
|
+
// A bump that names its pull request or sits off the first-parent chain is main's release, so no squash merge of this branch releases more under it.
|
|
74
|
+
if (namesPullRequest((yield* git(["log", "-1", "--format=%s", through], root)).trim())) return [];
|
|
75
|
+
if (!(yield* git(["rev-list", "--first-parent", "HEAD"], root)).split("\n").includes(through)) return [];
|
|
76
|
+
const merges = yield* git(["log", "--merges", "--format=%P", `${through}..HEAD`], root);
|
|
77
|
+
const tips: string[] = [];
|
|
78
|
+
for (const [, ...parents] of merges.split("\n").filter((line) => line !== "").map((line) => line.split(" "))) {
|
|
79
|
+
for (const parent of parents) {
|
|
80
|
+
// A parent past the bump belongs to a later release, so only a parent beside it joins this one.
|
|
81
|
+
const past = yield* git(["merge-base", "--is-ancestor", through, parent], root).pipe(
|
|
82
|
+
Effect.as(true),
|
|
83
|
+
Effect.catchTag("GitFailure", () => Effect.succeed(false)),
|
|
84
|
+
);
|
|
85
|
+
if (!past) tips.push(parent);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return tips;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// The bump reaches no commit main gained after it, yet the squash merge releases them under it.
|
|
92
|
+
const subjectsOf = Effect.fn("subjectsOf")(function* (root: string, { version, date, through, after }: Cut, extra: readonly string[] = []) {
|
|
93
|
+
const log = yield* git(["log", "--topo-order", "--format=%s", through, ...extra, "--not", ...after], root);
|
|
66
94
|
return { version, date, subjects: log.split("\n").filter((subject) => subject !== "") } satisfies Release;
|
|
67
95
|
});
|
|
68
96
|
|
|
@@ -86,7 +114,11 @@ const write = Effect.gen(function* () {
|
|
|
86
114
|
const recorded = (yield* fs.exists(target)) ? releaseDates(yield* fs.readFileString(target)) : new Map<string, string>();
|
|
87
115
|
const pending = version === (yield* versionAt(root, "HEAD")) ? undefined : { sha: "HEAD", version, date: yield* today };
|
|
88
116
|
const released = cuts(yield* readBumps(root), recorded, yield* readPublished(root), pending);
|
|
89
|
-
const
|
|
117
|
+
const tip = released.at(-1);
|
|
118
|
+
const extra = tip === undefined || tip.through === "HEAD" ? [] : yield* mergedTips(root, tip.through);
|
|
119
|
+
const found = (yield* Effect.forEach(released, (cut, index) => subjectsOf(root, cut, index === released.length - 1 ? extra : [])))
|
|
120
|
+
.filter(({ subjects }) => hasEntries(subjects))
|
|
121
|
+
.toReversed();
|
|
90
122
|
yield* fs.writeFileString(target, renderChangelog(name, found, repositoryUrl));
|
|
91
123
|
yield* Console.log(`${NAME}: wrote ${found.length} release(s) to ${CHANGELOG}`);
|
|
92
124
|
return true;
|
package/scripts/changelog.ts
CHANGED
|
@@ -28,6 +28,8 @@ type Entry = {
|
|
|
28
28
|
|
|
29
29
|
const CONVENTIONAL = /^([a-z]+)(?:\(([^()]*)\))?(!)?: (\S.*)$/;
|
|
30
30
|
|
|
31
|
+
const PULL_REQUEST = / \(#(\d+)\)$/;
|
|
32
|
+
|
|
31
33
|
const GROUP_OF_TYPE = new Map<string, ChangeGroup>([
|
|
32
34
|
["feat", "Features"],
|
|
33
35
|
["fix", "Fixes"],
|
|
@@ -42,11 +44,15 @@ export function groupOf(subject: string): ChangeGroup | undefined {
|
|
|
42
44
|
return breaking === "!" ? "Breaking changes" : GROUP_OF_TYPE.get(type);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
export function namesPullRequest(subject: string): boolean {
|
|
48
|
+
return PULL_REQUEST.test(subject);
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
function entryOf(subject: string, repositoryUrl: string): Entry | undefined {
|
|
46
52
|
const group = groupOf(subject);
|
|
47
53
|
if (group === undefined) return undefined;
|
|
48
54
|
const [, , scope = "", , description = ""] = CONVENTIONAL.exec(subject) ?? [];
|
|
49
|
-
const linkedDescription = description.replace(
|
|
55
|
+
const linkedDescription = description.replace(PULL_REQUEST, (_, number: string) => ` [#${number}](${repositoryUrl}/pull/${number})`);
|
|
50
56
|
return { group, text: scope === "" ? linkedDescription : `**${scope}:** ${linkedDescription}` };
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -82,6 +88,10 @@ export function cuts(
|
|
|
82
88
|
}));
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
export function hasEntries(subjects: readonly string[]): boolean {
|
|
92
|
+
return subjects.some((subject) => groupOf(subject) !== undefined);
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
function renderRelease({ version, date, subjects }: Release, repositoryUrl: string): readonly string[] {
|
|
86
96
|
const entries = subjects.flatMap((subject) => entryOf(subject, repositoryUrl) ?? []);
|
|
87
97
|
const groups = CHANGE_GROUPS.flatMap((group) => {
|
package/scripts/doc-templates.ts
CHANGED
|
@@ -91,7 +91,7 @@ export const TEMPLATES: Readonly<Record<Kind, Template>> = {
|
|
|
91
91
|
title: { type: "fixed", text: "Changelog" },
|
|
92
92
|
lead: ["Every release of <package>, newest first, written by the release from its conventional commits."],
|
|
93
93
|
sections: [
|
|
94
|
-
open("<version>", "version",
|
|
94
|
+
open("<version>", "version", optional("no release holds a conventional commit worth listing yet"), ["Released <YYYY-MM-DD>."], {
|
|
95
95
|
subsections: CHANGE_GROUPS.map((group) =>
|
|
96
96
|
fixed(group, optional("the release holds no such commit"), ["- **<the commit's scope, when it has one>:** <its description>"]),
|
|
97
97
|
),
|
package/templates/changelog.md
CHANGED