@oakoss/oakum 0.2.0 → 0.3.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 +129 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-09-16)
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
The workflow `init` and `migrate` print identifies the version pull request by head repository and author, not by branch name alone. `github.head_ref` carries no owner, so in a public repository any fork could name its head branch `oakum/version-packages` and skip `oakum check --strict`; a collaborator could do the same from a branch in the repository itself, and so could anyone pushing to the bot's branch, since that raises a `synchronize` whose pull-request author is still the bot. All four terms hold for any repository, so nothing has to be filled in: they test the head repository and the `type` of the pull request's author and of the event's sender, never a named account. The skip reaches the genuine version pull request only once its author is a bot whose push retriggers CI — under `secrets.GITHUB_TOKEN` that pull request raises no workflow run at all, so there is nothing to skip, and the comment above the step now says so rather than warning about a hole that is not one.
|
|
8
|
+
|
|
9
|
+
`oakum ci version-pr` says who the token opened the pull request as, on stderr — stdout stays the one URL a caller captures. A run under a personal access token opens it as a person, which the scaffolded skip does not excuse, so every check runs on the version pull request and the only other symptom is a longer bill. The identity is reported where it was measured rather than left to be inferred from a workflow that did or did not fire. It is reported, not read as a verdict: a person settles the question, because that term alone defeats the skip, while a bot does not, since the skip also tests the sender of the event the push raises and no run can see that from here. A response that omits the author, or omits its type, says so instead of defaulting into a verdict.
|
|
10
|
+
|
|
11
|
+
The workflow `init` and `migrate` print surfaces a `pr-status` run that could not post its report. That step carries `continue-on-error`, so a GitHub outage does not fail a contributor's check; the same setting also let a report that never posted pass for one that did, and left a green check over a log nobody opens. A step now follows it that, when the step failed, emits a warning into the run summary and names the check above as what still decides. It runs whether or not the check itself passed, since a failed check is exactly when the report matters. A post that fell back to the job summary, the designed path for a fork with no token that can comment, is not a failure and does not warn. oakum's own workflow gains the same step.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
`oakum check` prints its refusals as blocks, the deciding line first. Each refusal's summary comes first with its detail indented beneath it, and every refusal that did not decide the exit code follows as `also …` in the same shape, so a reader meets the verdict before what is subordinate to it, and evidence sits under the line it supports; two looks that fail identically say it once, keeping what each found. Before, each look printed its detail as it ran and its summary at the end, so the management guidance could sit five lines from its summary with three unrelated lines between, and `also` lines printed before the line they were also-to. `tag-drift` prints the same block for the same state, and with a stale install pin beside real drift it now exits 1 for the drift and names the pin as `also`, as `check` does.
|
|
16
|
+
|
|
17
|
+
Among refusals of one class, the order `check` announces decides which one carries the exit code, and the install pin is its own look after the tags. With git wholly unusable, the first line a reader meets says git could not run, not `install pin is 9.9.9`. Before, the pin was verified inside the tag look and before git was touched, so a stale pin decided for a repository whose git did not work. A base ref git cannot resolve is quoted on the scope line by its first line only; the coverage look still carries the whole failure.
|
|
18
|
+
|
|
19
|
+
No command loses its output to a panic when the reader has gone away. `println!` and `eprintln!` abort on a refused write, so `oakum version | head -1` exited 101 — neither ok, unverified, nor error under ADR-0034 — with its files already written and no account of them, and every other command shared the hazard. 0.3.0 said both channels went through a write that reports failure rather than panicking; that was true of `check` alone.
|
|
20
|
+
|
|
21
|
+
Every command now prints through writers that do not abort, and they differ by what the line is. A report — a `check:` scope line, a `pending:` plan, a migration step — survives a refusal in silence, because a reader who went away chose not to read. A result is the command's return value: the path `add` wrote, the URL `release` or `ci version-pr` opened, the plan `status` prints and the JSON `status --json` and `plan-intent` produce, the body `generate --dry-run` composes, the listings `reachable-tags` and `detect-release-tools` are, the summary `version` or `upgrade` gives of what it changed, the `created` records and the workflow `init` and `migrate` hand over, and the plan `ci pr-status` falls back to stdout when it has neither a token nor a job summary. A result nobody received is a run that did not answer. It exits 2, `unverified` under ADR-0034: the work landed and the answer did not, the same shape as a `migrate` that applied every file and could not verify them. Stderr says what already happened, so a caller that captured an empty stdout under `set -o pipefail` is told rather than left to notice. A descriptor the caller closed outright (`>&-`) is not a reader who went away: the standard library reports that write as delivered, and the run exits 0.
|
|
22
|
+
|
|
23
|
+
`init` and `migrate` finish every write before reporting a refused record, so a reader that went away does not leave `.changeset` half-written, and the verdict names each line that never arrived, even when a plan-comparison finding outranks it for the exit code; a write that fails after such a refusal, a gate look that fails beside it, or a comparison that could not run is named in the same verdict. An interactive prompt propagates the refusal because a question that cannot be shown cannot be answered. A `release` stopped by such a refusal keeps that class through its partial-failure report rather than reading as an error, and that report lists a tag whose GitHub release exists as `released` and not as remaining. A test fails the build naming any command module that reaches for the macros again.
|
|
24
|
+
|
|
25
|
+
`oakum ci pr-status` identifies the version pull request by the same four facts the scaffolded workflow tests — head branch, head repository, and the type of both the author and the sender — where it read the branch name alone. A fork or a collaborator naming a branch `oakum/version-packages` was treated as the version pull request, which suppressed the coverage comment on their own change. The cost of the other mistake is a redundant comment on the real version pull request, so an event that cannot be read counts as not the version pull request.
|
|
26
|
+
|
|
27
|
+
`oakum ci pr-status` reads `.changeset/_config.toml` once per run. It read it twice, once to choose the channel and once more inside the state builder to compose the plan, so a config edited between the two reads could choose a channel with one config and build the plan with another. `status`, `ci pr-status`, and `version` share one plan pipeline; `version` carried its own copy of it.
|
|
28
|
+
|
|
29
|
+
A release assembled from a bump file that declares more than one section renders each under its own heading. A note's first line was the only one read for a section name, so a file carrying `### Added` and `### Changed` was filed under the first and printed the second inside that block; 0.3.0's own changelog repeats `### Changed` and `### Fixed` for that reason. A known heading now starts a section wherever it appears, at up to three columns of indentation — the rule CommonMark applies and the one oakum's fence reader already used — while a fourth column is code, an unknown heading such as `### Notes` stays body text, and a heading inside a fenced block stays content. A heading with nothing but whitespace under it renders no section rather than a heading over two spaces. A template iterating `changes` or `notes` sees the same correction: a bump file's second section is its own entry, sharing that file's commit, pull request and author, rather than a stray heading buried inside the first entry's body. The rows a template rendered before were the bug, not a contract it can keep. MD024 stays disabled repo-wide, because specs and decision records repeat headings by design; the rendered changelog asserts the rule itself instead.
|
|
30
|
+
|
|
31
|
+
A `version` run that fails partway names every file it left changed, as a list. Rollback appended `; restoring X also failed (...)` to the cause once per file, which buried the last entry of a chain, and the one run where the tree is left half-written was the one that said least about it. The failure now prints the cause, a count, and one path per line. Two states that were silent are now named as files left changed: a staging file the run created and could not remove, and a file the run created and could not remove after the write itself failed. A directory the sweep could not read is named too, on its own line and under its own heading, because it is no evidence of a changed file — the fault that strands a file is the kind that also blocks the listing, so silence would hide a leak exactly when there is one, while counting it as damage sends a reader hunting for a half-written tree that may be intact. A staging file is attributed to this run by a delimited process id rather than a substring match, so a concurrent run's file is not named as this failure's; the wording holds either way. An empty list is documented for what the code checks — every restore reported success and every swept directory could be read — and no longer claims a byte-identical tree, which rollback does not verify because it writes the planned original back without re-reading it.
|
|
32
|
+
|
|
33
|
+
`version` writes a file it planned to create with an empty body, where the unchanged-text skip dropped the write and reported success over a file that never appeared. No caller produces one today.
|
|
34
|
+
|
|
35
|
+
`version` no longer loses its summary to a panic when the reader has gone away. `println!` aborts on a refused write, so `oakum version | head -1` exited 101 with the manifests, changelogs and bump files already written and no account of any of it — on the one command that performs the irreversible part of a release. The summary now goes through the same write that `check` was given in 0.3.0, which reports a refusal rather than panicking. Other commands still print directly, which is tracked separately.
|
|
36
|
+
|
|
37
|
+
## 0.3.0 (2026-09-12)
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
`migrate` derives `tag-format` from the tags a repository already carries, so a cutover from a tool that tags `name@version` no longer meets oakum's `name/v1.0.0` default at the first release. Derivation is structural rather than a list: a shape is written only when one template explains every tag, and only when oakum's own tag reader can read that shape back. Tags that disagree, or that no split explains, write nothing and are named among the remaining steps, because refusing beats cutting a tag the repository does not use; the shapes this repository could adopt are listed beside the ask, since which one is right depends on the tags it could not reconcile. That list drops any shape the workspace cannot use — above one tag-managed package the bare `v{{ version }}` is absent, because a release reads such a tag as leftover ambiguity, and the step that refuses a bare shape must not then offer it. A repository whose tags cannot be read is told so rather than treated as having none.
|
|
42
|
+
|
|
43
|
+
`migrate` also names the install pin among its remaining steps when the repository has none, quoting the version it just wrote to `tool-version` and the install command for the ecosystem it detected — the same one the workflow it prints below uses, rather than both ecosystems' commands regardless. Without a pin every later oakum command refuses, and a reader who installed globally would otherwise meet that refusal with the migration already applied.
|
|
44
|
+
|
|
45
|
+
A migration guide, at [docs/guide/migrating.md](https://github.com/oakoss/oakum/blob/main/docs/guide/migrating.md). It covers which source settings carry and which do not — and the three different reasons a setting does not carry, since "no counterpart, nothing lost" and "no counterpart, but your release commits change author" want different responses from a reader. It puts the remaining steps in dependency order rather than in the order they print, because pinning the binary before pasting the workflow is the difference between a clean cutover and meeting a refusal with the migration already applied.
|
|
46
|
+
|
|
47
|
+
It also names the one part of a cutover that cannot be ported: a pipeline built on a plan/act split, where one job computes a mode and downstream jobs run guarded by `--expect-mode`, has no equivalent here. oakum has no plan step — `ci version-pr` and `release` each derive their own precondition from repository state when they run — so there is no window between deciding and acting to guard. That is a different safety argument rather than a weaker one, but it means the workflow is rewritten, not translated.
|
|
48
|
+
|
|
49
|
+
The guide leads with the acceptance test, which is **`oakum status` names every package you expect to release** — not "`check --strict` exits 0". One real cutover was given the second and satisfied it immediately with a config that managed nothing. oakum has since closed the specific hole that cutover fell into: a config whose only packages are private now exits 2 and names the fix. One shape still passes deliberately — an `include`/`exclude` that selects nothing exits 0, because emptying a selection is a decision the config states and a gate must not refuse one. The argument is narrower for that, and it still holds: a plan is a positive statement about what will happen, a gate exiting 0 is the absence of a complaint, and reading the plan needs no guard to have been written in advance for the particular way your config is wrong.
|
|
50
|
+
|
|
51
|
+
`oakum migrate` now names what the repository can retire, instead of leaving it to be found in the schema. `extra-files` declares a JSON file that carries a version — a plugin manifest, a marketplace entry — and `oakum version` writes it in the same pass as the manifest and under the same rollback, which retires a sync script, its drift job and its tests. v1 writes JSON only, so a chart or a README badge is refused at config parse rather than written. The reporter who found this called it the strongest argument for the migration, and found it by noticing a phrase in `oakum version --help` and then reading `_schema.json`.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
The GitHub Actions guide says why the printed workflow carries no `concurrency:` block, and does not tell you to omit one. Whether a group is load-bearing depends on whether the pipeline carries a decision between jobs: one that does needs protecting from a cancellation window, and one that re-derives its preconditions per command does not. This repository's own workflow carries a group and the printed one does not, and both are correct for that reason. The earlier guidance would have generalised another tool's failure without checking whether the mechanism carries over.
|
|
56
|
+
|
|
57
|
+
`status` reports a changed package the config keeps but cannot version — a private package where `private-packages.version` is not set, so no bump file could ever cover it. It appears in the rendered summary and as an `unmanaged` array in `--json`, and it does not change the exit code. `check` stays silent on that state on purpose: `status` reports, `check` decides, and there is nothing here to decide.
|
|
58
|
+
|
|
59
|
+
`status --json` reports coverage for real. It emitted `uncovered: []` for every repository, whatever had changed, because the command never ran the coverage look — only `check` and `ci pr-status` did. `schema_version` is deliberately unchanged: the new fields are additive and an old parser sees the shape it always saw, but a reader that treated an empty `uncovered` as a clean result should now read `coverage`, which says what happened to the look — `ran`, `failed`, or `not-asked`. The third is not a hedge: a report rendered from a plan already in hand, such as the Version Packages body, never asks the question, and saying the look failed there would blame git for something nobody attempted. Where git cannot diff the tree, `status` says so in the rendered summary as well as the JSON, since stderr does not reach a step summary or a pull-request body.
|
|
60
|
+
|
|
61
|
+
`status` distinguishes a repository with nothing pending from one whose config manages no package on either axis — every selected package private, with the opt-in unset. Both printed `No packages planned.` and exited 0; the second now says so, and carries `manages_nothing` in its JSON. A selection that `include`/`exclude` empties is not covered by this and stays silent, in `status` as in `check`, because an empty selection is a decision someone wrote down. `ci pr-status` reports the same facts, and no longer stays silent on such a repository — it planned nothing and covered nothing, so the early return swallowed the one thing worth saying.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
`check` refuses when no selected package can produce work on either the version or the tag axis, where it exited 0 in silence. That state plans nothing and releases nothing while every gate reports green, and it is what a migration produces when the private-packages opt-in is lost. The refusal names both keys. A selection left empty by `include`/`exclude` stays silent, because that is a decision someone wrote down, while an absent `private-packages` key is the absence of one. `release` does not ask, so a release with nothing to do still says so in its own words.
|
|
66
|
+
|
|
67
|
+
`init` says the same thing where it writes such a config, rather than leaving it to the next command: an all-private workspace still gets its files and a zero exit, with the guidance on stderr.
|
|
68
|
+
|
|
69
|
+
`check` refuses a bump file that names a package it cannot version, matching what `status` and `version` already did. It asked a narrower question than the pipeline — it let an `include`/`exclude` exclusion through — so a repository could pass a green `check` and then have its version PR fail on the same file. If your `check` is green today on a bump file naming an excluded package, it will now fail, and so would the release that followed it. The refusal names every offending package and prints before returning, so a failure in an earlier look, such as a missing install pin, no longer erases it.
|
|
70
|
+
|
|
71
|
+
The coverage look refuses a shallow clone instead of reporting a clean result. At depth 1 the default base resolves to the checked-out commit itself, so the diff came back empty and every changed package looked covered. `actions/checkout` clones that way unless told otherwise, which made this the common CI shape rather than an edge case. `check` already refused such a clone through its tag look; `ci pr-status` did not, and now does, naming `fetch-depth: 0`. `status` reports it rather than refusing, because `status` is not a gate.
|
|
72
|
+
|
|
73
|
+
`check` stays silent on a changed package the config keeps but cannot version — an unpublishable package where `private-packages.version` is not set. That is a choice someone made, not a fault, and gating on it would turn every private-package change red until they wrote config; [ADR-0027](https://github.com/oakoss/oakum/blob/main/docs/decisions/0027-private-packages-version-opt-in.md) records that silence as something a changesets migratee keeps without a config change. `status` reports it instead — see the Added note.
|
|
74
|
+
|
|
75
|
+
`migrate` no longer calls an empty plan comparison a match. With nothing pending on either side it printed `0 package(s) planned by bumpy and by oakum; match`, which reads as verification of a transform that nothing exercised. That is the common case rather than the rare one, since a repository is usually migrated right after a release.
|
|
76
|
+
|
|
77
|
+
`oakum check` says what it covers, on every run. It exited 0 with no output at all, so a check that inspected a two-package workspace and a check that inspected nothing were the same observation — and a config selecting no package read as a clean repository. Every run now opens with two lines on **stdout**: how many of the workspace's packages the selection keeps, the ref the diff is taken from, the looks this run asks for, and — separately — the looks it does not, because a look nobody asked for must not read as a look that passed. The lines state scope and intent, never a verdict: they print before the first look so that a run which refuses describes itself too, and what became of those looks is the exit code's to say — nothing on stderr and exit 0 means every announced look finished and passed. **Breaking for anything that asserted `check` writes nothing to stdout**; the refusals themselves are unchanged and still land on stderr. Every look's refusal now reaches stderr, too: the looks are run together and then raised in order, so the second and third used to be discarded — a `--remote` run against a repository with no remotes lost that finding entirely whenever an earlier look refused first. And the base ref the report names is confirmed before it is quoted, so an explicit `--from` naming a ref git does not have is reported as that rather than announced and then failed on.
|
|
78
|
+
|
|
79
|
+
When a run refuses on more than one look, the refusal that sets the exit code is the one oakum measured, not the one written first. `check` runs its looks together and then raises one; it raised whichever came first in the source, so an unrelated leftover staging file turned a measured tag drift from `error` (exit 1) into `unverified` (exit 2) and erased the `error:` line from stderr — ADR-0034's split run backwards. A finding now outranks a look that did not happen ([ADR-0035](https://github.com/oakoss/oakum/blob/main/docs/decisions/0035-a-finding-outranks-an-unverified-look.md)), and every other refusal is still reported, prefixed `also`, so a reader can see which line the exit code came from. **Breaking for anything branching on `1` versus `2`**: a repository with both a finding and an unverified look now reports the finding. A base ref git does not have is `unverified`, not an error — a shallow CI checkout passing `--from` is the shape that hits it.
|
|
80
|
+
|
|
81
|
+
Neither a report nor a refusal that cannot be written replaces the outcome with a panic. `eprintln!` aborts when the write is refused, so a run whose stderr reader had gone away exited 101 and printed nothing at all, in place of the exit code ADR-0034 defines. Both channels now go through a write that reports failure rather than panicking — `oakum check | head -1` is an ordinary invocation, and the report this round added made it an exit-101 one.
|
|
82
|
+
|
|
83
|
+
An unverified outcome prints one outcome token on stderr, not two. Every failure was prefixed `error: `, and an unverified message already begins `unverified: `, so stderr read `error: unverified: …` — two words saying different things about one result. Both channels now read one classification of the variant, so the prose and the numeric channel (ADR-0034: `2` for unverified) cannot disagree — two exhaustive matches would have let a variant be filed one way for the shell and another for stderr. **Breaking for anything matching `error:` on an unverified failure**; it now matches `unverified:`.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
An unknown configuration key is named, fully qualified as TOML resolved it. A scalar written below a table header is scoped into that table, so `commit-message` typed at what looks like the top level is read as `private-packages.commit-message` — and the error gave a line number with nothing on that line to look for. The name comes from a span-preserving parse rather than from reading the source text, so a header carrying a trailing comment is still that key's table, a key inside an inline table is named at its own depth, a bracketed line inside a string value is not mistaken for a header, and a header is named the way TOML resolved it — spacing dropped, but a name that genuinely needs quotes keeping them, so the path is one the document holds. A key the parse cannot place keeps the message it always had, and a document that omits a required key is reported as that rather than as `invalid TOML syntax`, which sent readers hunting a typo in TOML that parses perfectly. Values are still never echoed, and neither is the source line.
|
|
88
|
+
|
|
89
|
+
A template that reads an undefined variable names it, and names the variables in scope. `tag-format = "{{ name }}"` failed with `undefined value (in tag-format:1)`, which named neither the offending variable nor the correct one — `package`, findable only by reading oakum's source. Both halves are derived: the unsupplied names are the template's own undeclared variables minus what the context supplies and minus the renderer's globals, and the scope is the context's own keys. It is stated as a set — "this template reads `name`, which the context does not supply" — because that is what the analysis knows: it reports every name the template reads anywhere and cannot say which branch ran, so calling one of them the cause of this failure would claim more than was established. A nested read is resolved against the context rather than matched against its top-level keys, so `repo.url` is not accused of being missing and `repo.nope` still names itself. A context that is not a map says so rather than reporting no variables at all. This covers every rendered surface, not just `tag-format`. The four template keys in `_config.toml`'s schema now name the variables each surface receives, and a test ties each list to the context that surface actually renders with.
|
|
90
|
+
|
|
91
|
+
A git operation that failed for a non-ssh reason no longer reports an ssh configuration problem. oakum resolves the ssh transport once and puts it on every child, including local ones; when that probe failed, every operation was reported as needing `an ssh configuration oakum could not read`, with the real cause in parentheses and a remedy measured to move the failure rather than fix it — including when git was not installed. Four outcomes are now kept apart: a repository git will not open is reported as itself; an ssh configuration git would not give up keeps the skip-the-probe remedy; an ssh *variable* oakum cannot read is told to repair or unset that variable, rather than being answered with advice to set it; and a probe that never reached git at all claims no cause, because offering one would be a diagnosis nobody made. A child that died on a signal counts as the last of those: it establishes nothing about ssh or about the repository. The second probe is also asked only when the first one ran and refused, which halves the worst-case wait on a git that hangs.
|
|
92
|
+
|
|
93
|
+
`oakum status --help` says which render a bare `status` produces. It described the command as printing `JSON or a named render`, which reads as JSON being the default; it is the `summary` render, and the reporter ran both commands to find out.
|
|
94
|
+
|
|
95
|
+
`oakum migrate` names `gitUser` as a step rather than a forgettable dropped key. It decided who authored every release commit and tag; oakum has no counterpart, so a cutover reassigned authorship silently. Both the step and the printed workflow now say what that identity actually governs, because the two halves differ: annotated tags take the `git config user.name` / `user.email` the release job sets, while the version commit is written through the GitHub API and carries the token's own account, which no git config can change. A reader swapping in a GitHub App token — which a bot push needs to retrigger CI — changes the commit author whatever they configure.
|
|
96
|
+
|
|
97
|
+
A failed bump-file gate look states one fact. The printed step and the `unverified` refusal were two separately-authored sentences about one outcome, each free to drift from the other; they still read differently, because one is a bullet and one sets the exit code, but the fact they state now comes from one place. Which outcome refuses is a property of the look rather than a `match` repeated at each site.
|
|
98
|
+
|
|
99
|
+
An outcome oakum could not verify now exits `2`. Every failure exited `1`, which put "this repository failed the check" and "the check could not be run" behind the same number — the collapse the three-outcome rule exists to prevent, surviving in the one channel a caller reads without parsing prose. The three outcomes are now `0` ok, `2` unverified, `1` error, decided in one place and applied by every command that can report one. Every exit stays non-zero, so `set -e`, `if ! oakum check`, and a GitHub Actions step all behave as before; a script comparing the code against `1` exactly does not. See [ADR-0034](https://github.com/oakoss/oakum/blob/main/docs/decisions/0034-exit-two-for-unverified.md).
|
|
100
|
+
|
|
101
|
+
`migrate` is where this was costing the most. A cutover that wrote every file correctly and merely could not run the source tool to compare against reported `error: unverified: migrated files were kept` and exited `1` — indistinguishable from a migration that failed, on the path the install docs produce.
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
`migrate` carries a source tool's `privatePackages` into oakum's `private-packages`, where it dropped it. A repository whose packages are all private migrated to a config that managed nothing, so `check` exited 0, `status` planned nothing, and a release would have done nothing while reporting success. Changesets' bare-boolean form is read the way changesets reads it, as both axes. Both source files are read when present and their axes are unioned, so a repository migrating from one tool while a readable config from the other is still on disk carries both; the report names what each file contributed and the one line the write produces. A bumpy source also gets the "not carried over" report the changesets path already had. A source config oakum cannot open, read, or parse, or one whose axis is not a boolean, is reported and skipped rather than stopping the migration: both tools accept JSON their own loaders tolerate and `serde_json` does not, so a leftover config from an abandoned experiment no longer aborts an unrelated migration. A dangling symlink at either path is named as one rather than read as the file being absent.
|
|
106
|
+
|
|
107
|
+
`migrate` looks for the source tool in `node_modules/.bin` before giving up on it. `npm i -g` and Homebrew put `oakum` on `PATH` and a devDependency's binaries nowhere near it, so the install the docs recommend left the parity check unrunnable — on the one command whose whole job is comparing against that tool. The comparison then ran oakum against oakum, which cannot detect a mapping error, and said `match` about it. A source tool that really is absent now says where oakum looked, because installing globally and installing locally fix different halves of that. A source tool that ran and failed is no longer mistaken for one that answered: a crash whose output happens to parse is not an empty plan, a child the kernel killed is reported as such rather than as `exited -1`, and a difference measured against a plan read under a tool's exit-1 convention is unverified rather than proof the transform changed anything.
|
|
108
|
+
|
|
109
|
+
A tag that states no version — `v1`, `latest`, `nightly`, a date stamp — no longer cancels the whole tag-format derivation. `release` already classifies such a tag as someone else's and skips it, so one `v1` (the GitHub Actions convention) turned the derivation off on exactly the histories it was written for, and the two commands disagreed about the same tag. A history of nothing but moving tags is still reported, naming them: it has tags, and calling that "no tags" would say never released about a repository nobody managed to read.
|
|
110
|
+
|
|
111
|
+
`migrate` says which `versioning` mode it wrote and what settled it. `semver` is the non-default, and for a repository whose packages are all below 1.0.0 it is the difference between the next release being `0.18.0` and `1.0.0` — written silently until now. knope and release-plz imply `zero-major` instead; release-plz's own configuration documentation states the rule, and it had been grouped with the tools that take `0.1.3` to `1.0.0`.
|
|
112
|
+
|
|
113
|
+
A bump file copied out of the old tool's directory is described as a copy rather than a rewrite, and every original left behind is named among the remaining steps. oakum does not own that directory and will not delete from it, but the old tool goes on counting those files: measured in a clone, after `migrate` and `version` released a package 0.17.0 to 0.18.0, the old tool still showed three pending and would have released it again. A repository whose workflow still runs the old tool on every push to `main` gets that second release on the merge commit.
|
|
114
|
+
|
|
115
|
+
`migrate` names what in a repository refers to the old tool's bump-file directory, so a commit hook or CI step pointed at it can be repointed before it rejects the bump files oakum writes — which is what happened in the first repository to adopt oakum. oakum cannot tell a gate from a mention in prose, so the step asks the reader to look rather than asserting each match is a gate, and it says what it did not search: the look reads git's index outside `.changeset/`, so an untracked file, an unstaged edit, a submodule and `.git/hooks/` are all uncovered. A look that fails says so and exits unverified rather than passing for no gate. Only a bumpy cutover has an old directory to repoint anything at; changesets and knope already keep bump files in `.changeset/`, which oakum adopts in place.
|
|
116
|
+
|
|
117
|
+
The plan `migrate` prints is sorted. It came out in filesystem order, so the same repository could show a reader a differently ordered plan on a different machine.
|
|
118
|
+
|
|
119
|
+
A difference the parity check found outranks a look that could not happen. The first is a finding and the second is missing evidence, so a migration that proved the release plan changed reports that, at exit 1, even when the gate look also failed.
|
|
120
|
+
|
|
121
|
+
`oakum version` says what it wrote. The command that performs the irreversible part of a release — manifests, lockfile rows, changelogs, declared `extra-files`, and the bump files it consumes — exited 0 having said nothing at all, so a run that rewrote five files was indistinguishable from one that did nothing. It now names each package with its old and new version, says which package triggered a cascade, lists every file whose bytes changed, and names every bump file consumed. Printed after the writes land, so it reports rather than promises. `ci version-pr` plans the same writes through a different path and its output is unchanged.
|
|
122
|
+
|
|
123
|
+
The `.changeset/README.md` oakum writes no longer documents a bump-file form the parser refuses. It said oakum reads a quoted bare package name; `status`, `check` and `version` all refuse one, naming the file — `docs/specs/bump-files.md` has always listed quoted keys as not permitted. Since `init` and `migrate` write that README into the repository, the tool was teaching a rule it then rejected people for following. The corrected text also records that `migrate` rewrites the quoted form `@changesets/cli` writes, so a cutover needs no hand-editing.
|
|
124
|
+
|
|
125
|
+
`status` reports a config whose `include`/`exclude` leave no package selected. Such a config can never release anything, and both verbs said only `No packages planned.` — the same sentence a quiet repository prints. `check` still stays silent, because emptying a selection is a decision the config states and a gate must not refuse one; `status` is not a gate, so it says what the reader is looking at, and says that `check` will not refuse it. `status --json` carries `selection_empty` beside the existing `manages_nothing`; the two are exclusive, and a document asserting both, or asserting either beside a planned release, reads as neither.
|
|
126
|
+
|
|
127
|
+
`ReleaseState::manages_nothing` is no longer `const`, and the new `selection_empty` accessor is not `const` either: both read `packages.is_empty()`, which is not a const operation. A caller using either in a const context breaks; nothing in oakum does, and the library's `pub` surface is the seam between it and the binary rather than a curated API.
|
|
128
|
+
|
|
129
|
+
`migrate` carries `versionCommitMessage` into `commit-message`, an exact equivalent it previously left for the reader to restore by hand — and says so before writing it, beside the other settings it carries. A message equal to what oakum writes anyway is not written as a config line restating a default, and the run says that rather than reporting a loss that did not happen. A message oakum cannot carry is named with the reason: `commit-message` is rendered as a template, so a literal containing `{{ … }}` would fail the version PR or quietly become a different message, and a control character has no place in a commit headline; a newline ends the config's string form outright. `changelog` is named as a decision the reader owes rather than translated, because it means different things in each source tool and neither is oakum's `template`.
|
|
130
|
+
|
|
3
131
|
## 0.2.0 (2026-09-10)
|
|
4
132
|
|
|
5
133
|
### Added
|
|
@@ -72,4 +200,4 @@ Release workflow checks out the homebrew tap in a subdirectory so the composite
|
|
|
72
200
|
|
|
73
201
|
Self-host release CI uploads cargo-dist artifacts into the GitHub Release oakum already created, instead of calling create again.
|
|
74
202
|
|
|
75
|
-
Generated by oakum 0.
|
|
203
|
+
Generated by oakum 0.3.0.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"hasInstallScript": true,
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"name": "@oakoss/oakum",
|
|
22
|
-
"version": "0.
|
|
22
|
+
"version": "0.3.1"
|
|
23
23
|
},
|
|
24
24
|
"node_modules/detect-libc": {
|
|
25
25
|
"engines": {
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"requires": true,
|
|
51
|
-
"version": "0.
|
|
51
|
+
"version": "0.3.1"
|
|
52
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"artifactDownloadUrls": [
|
|
3
|
-
"https://github.com/oakoss/oakum/releases/download/v0.
|
|
3
|
+
"https://github.com/oakoss/oakum/releases/download/v0.3.1"
|
|
4
4
|
],
|
|
5
5
|
"author": "Jace Babin",
|
|
6
6
|
"bin": {
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"zipExt": ".tar.xz"
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
|
-
"version": "0.
|
|
108
|
+
"version": "0.3.1",
|
|
109
109
|
"volta": {
|
|
110
110
|
"node": "18.14.1",
|
|
111
111
|
"npm": "9.5.0"
|