versionary 0.2.0 → 0.3.0
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 +187 -11
- package/dist/{simple → app/release}/pr.d.ts +7 -4
- package/dist/{simple → app/release}/pr.js +89 -74
- package/dist/app/release/recovery.d.ts +17 -0
- package/dist/app/release/recovery.js +101 -0
- package/dist/{simple → app/release}/release.js +37 -24
- package/dist/app/release/state.d.ts +10 -0
- package/dist/{simple → app/release}/state.js +16 -4
- package/dist/app/release/verify.d.ts +2 -0
- package/dist/app/release/verify.js +5 -0
- package/dist/cli/index.js +12 -10
- package/dist/config/load-config.js +2 -12
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +1 -0
- package/dist/domain/release/changelog.d.ts +12 -0
- package/dist/domain/release/changelog.js +123 -0
- package/dist/{simple → domain/release}/plan.d.ts +4 -3
- package/dist/{simple → domain/release}/plan.js +32 -15
- package/dist/domain/release/semver.d.ts +15 -0
- package/dist/domain/release/semver.js +111 -0
- package/dist/domain/strategy/node.d.ts +2 -0
- package/dist/domain/strategy/node.js +37 -0
- package/dist/domain/strategy/resolve.d.ts +3 -0
- package/dist/domain/strategy/resolve.js +11 -0
- package/dist/domain/strategy/simple.d.ts +2 -0
- package/dist/domain/strategy/simple.js +28 -0
- package/dist/domain/strategy/types.d.ts +7 -0
- package/dist/domain/strategy/types.js +2 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -1
- package/dist/infra/git/commits.d.ts +65 -0
- package/dist/infra/git/commits.js +436 -0
- package/dist/infra/scm/github/plugin.d.ts +1 -0
- package/dist/infra/scm/github/plugin.js +5 -0
- package/dist/infra/scm/runtime.d.ts +2 -0
- package/dist/infra/scm/runtime.js +24 -0
- package/dist/infra/scm/types.d.ts +1 -0
- package/dist/infra/scm/types.js +2 -0
- package/dist/plugins/runtime.d.ts +1 -2
- package/dist/plugins/runtime.js +3 -22
- package/dist/scm/github-plugin.js +133 -40
- package/dist/strategies/node.d.ts +1 -0
- package/dist/strategies/node.js +5 -0
- package/dist/strategies/resolve.d.ts +1 -0
- package/dist/strategies/resolve.js +5 -0
- package/dist/strategies/simple.d.ts +1 -0
- package/dist/strategies/simple.js +5 -0
- package/dist/strategies/types.d.ts +1 -0
- package/dist/strategies/types.js +2 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/plugins.d.ts +1 -0
- package/dist/verify/verify-project.js +3 -1
- package/package.json +1 -1
- package/dist/simple/changelog.d.ts +0 -3
- package/dist/simple/changelog.js +0 -46
- package/dist/simple/git.d.ts +0 -10
- package/dist/simple/git.js +0 -114
- package/dist/simple/semver.d.ts +0 -8
- package/dist/simple/semver.js +0 -25
- package/dist/simple/state.d.ts +0 -3
- /package/dist/{simple → app/release}/release.d.ts +0 -0
- /package/dist/{simple → infra/git}/repo-url.d.ts +0 -0
- /package/dist/{simple → infra/git}/repo-url.js +0 -0
package/README.md
CHANGED
|
@@ -1,9 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Versionary
|
|
2
2
|
|
|
3
3
|
Versionary is a software-agnostic automated release tool focused on SemVer,
|
|
4
4
|
conventional commits, release PR workflows, and extensibility.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
## Why this exists
|
|
7
|
+
|
|
8
|
+
Versionary is designed as a practical middle ground between `semantic-release`
|
|
9
|
+
and `release-please`.
|
|
10
|
+
|
|
11
|
+
- Like `semantic-release`, it supports direct release execution.
|
|
12
|
+
- Like `release-please`, it supports a release PR workflow so maintainers can
|
|
13
|
+
preview and review changes before publication.
|
|
14
|
+
|
|
15
|
+
The core idea is to keep versioning, changelog generation, tagging, and SCM
|
|
16
|
+
release metadata in one tool, while leaving package publication (npm, crates.io,
|
|
17
|
+
etc.) to dedicated CI workflows triggered by tags or releases.
|
|
18
|
+
|
|
19
|
+
## Product direction
|
|
20
|
+
|
|
21
|
+
Versionary is being built to:
|
|
22
|
+
|
|
23
|
+
- support both direct releases and release-PR-gated releases
|
|
24
|
+
- work across repository types (Node, Rust, docs/LaTeX, etc.)
|
|
25
|
+
- stay SCM-agnostic at the core, with integrations via plugin capabilities
|
|
26
|
+
(GitHub first; GitLab/Codeberg later)
|
|
27
|
+
- keep a small, stable core with explicit extension points
|
|
28
|
+
- handle trunk-based development and monorepo workflows cleanly
|
|
29
|
+
|
|
30
|
+
## Scope and non-goals
|
|
31
|
+
|
|
32
|
+
In scope:
|
|
33
|
+
|
|
34
|
+
- semantic version planning from commits
|
|
35
|
+
- changelog generation
|
|
36
|
+
- release PR automation
|
|
37
|
+
- tags + SCM release metadata (e.g. GitHub Releases)
|
|
38
|
+
|
|
39
|
+
Out of scope (intentional):
|
|
40
|
+
|
|
41
|
+
- publishing artifacts to language registries
|
|
42
|
+
- replacing package-specific publish tooling
|
|
43
|
+
|
|
44
|
+
Use your CI/CD platform for registry publishing, triggered from a created
|
|
45
|
+
release/tag.
|
|
46
|
+
|
|
47
|
+
## Current status vs roadmap
|
|
48
|
+
|
|
49
|
+
Current implementation focuses on:
|
|
50
|
+
|
|
51
|
+
- strategy-based version updates (`simple`, `node`)
|
|
52
|
+
- release planning and changelog generation
|
|
53
|
+
- review-mode vs direct-mode release flow
|
|
54
|
+
- built-in GitHub SCM plugin capabilities
|
|
55
|
+
|
|
56
|
+
Planned/harder areas include deeper monorepo ergonomics, broader SCM coverage,
|
|
57
|
+
and stronger failure recovery around release steps.
|
|
58
|
+
|
|
59
|
+
## Architecture layout (current migration)
|
|
60
|
+
|
|
61
|
+
The repository is moving to explicit layered modules:
|
|
62
|
+
|
|
63
|
+
- `src/app/`: command-level application services and orchestration boundaries
|
|
64
|
+
- `src/domain/`: release and strategy domain logic/contracts
|
|
65
|
+
- `src/infra/`: platform integrations (SCM, git/runtime adapters)
|
|
66
|
+
|
|
67
|
+
Legacy `src/simple/` has been removed. Remaining compatibility paths are
|
|
68
|
+
`src/strategies` and `src/scm` while migration is finalized.
|
|
69
|
+
|
|
70
|
+
Configuration is loaded from `versionary.jsonc` by default (or
|
|
71
|
+
`versionary.json`).
|
|
7
72
|
|
|
8
73
|
Schema URL for editor support:
|
|
9
74
|
|
|
@@ -15,16 +80,40 @@ For a quick trial, use:
|
|
|
15
80
|
|
|
16
81
|
- `version-file` (default `version.txt`) as version source
|
|
17
82
|
- `changelog-file` (default `CHANGELOG.md`) as release notes output
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
83
|
+
- `release-type: "node"` uses `package.json` as version source and updates it
|
|
84
|
+
during release PR prep
|
|
85
|
+
- simple/default strategy keeps `version.txt` as source of truth and does not
|
|
86
|
+
update `package.json`
|
|
87
|
+
- stable release branch (`release-branch`, default: `versionary/release`) so
|
|
88
|
+
release PRs are updated in-place
|
|
89
|
+
- `baseline-file` (default `.versionary-manifest.json`) tracks baseline SHA for
|
|
90
|
+
deterministic commit ranges independent of tags
|
|
91
|
+
- pre-1.0 policy defaults to conservative major handling: for `0.y.z`, breaking
|
|
92
|
+
changes bump to `0.(y+1).0`; set `allow-stable-major: true` to allow explicit
|
|
93
|
+
auto-transition to `1.0.0` on a breaking release
|
|
94
|
+
- review mode (`review-mode`): `review` (PR/MR style) or `direct` (no review
|
|
95
|
+
request)
|
|
24
96
|
- optional monorepo planning with `monorepo-mode` and `packages`:
|
|
25
97
|
- `independent` computes package bumps per path
|
|
26
98
|
- `fixed` computes one shared bump across configured package paths
|
|
27
99
|
|
|
100
|
+
## Commit parsing and release analysis
|
|
101
|
+
|
|
102
|
+
Release planning is based on Conventional Commit parsing semantics:
|
|
103
|
+
|
|
104
|
+
- parses type/scope/description from commit headers
|
|
105
|
+
- exposes structured parsed fields (`header`, `body`, `footer`, `type`, `scope`,
|
|
106
|
+
`description`, `notes`, `references`, `mentions`, `revert`)
|
|
107
|
+
- separates parser output from release policy mapping (`inferReleaseType*`)
|
|
108
|
+
- recognizes breaking changes from `!` and `BREAKING CHANGE` /
|
|
109
|
+
`BREAKING-CHANGE` footers
|
|
110
|
+
- maps release impact as `feat => minor`, `fix|perf => patch`, breaking => major
|
|
111
|
+
- treats `revert:` as non-releasable commits
|
|
112
|
+
- suppresses commits that are reverted within the analyzed release window so they
|
|
113
|
+
do not affect bump/changelog output
|
|
114
|
+
- emits parser diagnostics for malformed headers/footers/references and ambiguous
|
|
115
|
+
revert messages
|
|
116
|
+
|
|
28
117
|
Commands:
|
|
29
118
|
|
|
30
119
|
- `pnpm verify`
|
|
@@ -39,8 +128,23 @@ Commands:
|
|
|
39
128
|
via SCM plugin capability. `pnpm run` is the recommended CI entrypoint and
|
|
40
129
|
auto-dispatches between PR/update and release publish.
|
|
41
130
|
|
|
42
|
-
For first-run bootstrapping, set `bootstrap-sha` (similar to release-please).
|
|
43
|
-
the baseline state file.
|
|
131
|
+
For first-run bootstrapping, set `bootstrap-sha` (similar to release-please).
|
|
132
|
+
Subsequent runs use the baseline state file.
|
|
133
|
+
|
|
134
|
+
## Release retry and recovery behavior
|
|
135
|
+
|
|
136
|
+
Release publish (`pnpm release` or the publish path in `pnpm run`) is
|
|
137
|
+
idempotent by target tag:
|
|
138
|
+
|
|
139
|
+
- if a tag already exists, Versionary reuses it rather than recreating it
|
|
140
|
+
- if release metadata already exists for the tag (e.g., GitHub Release), it is
|
|
141
|
+
reused
|
|
142
|
+
- if a prior run created/pushed the tag but failed before metadata creation, a
|
|
143
|
+
rerun creates the missing metadata and proceeds
|
|
144
|
+
|
|
145
|
+
Versionary fails fast when recovery is unsafe (for example, local and remote
|
|
146
|
+
tags with the same name point to different SHAs). In these cases, the error
|
|
147
|
+
message includes remediation guidance so CI logs are actionable.
|
|
44
148
|
|
|
45
149
|
## Built-in plugins
|
|
46
150
|
|
|
@@ -48,8 +152,80 @@ Versionary ships with built-in SCM plugin support:
|
|
|
48
152
|
|
|
49
153
|
- `github` (default): review request + release metadata
|
|
50
154
|
|
|
155
|
+
### GitHub integration: env, permissions, and flow
|
|
156
|
+
|
|
157
|
+
Required environment for the built-in GitHub plugin:
|
|
158
|
+
|
|
159
|
+
- `GITHUB_REPOSITORY` (format: `owner/repo`)
|
|
160
|
+
- one token env var: `VERSIONARY_PR_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN`
|
|
161
|
+
|
|
162
|
+
Token precedence is:
|
|
163
|
+
|
|
164
|
+
- `VERSIONARY_PR_TOKEN` > `GH_TOKEN` > `GITHUB_TOKEN`
|
|
165
|
+
|
|
166
|
+
Minimum GitHub token/repo permissions for Versionary-managed metadata:
|
|
167
|
+
|
|
168
|
+
- release PR create/update flow: `contents: write`, `pull-requests: write`
|
|
169
|
+
- release metadata flow (GitHub Release create/read): `contents: write`
|
|
170
|
+
|
|
171
|
+
`review-mode` behavior:
|
|
172
|
+
|
|
173
|
+
- `review`: `pnpm run run` prepares/updates the release branch and creates or
|
|
174
|
+
updates a release PR
|
|
175
|
+
- `direct`: `pnpm run run` prepares/updates the release branch and skips review
|
|
176
|
+
request creation
|
|
177
|
+
|
|
178
|
+
Concise GitHub Actions examples:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
# 1) Release PR / update flow (run on push to default branch)
|
|
182
|
+
permissions:
|
|
183
|
+
contents: write
|
|
184
|
+
pull-requests: write
|
|
185
|
+
|
|
186
|
+
steps:
|
|
187
|
+
- uses: actions/checkout@v6
|
|
188
|
+
with:
|
|
189
|
+
fetch-depth: 0
|
|
190
|
+
fetch-tags: true
|
|
191
|
+
- uses: pnpm/action-setup@v6
|
|
192
|
+
- uses: actions/setup-node@v6
|
|
193
|
+
with:
|
|
194
|
+
node-version-file: .nvmrc
|
|
195
|
+
cache: pnpm
|
|
196
|
+
- run: pnpm install --frozen-lockfile --ignore-scripts
|
|
197
|
+
- run: |
|
|
198
|
+
git config user.name "github-actions[bot]"
|
|
199
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
200
|
+
pnpm run run
|
|
201
|
+
env:
|
|
202
|
+
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
# 2) Release publish flow after merge (release commit context)
|
|
207
|
+
permissions:
|
|
208
|
+
contents: write
|
|
209
|
+
|
|
210
|
+
steps:
|
|
211
|
+
- uses: actions/checkout@v6
|
|
212
|
+
with:
|
|
213
|
+
fetch-depth: 0
|
|
214
|
+
fetch-tags: true
|
|
215
|
+
- uses: pnpm/action-setup@v6
|
|
216
|
+
- uses: actions/setup-node@v6
|
|
217
|
+
with:
|
|
218
|
+
node-version-file: .nvmrc
|
|
219
|
+
cache: pnpm
|
|
220
|
+
- run: pnpm install --frozen-lockfile --ignore-scripts
|
|
221
|
+
- run: pnpm run run
|
|
222
|
+
env:
|
|
223
|
+
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
224
|
+
```
|
|
225
|
+
|
|
51
226
|
Package publication is intentionally out of scope in the current release flow.
|
|
52
|
-
Use separate CI workflows for publishing after Versionary has prepared/tagged
|
|
227
|
+
Use separate CI workflows for publishing after Versionary has prepared/tagged
|
|
228
|
+
the release.
|
|
53
229
|
|
|
54
230
|
## Install from GitHub
|
|
55
231
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SimplePlan } from "../../domain/release/plan.js";
|
|
2
|
+
import type { ParsedCommit } from "../../infra/git/commits.js";
|
|
2
3
|
export declare function splitSafeDirtyFiles(files: string[]): {
|
|
3
4
|
ignored: string[];
|
|
4
5
|
blocking: string[];
|
|
@@ -7,9 +8,11 @@ export declare function prepareSimpleReleasePr(cwd?: string): {
|
|
|
7
8
|
branch: string;
|
|
8
9
|
title: string;
|
|
9
10
|
version: string;
|
|
10
|
-
|
|
11
|
+
previousVersion: string;
|
|
12
|
+
commits: ParsedCommit[];
|
|
13
|
+
plan: SimplePlan;
|
|
11
14
|
};
|
|
12
|
-
export declare function renderSimpleReviewRequestBody(version: string, commits:
|
|
13
|
-
export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, commits:
|
|
15
|
+
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, cwd?: string): string;
|
|
16
|
+
export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null): Promise<string>;
|
|
14
17
|
export declare function pushReleaseBranch(cwd: string, branch: string): void;
|
|
15
18
|
export declare function isReleaseCommitMessage(subject: string): boolean;
|
|
@@ -9,16 +9,14 @@ exports.renderSimpleReviewRequestBody = renderSimpleReviewRequestBody;
|
|
|
9
9
|
exports.openOrUpdateSimpleReviewRequest = openOrUpdateSimpleReviewRequest;
|
|
10
10
|
exports.pushReleaseBranch = pushReleaseBranch;
|
|
11
11
|
exports.isReleaseCommitMessage = isReleaseCommitMessage;
|
|
12
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
14
12
|
const node_child_process_1 = require("node:child_process");
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const plan_js_1 = require("
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const load_config_js_1 = require("../../config/load-config.js");
|
|
15
|
+
const changelog_js_1 = require("../../domain/release/changelog.js");
|
|
16
|
+
const plan_js_1 = require("../../domain/release/plan.js");
|
|
17
|
+
const resolve_js_1 = require("../../domain/strategy/resolve.js");
|
|
18
|
+
const capabilities_js_1 = require("../../plugins/capabilities.js");
|
|
19
|
+
const runtime_js_1 = require("../../plugins/runtime.js");
|
|
22
20
|
const state_js_1 = require("./state.js");
|
|
23
21
|
const SAFE_DIRTY_FILES = new Set([
|
|
24
22
|
"pnpm-lock.yaml",
|
|
@@ -69,94 +67,111 @@ function ensureCleanWorktree(cwd) {
|
|
|
69
67
|
}
|
|
70
68
|
function prepareSimpleReleasePr(cwd = process.cwd()) {
|
|
71
69
|
const plan = (0, plan_js_1.createSimplePlan)(cwd);
|
|
70
|
+
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
71
|
+
const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
|
|
72
72
|
if (!plan.nextVersion) {
|
|
73
73
|
throw new Error("No releasable commits found. Nothing to open a release PR for.");
|
|
74
74
|
}
|
|
75
75
|
ensureCleanWorktree(cwd);
|
|
76
|
-
const
|
|
77
|
-
node_fs_1.default.writeFileSync(versionPath, `${plan.nextVersion}\n`, "utf8");
|
|
76
|
+
const updatedVersionFiles = strategy.writeVersion(cwd, loaded.config, plan.nextVersion);
|
|
78
77
|
const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
|
|
79
78
|
(0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
|
|
80
79
|
const branch = plan.releaseBranchPrefix;
|
|
81
80
|
const title = `chore(release): v${plan.nextVersion}`;
|
|
82
|
-
(0, node_child_process_1.execFileSync)("git", ["checkout", "-B", branch], {
|
|
83
|
-
|
|
81
|
+
(0, node_child_process_1.execFileSync)("git", ["checkout", "-B", branch], {
|
|
82
|
+
cwd,
|
|
83
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
+
});
|
|
85
|
+
const filesToAdd = [...new Set([...updatedVersionFiles, plan.changelogFile])];
|
|
86
|
+
(0, node_child_process_1.execFileSync)("git", ["add", ...filesToAdd], {
|
|
84
87
|
cwd,
|
|
85
88
|
stdio: ["ignore", "pipe", "ignore"],
|
|
86
89
|
});
|
|
87
|
-
(0, node_child_process_1.execFileSync)("git", ["commit", "-m", title], {
|
|
88
|
-
|
|
90
|
+
(0, node_child_process_1.execFileSync)("git", ["commit", "-m", title], {
|
|
91
|
+
cwd,
|
|
92
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
93
|
+
});
|
|
94
|
+
const releaseTargets = plan.packages
|
|
95
|
+
? plan.packages
|
|
96
|
+
.filter((pkg) => pkg.nextVersion)
|
|
97
|
+
.map((pkg) => ({
|
|
98
|
+
path: pkg.path,
|
|
99
|
+
version: pkg.nextVersion ?? "",
|
|
100
|
+
tag: pkg.path === "."
|
|
101
|
+
? `v${pkg.nextVersion ?? ""}`
|
|
102
|
+
: `${pkg.path.replaceAll("/", "-")}-v${pkg.nextVersion ?? ""}`,
|
|
103
|
+
notes: (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
104
|
+
currentVersion: pkg.currentVersion,
|
|
105
|
+
nextVersion: pkg.nextVersion ?? "",
|
|
106
|
+
commits: pkg.commits,
|
|
107
|
+
cwd,
|
|
108
|
+
}, { includeFooter: false }),
|
|
109
|
+
}))
|
|
110
|
+
: [
|
|
111
|
+
{
|
|
112
|
+
path: ".",
|
|
113
|
+
version: plan.nextVersion,
|
|
114
|
+
tag: `v${plan.nextVersion}`,
|
|
115
|
+
notes: (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
116
|
+
currentVersion: plan.currentVersion,
|
|
117
|
+
nextVersion: plan.nextVersion,
|
|
118
|
+
commits: plan.commits,
|
|
119
|
+
cwd,
|
|
120
|
+
}, { includeFooter: false }),
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
(0, state_js_1.writeBaselineSha)(cwd, undefined, releaseTargets);
|
|
89
124
|
(0, node_child_process_1.execFileSync)("git", ["add", (0, state_js_1.getBaselineStatePath)(cwd)], {
|
|
90
125
|
cwd,
|
|
91
126
|
stdio: ["ignore", "pipe", "ignore"],
|
|
92
127
|
});
|
|
93
|
-
(0, node_child_process_1.execFileSync)("git", ["commit", "--amend", "--no-edit"], {
|
|
128
|
+
(0, node_child_process_1.execFileSync)("git", ["commit", "--amend", "--no-edit"], {
|
|
129
|
+
cwd,
|
|
130
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
131
|
+
});
|
|
94
132
|
return {
|
|
95
133
|
branch,
|
|
96
134
|
title,
|
|
97
135
|
version: plan.nextVersion,
|
|
136
|
+
previousVersion: plan.currentVersion,
|
|
98
137
|
commits: plan.commits,
|
|
138
|
+
plan,
|
|
99
139
|
};
|
|
100
140
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
function renderSimpleReviewRequestBody(version, previousVersion, commits, plan = null, cwd = process.cwd()) {
|
|
142
|
+
if (plan?.packages && plan.packages.length > 1) {
|
|
143
|
+
const sections = plan.packages
|
|
144
|
+
.filter((pkg) => pkg.nextVersion)
|
|
145
|
+
.map((pkg) => {
|
|
146
|
+
const notes = (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
147
|
+
currentVersion: pkg.currentVersion,
|
|
148
|
+
nextVersion: pkg.nextVersion ?? "",
|
|
149
|
+
commits: pkg.commits,
|
|
150
|
+
cwd,
|
|
151
|
+
}, { includeFooter: false });
|
|
152
|
+
const linkedHeader = notes.match(/^##\s+\[([^\]]+)\]\(([^)]+)\)\s+\(([^)]+)\)/u);
|
|
153
|
+
if (linkedHeader) {
|
|
154
|
+
const [, , compareUrl, date] = linkedHeader;
|
|
155
|
+
return notes.replace(/^##\s+\[[^\]]+\]\([^)]+\)\s+\([^)]+\)/u, `## [${pkg.path}: ${pkg.nextVersion ?? ""}](${compareUrl}) (${date})`);
|
|
156
|
+
}
|
|
157
|
+
const plainHeader = notes.match(/^##\s+([^\s]+)\s+\(([^)]+)\)/u);
|
|
158
|
+
if (plainHeader) {
|
|
159
|
+
const [, , date] = plainHeader;
|
|
160
|
+
return notes.replace(/^##\s+[^\s]+\s+\([^)]+\)/u, `## ${pkg.path}: ${pkg.nextVersion ?? ""} (${date})`);
|
|
161
|
+
}
|
|
162
|
+
return notes;
|
|
163
|
+
})
|
|
164
|
+
.join("\n\n");
|
|
165
|
+
return `${sections}\n\nThis PR was generated by Versionary.`;
|
|
105
166
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const breaking = [];
|
|
113
|
-
const features = [];
|
|
114
|
-
const fixes = [];
|
|
115
|
-
const commitBaseUrl = (0, repo_url_js_1.resolveRepositoryWebBaseUrl)(cwd);
|
|
116
|
-
for (const commit of commits) {
|
|
117
|
-
const subject = commit.subject;
|
|
118
|
-
const { label, message } = formatCommitMessage(subject);
|
|
119
|
-
const hash = commit.hash.slice(0, 7);
|
|
120
|
-
const hashLabel = commitBaseUrl ? `[\`${hash}\`](${commitBaseUrl}/commit/${commit.hash})` : `\`${hash}\``;
|
|
121
|
-
const type = (0, git_js_1.inferReleaseTypeFromSubject)(subject);
|
|
122
|
-
if (!type) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
const item = `- ${label}${message} (${hashLabel})`;
|
|
126
|
-
if (type === "major") {
|
|
127
|
-
breaking.push(item);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (type === "minor") {
|
|
131
|
-
features.push(item);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
fixes.push(item);
|
|
135
|
-
}
|
|
136
|
-
const sections = [];
|
|
137
|
-
if (breaking.length > 0) {
|
|
138
|
-
sections.push("### Breaking changes", ...breaking, "");
|
|
139
|
-
}
|
|
140
|
-
if (features.length > 0) {
|
|
141
|
-
sections.push("### Features", ...features, "");
|
|
142
|
-
}
|
|
143
|
-
if (fixes.length > 0) {
|
|
144
|
-
sections.push("### Fixes", ...fixes, "");
|
|
145
|
-
}
|
|
146
|
-
return [
|
|
147
|
-
":robot: I have created a release PR for this repository.",
|
|
148
|
-
"",
|
|
149
|
-
`## Version`,
|
|
150
|
-
"",
|
|
151
|
-
`This PR prepares **v${version}**.`,
|
|
152
|
-
"",
|
|
153
|
-
"## Release notes preview",
|
|
154
|
-
"",
|
|
155
|
-
...sections,
|
|
156
|
-
"This PR was generated by Versionary.",
|
|
157
|
-
].join("\n");
|
|
167
|
+
return (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
168
|
+
currentVersion: previousVersion,
|
|
169
|
+
nextVersion: version,
|
|
170
|
+
commits,
|
|
171
|
+
cwd,
|
|
172
|
+
}, { includeFooter: true });
|
|
158
173
|
}
|
|
159
|
-
async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, commits) {
|
|
174
|
+
async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null) {
|
|
160
175
|
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
161
176
|
const releaseFlow = loaded.config["review-mode"] ?? "direct";
|
|
162
177
|
if (releaseFlow !== "review") {
|
|
@@ -175,7 +190,7 @@ async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, comm
|
|
|
175
190
|
baseBranch: process.env.VERSIONARY_BASE_BRANCH ?? "main",
|
|
176
191
|
headBranch: branch,
|
|
177
192
|
title,
|
|
178
|
-
body: renderSimpleReviewRequestBody(version, commits, cwd),
|
|
193
|
+
body: renderSimpleReviewRequestBody(version, previousVersion, commits, plan, cwd),
|
|
179
194
|
labels: ["release"],
|
|
180
195
|
}, {
|
|
181
196
|
cwd,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { VersionaryScmReleaseMetadataResult } from "../../types/plugins.js";
|
|
2
|
+
export type TagStepResult = "created" | "exists";
|
|
3
|
+
export interface ReleaseTargetInput {
|
|
4
|
+
tag: string;
|
|
5
|
+
version: string;
|
|
6
|
+
notes: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ReleaseExecutionContext {
|
|
9
|
+
createReleaseMetadata: (input: ReleaseTargetInput) => Promise<VersionaryScmReleaseMetadataResult>;
|
|
10
|
+
}
|
|
11
|
+
export interface ReleaseTargetOutcome {
|
|
12
|
+
tag: string;
|
|
13
|
+
tagStatus: TagStepResult;
|
|
14
|
+
metadataStatus: "created" | "exists";
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function executeIdempotentReleaseTarget(cwd: string, target: ReleaseTargetInput, context: ReleaseExecutionContext): Promise<ReleaseTargetOutcome>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeIdempotentReleaseTarget = executeIdempotentReleaseTarget;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
function hasRemoteTag(cwd, tag) {
|
|
6
|
+
try {
|
|
7
|
+
const output = (0, node_child_process_1.execFileSync)("git", ["ls-remote", "--tags", "origin", `refs/tags/${tag}`], {
|
|
8
|
+
cwd,
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11
|
+
}).trim();
|
|
12
|
+
return output.length > 0;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function readRemoteTagSha(cwd, tag) {
|
|
19
|
+
try {
|
|
20
|
+
const output = (0, node_child_process_1.execFileSync)("git", ["ls-remote", "--tags", "origin", `refs/tags/${tag}`], {
|
|
21
|
+
cwd,
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
24
|
+
}).trim();
|
|
25
|
+
if (!output) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const [sha] = output.split(/\s+/u);
|
|
29
|
+
return sha ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createTagWithRecovery(cwd, tag) {
|
|
36
|
+
const localTag = (0, node_child_process_1.execFileSync)("git", ["tag", "--list", tag], {
|
|
37
|
+
cwd,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
40
|
+
}).trim();
|
|
41
|
+
if (localTag.length > 0) {
|
|
42
|
+
const localSha = (0, node_child_process_1.execFileSync)("git", ["rev-parse", `refs/tags/${tag}`], {
|
|
43
|
+
cwd,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
46
|
+
}).trim();
|
|
47
|
+
const remoteSha = readRemoteTagSha(cwd, tag);
|
|
48
|
+
if (remoteSha && remoteSha !== localSha) {
|
|
49
|
+
throw new Error(`Tag drift detected for "${tag}": local tag (${localSha.slice(0, 7)}) differs from remote (${remoteSha.slice(0, 7)}). Resolve the tag mismatch before retrying release.`);
|
|
50
|
+
}
|
|
51
|
+
return "exists";
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
(0, node_child_process_1.execFileSync)("git", ["tag", tag], {
|
|
55
|
+
cwd,
|
|
56
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`Failed creating local tag "${tag}". Ensure repository is writable and retry.`);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
(0, node_child_process_1.execFileSync)("git", ["push", "origin", tag], {
|
|
64
|
+
cwd,
|
|
65
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
66
|
+
});
|
|
67
|
+
return "created";
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
if (hasRemoteTag(cwd, tag)) {
|
|
71
|
+
const localSha = (0, node_child_process_1.execFileSync)("git", ["rev-parse", `refs/tags/${tag}`], {
|
|
72
|
+
cwd,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
75
|
+
}).trim();
|
|
76
|
+
const remoteSha = readRemoteTagSha(cwd, tag);
|
|
77
|
+
if (remoteSha && remoteSha !== localSha) {
|
|
78
|
+
throw new Error(`Tag drift detected for "${tag}": local tag (${localSha.slice(0, 7)}) differs from remote (${remoteSha.slice(0, 7)}). Resolve the tag mismatch before retrying release.`);
|
|
79
|
+
}
|
|
80
|
+
return "exists";
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Failed pushing tag "${tag}" to origin. Check push permissions and remote connectivity, then retry.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function executeIdempotentReleaseTarget(cwd, target, context) {
|
|
86
|
+
const tagStatus = createTagWithRecovery(cwd, target.tag);
|
|
87
|
+
const metadata = await context.createReleaseMetadata(target);
|
|
88
|
+
const metadataStatus = metadata.status ?? "created";
|
|
89
|
+
if (tagStatus === "exists" && metadataStatus === "created") {
|
|
90
|
+
console.info(`Recovered drift for ${target.tag}: tag already existed and release metadata has now been created.`);
|
|
91
|
+
}
|
|
92
|
+
if (!metadata.url) {
|
|
93
|
+
throw new Error(`Release metadata for "${target.tag}" did not return a URL. Verify SCM API permissions and retry.`);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
tag: target.tag,
|
|
97
|
+
tagStatus,
|
|
98
|
+
metadataStatus,
|
|
99
|
+
url: metadata.url,
|
|
100
|
+
};
|
|
101
|
+
}
|