versionary 0.2.0 → 0.4.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 +231 -12
- package/dist/app/release/artifact-rules.d.ts +3 -0
- package/dist/app/release/artifact-rules.js +186 -0
- package/dist/app/release/pr.d.ts +23 -0
- package/dist/{simple → app/release}/pr.js +90 -79
- package/dist/app/release/recovery.d.ts +20 -0
- package/dist/app/release/recovery.js +101 -0
- package/dist/app/release/release.d.ts +19 -0
- package/dist/app/release/release.js +111 -0
- package/dist/app/release/state.d.ts +9 -0
- package/dist/app/release/state.js +93 -0
- package/dist/app/release/verify.d.ts +2 -0
- package/dist/app/release/verify.js +5 -0
- package/dist/cli/index.js +71 -15
- package/dist/config/load-config.js +2 -12
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +34 -1
- 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 +67 -0
- package/dist/domain/strategy/r.d.ts +2 -0
- package/dist/domain/strategy/r.js +46 -0
- package/dist/domain/strategy/resolve.d.ts +3 -0
- package/dist/domain/strategy/resolve.js +19 -0
- package/dist/domain/strategy/rust.d.ts +2 -0
- package/dist/domain/strategy/rust.js +369 -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 +3 -2
- 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/pr.d.ts +0 -15
- package/dist/simple/release.d.ts +0 -1
- package/dist/simple/release.js +0 -77
- 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/state.js +0 -47
- /package/dist/{simple → infra/git}/repo-url.d.ts +0 -0
- /package/dist/{simple → infra/git}/repo-url.js +0 -0
|
@@ -9,16 +9,15 @@ 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");
|
|
20
|
+
const artifact_rules_js_1 = require("./artifact-rules.js");
|
|
22
21
|
const state_js_1 = require("./state.js");
|
|
23
22
|
const SAFE_DIRTY_FILES = new Set([
|
|
24
23
|
"pnpm-lock.yaml",
|
|
@@ -57,106 +56,118 @@ function splitSafeDirtyFiles(files) {
|
|
|
57
56
|
}
|
|
58
57
|
return { ignored, blocking };
|
|
59
58
|
}
|
|
60
|
-
function ensureCleanWorktree(cwd) {
|
|
59
|
+
function ensureCleanWorktree(cwd, logger) {
|
|
61
60
|
const dirtyFiles = listTrackedDirtyFiles(cwd);
|
|
62
61
|
const { ignored, blocking } = splitSafeDirtyFiles(dirtyFiles);
|
|
63
62
|
if (blocking.length > 0) {
|
|
64
63
|
throw new Error(`Working tree has tracked modifications before versionary pr:\n${blocking.join("\n")}\nCommit/stash tracked changes first.`);
|
|
65
64
|
}
|
|
66
65
|
if (ignored.length > 0) {
|
|
67
|
-
|
|
66
|
+
logger?.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
|
-
function prepareSimpleReleasePr(cwd = process.cwd()) {
|
|
69
|
+
function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
|
|
71
70
|
const plan = (0, plan_js_1.createSimplePlan)(cwd);
|
|
71
|
+
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
72
|
+
const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
|
|
72
73
|
if (!plan.nextVersion) {
|
|
73
74
|
throw new Error("No releasable commits found. Nothing to open a release PR for.");
|
|
74
75
|
}
|
|
75
|
-
ensureCleanWorktree(cwd);
|
|
76
|
-
const
|
|
77
|
-
|
|
76
|
+
ensureCleanWorktree(cwd, options.logger);
|
|
77
|
+
const updatedVersionFiles = strategy.writeVersion(cwd, loaded.config, plan.nextVersion);
|
|
78
|
+
const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
|
|
78
79
|
const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
|
|
79
80
|
(0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
|
|
80
81
|
const branch = plan.releaseBranchPrefix;
|
|
81
82
|
const title = `chore(release): v${plan.nextVersion}`;
|
|
82
|
-
(0, node_child_process_1.execFileSync)("git", ["checkout", "-B", branch], {
|
|
83
|
-
|
|
83
|
+
(0, node_child_process_1.execFileSync)("git", ["checkout", "-B", branch], {
|
|
84
|
+
cwd,
|
|
85
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
86
|
+
});
|
|
87
|
+
const filesToAdd = [
|
|
88
|
+
...new Set([
|
|
89
|
+
...updatedVersionFiles,
|
|
90
|
+
...updatedArtifactFiles,
|
|
91
|
+
plan.changelogFile,
|
|
92
|
+
]),
|
|
93
|
+
];
|
|
94
|
+
(0, node_child_process_1.execFileSync)("git", ["add", ...filesToAdd], {
|
|
84
95
|
cwd,
|
|
85
96
|
stdio: ["ignore", "pipe", "ignore"],
|
|
86
97
|
});
|
|
87
|
-
(0, node_child_process_1.execFileSync)("git", ["commit", "-m", title], {
|
|
88
|
-
|
|
98
|
+
(0, node_child_process_1.execFileSync)("git", ["commit", "-m", title], {
|
|
99
|
+
cwd,
|
|
100
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
101
|
+
});
|
|
102
|
+
const releaseTargets = plan.packages
|
|
103
|
+
? plan.packages
|
|
104
|
+
.filter((pkg) => pkg.nextVersion)
|
|
105
|
+
.map((pkg) => ({
|
|
106
|
+
path: pkg.path,
|
|
107
|
+
version: pkg.nextVersion ?? "",
|
|
108
|
+
tag: pkg.path === "."
|
|
109
|
+
? `v${pkg.nextVersion ?? ""}`
|
|
110
|
+
: `${pkg.path.replaceAll("/", "-")}-v${pkg.nextVersion ?? ""}`,
|
|
111
|
+
}))
|
|
112
|
+
: [
|
|
113
|
+
{
|
|
114
|
+
path: ".",
|
|
115
|
+
version: plan.nextVersion,
|
|
116
|
+
tag: `v${plan.nextVersion}`,
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
(0, state_js_1.writeBaselineSha)(cwd, undefined, releaseTargets);
|
|
89
120
|
(0, node_child_process_1.execFileSync)("git", ["add", (0, state_js_1.getBaselineStatePath)(cwd)], {
|
|
90
121
|
cwd,
|
|
91
122
|
stdio: ["ignore", "pipe", "ignore"],
|
|
92
123
|
});
|
|
93
|
-
(0, node_child_process_1.execFileSync)("git", ["commit", "--amend", "--no-edit"], {
|
|
124
|
+
(0, node_child_process_1.execFileSync)("git", ["commit", "--amend", "--no-edit"], {
|
|
125
|
+
cwd,
|
|
126
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
127
|
+
});
|
|
94
128
|
return {
|
|
95
129
|
branch,
|
|
96
130
|
title,
|
|
97
131
|
version: plan.nextVersion,
|
|
132
|
+
previousVersion: plan.currentVersion,
|
|
98
133
|
commits: plan.commits,
|
|
134
|
+
plan,
|
|
99
135
|
};
|
|
100
136
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
137
|
+
function renderSimpleReviewRequestBody(version, previousVersion, commits, plan = null, cwd = process.cwd()) {
|
|
138
|
+
if (plan?.packages && plan.packages.length > 1) {
|
|
139
|
+
const sections = plan.packages
|
|
140
|
+
.filter((pkg) => pkg.nextVersion)
|
|
141
|
+
.map((pkg) => {
|
|
142
|
+
const notes = (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
143
|
+
currentVersion: pkg.currentVersion,
|
|
144
|
+
nextVersion: pkg.nextVersion ?? "",
|
|
145
|
+
commits: pkg.commits,
|
|
146
|
+
cwd,
|
|
147
|
+
}, { includeFooter: false });
|
|
148
|
+
const linkedHeader = notes.match(/^##\s+\[([^\]]+)\]\(([^)]+)\)\s+\(([^)]+)\)/u);
|
|
149
|
+
if (linkedHeader) {
|
|
150
|
+
const [, , compareUrl, date] = linkedHeader;
|
|
151
|
+
return notes.replace(/^##\s+\[[^\]]+\]\([^)]+\)\s+\([^)]+\)/u, `## [${pkg.path}: ${pkg.nextVersion ?? ""}](${compareUrl}) (${date})`);
|
|
152
|
+
}
|
|
153
|
+
const plainHeader = notes.match(/^##\s+([^\s]+)\s+\(([^)]+)\)/u);
|
|
154
|
+
if (plainHeader) {
|
|
155
|
+
const [, , date] = plainHeader;
|
|
156
|
+
return notes.replace(/^##\s+[^\s]+\s+\([^)]+\)/u, `## ${pkg.path}: ${pkg.nextVersion ?? ""} (${date})`);
|
|
157
|
+
}
|
|
158
|
+
return notes;
|
|
159
|
+
})
|
|
160
|
+
.join("\n\n");
|
|
161
|
+
return `${sections}\n\nThis PR was generated by Versionary.`;
|
|
105
162
|
}
|
|
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");
|
|
163
|
+
return (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
164
|
+
currentVersion: previousVersion,
|
|
165
|
+
nextVersion: version,
|
|
166
|
+
commits,
|
|
167
|
+
cwd,
|
|
168
|
+
}, { includeFooter: true });
|
|
158
169
|
}
|
|
159
|
-
async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, commits) {
|
|
170
|
+
async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null, options = {}) {
|
|
160
171
|
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
161
172
|
const releaseFlow = loaded.config["review-mode"] ?? "direct";
|
|
162
173
|
if (releaseFlow !== "review") {
|
|
@@ -175,11 +186,11 @@ async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, comm
|
|
|
175
186
|
baseBranch: process.env.VERSIONARY_BASE_BRANCH ?? "main",
|
|
176
187
|
headBranch: branch,
|
|
177
188
|
title,
|
|
178
|
-
body: renderSimpleReviewRequestBody(version, commits, cwd),
|
|
189
|
+
body: renderSimpleReviewRequestBody(version, previousVersion, commits, plan, cwd),
|
|
179
190
|
labels: ["release"],
|
|
180
191
|
}, {
|
|
181
192
|
cwd,
|
|
182
|
-
logger:
|
|
193
|
+
logger: options.logger,
|
|
183
194
|
});
|
|
184
195
|
return result.url;
|
|
185
196
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
logger?: {
|
|
11
|
+
info: (message: string) => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface ReleaseTargetOutcome {
|
|
15
|
+
tag: string;
|
|
16
|
+
tagStatus: TagStepResult;
|
|
17
|
+
metadataStatus: "created" | "exists";
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
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
|
+
context.logger?.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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { VersionaryPluginContext } from "../../types/plugins.js";
|
|
2
|
+
export declare function runSimpleRelease(cwd?: string): Promise<string>;
|
|
3
|
+
export type SimpleRunReleaseResult = {
|
|
4
|
+
action: "release-skipped";
|
|
5
|
+
reason: string;
|
|
6
|
+
} | {
|
|
7
|
+
action: "release-published";
|
|
8
|
+
message: string;
|
|
9
|
+
releases: {
|
|
10
|
+
tag: string;
|
|
11
|
+
url: string;
|
|
12
|
+
tagStatus: "created" | "exists";
|
|
13
|
+
metadataStatus: "created" | "exists";
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
export interface RunSimpleReleaseOptions {
|
|
17
|
+
logger?: VersionaryPluginContext["logger"];
|
|
18
|
+
}
|
|
19
|
+
export declare function runSimpleReleaseDetailed(cwd?: string, options?: RunSimpleReleaseOptions): Promise<SimpleRunReleaseResult>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runSimpleRelease = runSimpleRelease;
|
|
7
|
+
exports.runSimpleReleaseDetailed = runSimpleReleaseDetailed;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const load_config_js_1 = require("../../config/load-config.js");
|
|
12
|
+
const resolve_js_1 = require("../../domain/strategy/resolve.js");
|
|
13
|
+
const capabilities_js_1 = require("../../plugins/capabilities.js");
|
|
14
|
+
const runtime_js_1 = require("../../plugins/runtime.js");
|
|
15
|
+
const pr_js_1 = require("./pr.js");
|
|
16
|
+
const recovery_js_1 = require("./recovery.js");
|
|
17
|
+
const state_js_1 = require("./state.js");
|
|
18
|
+
function getHeadCommitSubject(cwd) {
|
|
19
|
+
return (0, node_child_process_1.execFileSync)("git", ["log", "-1", "--pretty=%s"], {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
23
|
+
}).trim();
|
|
24
|
+
}
|
|
25
|
+
function readReleaseNotes(cwd, version, changelogFile) {
|
|
26
|
+
const changelogPath = node_path_1.default.join(cwd, changelogFile);
|
|
27
|
+
if (!node_fs_1.default.existsSync(changelogPath)) {
|
|
28
|
+
return `Automated release for v${version}`;
|
|
29
|
+
}
|
|
30
|
+
const content = node_fs_1.default.readFileSync(changelogPath, "utf8");
|
|
31
|
+
const lines = content.split("\n");
|
|
32
|
+
const start = lines.findIndex((line) => line.startsWith(`## ${version} -`) || line.startsWith(`## [${version}](`));
|
|
33
|
+
if (start < 0) {
|
|
34
|
+
return `Automated release for v${version}`;
|
|
35
|
+
}
|
|
36
|
+
let end = lines.length;
|
|
37
|
+
for (let idx = start + 1; idx < lines.length; idx += 1) {
|
|
38
|
+
if (lines[idx]?.startsWith("## ")) {
|
|
39
|
+
end = idx;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const notes = lines
|
|
44
|
+
.slice(start + 1, end)
|
|
45
|
+
.join("\n")
|
|
46
|
+
.trim();
|
|
47
|
+
return notes.length > 0 ? notes : `Automated release for v${version}`;
|
|
48
|
+
}
|
|
49
|
+
async function runSimpleRelease(cwd = process.cwd()) {
|
|
50
|
+
const result = await runSimpleReleaseDetailed(cwd, { logger: console });
|
|
51
|
+
if (result.action === "release-skipped") {
|
|
52
|
+
return result.reason;
|
|
53
|
+
}
|
|
54
|
+
return result.message;
|
|
55
|
+
}
|
|
56
|
+
async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
57
|
+
const subject = getHeadCommitSubject(cwd);
|
|
58
|
+
if (!(0, pr_js_1.isReleaseCommitMessage)(subject)) {
|
|
59
|
+
return {
|
|
60
|
+
action: "release-skipped",
|
|
61
|
+
reason: "No release commit context detected; skipping release stage.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
65
|
+
const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
|
|
66
|
+
const changelogFile = loaded.config["changelog-file"] ?? "CHANGELOG.md";
|
|
67
|
+
const version = strategy.readVersion(cwd, loaded.config);
|
|
68
|
+
const defaultTag = `v${version}`;
|
|
69
|
+
const plugins = (0, runtime_js_1.loadRuntimePlugins)();
|
|
70
|
+
const scmPlugins = (0, capabilities_js_1.findPluginsByCapability)(plugins, "scm.releaseMetadata");
|
|
71
|
+
if (scmPlugins.length === 0) {
|
|
72
|
+
throw new Error("No scm.releaseMetadata plugin is available.");
|
|
73
|
+
}
|
|
74
|
+
const plugin = scmPlugins[0];
|
|
75
|
+
if (!plugin?.createReleaseMetadata) {
|
|
76
|
+
throw new Error(`Plugin "${plugin?.name ?? "unknown"}" does not implement createReleaseMetadata.`);
|
|
77
|
+
}
|
|
78
|
+
const releaseTargets = (0, state_js_1.readReleaseTargets)(cwd);
|
|
79
|
+
const targets = releaseTargets.length > 0
|
|
80
|
+
? releaseTargets
|
|
81
|
+
: [
|
|
82
|
+
{
|
|
83
|
+
path: ".",
|
|
84
|
+
version,
|
|
85
|
+
tag: defaultTag,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
const releases = [];
|
|
89
|
+
for (const target of targets) {
|
|
90
|
+
const outcome = await (0, recovery_js_1.executeIdempotentReleaseTarget)(cwd, {
|
|
91
|
+
tag: target.tag,
|
|
92
|
+
version: target.version,
|
|
93
|
+
notes: readReleaseNotes(cwd, target.version, changelogFile),
|
|
94
|
+
}, {
|
|
95
|
+
createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger: options.logger }),
|
|
96
|
+
logger: options.logger,
|
|
97
|
+
});
|
|
98
|
+
releases.push({
|
|
99
|
+
tag: outcome.tag,
|
|
100
|
+
url: outcome.url,
|
|
101
|
+
tagStatus: outcome.tagStatus,
|
|
102
|
+
metadataStatus: outcome.metadataStatus,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const published = releases.map((outcome) => `${outcome.tag}: ${outcome.url} (tag=${outcome.tagStatus}, metadata=${outcome.metadataStatus})`);
|
|
106
|
+
return {
|
|
107
|
+
action: "release-published",
|
|
108
|
+
releases,
|
|
109
|
+
message: `Published releases ${published.join(", ")}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ReleaseTargetState {
|
|
2
|
+
path: string;
|
|
3
|
+
version: string;
|
|
4
|
+
tag: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getBaselineStatePath(cwd: string): string;
|
|
7
|
+
export declare function readBaselineSha(cwd?: string): string | null;
|
|
8
|
+
export declare function readReleaseTargets(cwd?: string): ReleaseTargetState[];
|
|
9
|
+
export declare function writeBaselineSha(cwd?: string, sha?: string, releaseTargets?: ReleaseTargetState[]): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getBaselineStatePath = getBaselineStatePath;
|
|
7
|
+
exports.readBaselineSha = readBaselineSha;
|
|
8
|
+
exports.readReleaseTargets = readReleaseTargets;
|
|
9
|
+
exports.writeBaselineSha = writeBaselineSha;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const load_config_js_1 = require("../../config/load-config.js");
|
|
14
|
+
function parseStateFile(raw, filePath) {
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
17
|
+
throw new Error(`Invalid release manifest at ${filePath}: expected an object.`);
|
|
18
|
+
}
|
|
19
|
+
const manifest = parsed;
|
|
20
|
+
if (manifest.manifestVersion !== undefined &&
|
|
21
|
+
manifest.manifestVersion !== 1) {
|
|
22
|
+
throw new Error(`Unsupported manifestVersion in ${filePath}: ${String(manifest.manifestVersion)}.`);
|
|
23
|
+
}
|
|
24
|
+
if (manifest.baselineSha !== undefined &&
|
|
25
|
+
typeof manifest.baselineSha !== "string") {
|
|
26
|
+
throw new Error(`Invalid release manifest at ${filePath}: baselineSha must be a string.`);
|
|
27
|
+
}
|
|
28
|
+
if (manifest.releaseTargets !== undefined &&
|
|
29
|
+
!Array.isArray(manifest.releaseTargets)) {
|
|
30
|
+
throw new Error(`Invalid release manifest at ${filePath}: releaseTargets must be an array.`);
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(manifest.releaseTargets)) {
|
|
33
|
+
for (const target of manifest.releaseTargets) {
|
|
34
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
|
35
|
+
throw new Error(`Invalid release manifest at ${filePath}: each release target must be an object.`);
|
|
36
|
+
}
|
|
37
|
+
const record = target;
|
|
38
|
+
if (typeof record.path !== "string" ||
|
|
39
|
+
typeof record.version !== "string" ||
|
|
40
|
+
typeof record.tag !== "string") {
|
|
41
|
+
throw new Error(`Invalid release manifest at ${filePath}: releaseTargets must contain string path, version, and tag.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return manifest;
|
|
46
|
+
}
|
|
47
|
+
function getBaselineStatePath(cwd) {
|
|
48
|
+
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
49
|
+
const configured = loaded.config["baseline-file"];
|
|
50
|
+
if (configured) {
|
|
51
|
+
return node_path_1.default.join(cwd, configured);
|
|
52
|
+
}
|
|
53
|
+
const preferred = node_path_1.default.join(cwd, ".versionary-manifest.json");
|
|
54
|
+
if (node_fs_1.default.existsSync(preferred)) {
|
|
55
|
+
return preferred;
|
|
56
|
+
}
|
|
57
|
+
const legacy = node_path_1.default.join(cwd, "versionary.versions.json");
|
|
58
|
+
if (node_fs_1.default.existsSync(legacy)) {
|
|
59
|
+
return legacy;
|
|
60
|
+
}
|
|
61
|
+
return preferred;
|
|
62
|
+
}
|
|
63
|
+
function readBaselineSha(cwd = process.cwd()) {
|
|
64
|
+
const filePath = getBaselineStatePath(cwd);
|
|
65
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const parsed = parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath);
|
|
69
|
+
return parsed.baselineSha ?? null;
|
|
70
|
+
}
|
|
71
|
+
function readReleaseTargets(cwd = process.cwd()) {
|
|
72
|
+
const filePath = getBaselineStatePath(cwd);
|
|
73
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const parsed = parseStateFile(node_fs_1.default.readFileSync(filePath, "utf8"), filePath);
|
|
77
|
+
return parsed.releaseTargets ?? [];
|
|
78
|
+
}
|
|
79
|
+
function writeBaselineSha(cwd = process.cwd(), sha, releaseTargets = []) {
|
|
80
|
+
const baselineSha = sha ??
|
|
81
|
+
(0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
82
|
+
cwd,
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
85
|
+
}).trim();
|
|
86
|
+
const filePath = getBaselineStatePath(cwd);
|
|
87
|
+
const next = {
|
|
88
|
+
manifestVersion: 1,
|
|
89
|
+
baselineSha,
|
|
90
|
+
releaseTargets,
|
|
91
|
+
};
|
|
92
|
+
node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
93
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyProject = void 0;
|
|
4
|
+
var verify_project_js_1 = require("../../verify/verify-project.js");
|
|
5
|
+
Object.defineProperty(exports, "verifyProject", { enumerable: true, get: function () { return verify_project_js_1.verifyProject; } });
|