@avi2dg/checks 0.61.0 → 0.61.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
4
4
 
5
+ ## 0.61.1
6
+
7
+ Released 2026-11-02.
8
+
9
+ ### Fixes
10
+
11
+ - **changelog:** keep a release's section when its commit gets its tag [#23](https://github.com/avi2d/checks/pull/23)
12
+
5
13
  ## 0.61.0
6
14
 
7
15
  Released 2026-11-01.
@@ -27,6 +27,8 @@ It finds releases in the version bumps of `package.json` across all of `HEAD` an
27
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.
28
28
  A bump to a version no newer than the release before it is a revert.
29
29
  A revert cancels every release above the version it returns to, except a release a `v` tag marks as published.
30
+ A `v` tag publishes only the newest bump of its version that the tagged commit reaches.
31
+ So a version bumped again after a cancel keeps its cancelled bump cancelled, and the tag leaves the changelog unchanged.
30
32
  A version older than the newest in the committed `CHANGELOG.md` and absent from it was never published, so its commits go into the next release.
31
33
  It refuses a shallow checkout, since the releases reach back past its history.
32
34
  It refuses a `package.json` with no repository address, since each entry links its pull request under it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avi2dg/checks",
3
- "version": "0.61.0",
3
+ "version": "0.61.1",
4
4
  "description": "Deterministic checks shared across a set of TypeScript repositories",
5
5
  "author": {
6
6
  "name": "avi2d",
@@ -64,9 +64,26 @@ const readBumps = Effect.fn("readBumps")(function* (root: string) {
64
64
  return bumps;
65
65
  });
66
66
 
67
- const readPublished = Effect.fn("readPublished")(function* (root: string) {
67
+ const reaches = (root: string, ancestor: string, descendant: string) =>
68
+ git(["merge-base", "--is-ancestor", ancestor, descendant], root).pipe(
69
+ Effect.as(true),
70
+ Effect.catchTag("GitFailure", () => Effect.succeed(false)),
71
+ );
72
+
73
+ // A tag publishes the newest bump of its version that it reaches, so a cancelled older bump of that version stays cancelled.
74
+ const readPublishedShas = Effect.fn("readPublishedShas")(function* (root: string, bumps: readonly Bump[]) {
68
75
  const tags = yield* git(["for-each-ref", "--format=%(refname:strip=2)", `refs/tags/${TAG_PREFIX}*`], root);
69
- return new Set(tags.split("\n").flatMap((tag) => (tag.startsWith(TAG_PREFIX) ? [tag.slice(TAG_PREFIX.length)] : [])));
76
+ const published = new Set<string>();
77
+ for (const tag of tags.split("\n").filter((line) => line.startsWith(TAG_PREFIX))) {
78
+ const version = tag.slice(TAG_PREFIX.length);
79
+ for (const bump of bumps.filter((one) => one.version === version).toReversed()) {
80
+ if (yield* reaches(root, bump.sha, `refs/tags/${tag}`)) {
81
+ published.add(bump.sha);
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ return published;
70
87
  });
71
88
 
72
89
  const mergedTips = Effect.fn("mergedTips")(function* (root: string, through: string) {
@@ -78,11 +95,7 @@ const mergedTips = Effect.fn("mergedTips")(function* (root: string, through: str
78
95
  for (const [, ...parents] of merges.split("\n").filter((line) => line !== "").map((line) => line.split(" "))) {
79
96
  for (const parent of parents) {
80
97
  // 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);
98
+ if (!(yield* reaches(root, through, parent))) tips.push(parent);
86
99
  }
87
100
  }
88
101
  return tips;
@@ -113,7 +126,8 @@ const write = Effect.gen(function* () {
113
126
  }
114
127
  const recorded = (yield* fs.exists(target)) ? releaseDates(yield* fs.readFileString(target)) : new Map<string, string>();
115
128
  const pending = version === (yield* versionAt(root, "HEAD")) ? undefined : { sha: "HEAD", version, date: yield* today };
116
- const released = cuts(yield* readBumps(root), recorded, yield* readPublished(root), pending);
129
+ const bumps = yield* readBumps(root);
130
+ const released = cuts(bumps, recorded, yield* readPublishedShas(root, bumps), pending);
117
131
  const tip = released.at(-1);
118
132
  const extra = tip === undefined || tip.through === "HEAD" ? [] : yield* mergedTips(root, tip.through);
119
133
  const found = (yield* Effect.forEach(released, (cut, index) => subjectsOf(root, cut, index === released.length - 1 ? extra : [])))
@@ -57,12 +57,13 @@ function entryOf(subject: string, repositoryUrl: string): Entry | undefined {
57
57
  }
58
58
 
59
59
  // A bump not newer than the release before it is a revert: it cancels every release above the version it
60
- // returns to, except a published one.
61
- function standing(bumps: readonly Bump[], published: ReadonlySet<string>): readonly Bump[] {
60
+ // returns to, except a published one. A version cancelled and bumped again holds two bumps, so a bump is
61
+ // published by its sha and never by its version.
62
+ function standing(bumps: readonly Bump[], publishedShas: ReadonlySet<string>): readonly Bump[] {
62
63
  return bumps.reduce<readonly Bump[]>((kept, bump) => {
63
64
  const before = kept.at(-1);
64
65
  if (before === undefined || Bun.semver.order(bump.version, before.version) === 1) return [...kept, bump];
65
- return kept.filter(({ version }) => published.has(version) || Bun.semver.order(version, bump.version) !== 1);
66
+ return kept.filter(({ sha, version }) => publishedShas.has(sha) || Bun.semver.order(version, bump.version) !== 1);
66
67
  }, []);
67
68
  }
68
69
 
@@ -71,11 +72,11 @@ function standing(bumps: readonly Bump[], published: ReadonlySet<string>): reado
71
72
  export function cuts(
72
73
  bumps: readonly Bump[],
73
74
  recorded: ReadonlyMap<string, string>,
74
- published: ReadonlySet<string>,
75
+ publishedShas: ReadonlySet<string>,
75
76
  pending: Bump | undefined,
76
77
  ): readonly Cut[] {
77
78
  const newest = [...recorded.keys()].toSorted(Bun.semver.order).at(-1);
78
- const candidates = standing(pending === undefined ? bumps : [...bumps, pending], published);
79
+ const candidates = standing(pending === undefined ? bumps : [...bumps, pending], publishedShas);
79
80
  const released = candidates.filter(
80
81
  ({ version }, index) =>
81
82
  index === candidates.length - 1 || newest === undefined || recorded.has(version) || Bun.semver.order(version, newest) !== -1,