versionary 1.4.0 → 1.4.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.
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { appendFileSync } from "node:fs";
3
+ import { appendFileSync, readFileSync } from "node:fs";
4
4
  function getInput(name) {
5
5
  const canonical = `INPUT_${name.replace(/ /g, "_").toUpperCase()}`;
6
6
  const underscoreAlias = canonical.replace(/-/g, "_");
@@ -74,9 +74,41 @@ function setOutput(name, value) {
74
74
  const delimiter = `versionary-${randomUUID()}`;
75
75
  appendFileSync(outputPath, `${name}<<${delimiter}\n${value}\n${delimiter}\n`, "utf8");
76
76
  }
77
+ function isForkRepository() {
78
+ const eventPath = process.env.GITHUB_EVENT_PATH;
79
+ if (!eventPath) {
80
+ return false;
81
+ }
82
+ try {
83
+ const event = JSON.parse(readFileSync(eventPath, "utf8"));
84
+ return event?.repository?.fork === true;
85
+ }
86
+ catch {
87
+ // Only a confirmed fork may bypass the token requirement.
88
+ return false;
89
+ }
90
+ }
91
+ function skipRun(action, message) {
92
+ const payload = { action, message, releaseCreated: false, tagNames: [] };
93
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
94
+ setOutput("action", action);
95
+ setOutput("message", message);
96
+ setOutput("release_created", "false");
97
+ setOutput("tag_name", "");
98
+ setOutput("tag_names", "[]");
99
+ setOutput("release_targets", "[]");
100
+ setOutput("review_url", "");
101
+ setOutput("review_requests", "[]");
102
+ setOutput("branch", "");
103
+ setOutput("title", "");
104
+ }
77
105
  function main() {
78
106
  const token = getInput("token");
79
107
  if (!token) {
108
+ if (isForkRepository()) {
109
+ skipRun("fork-skipped", "Skipping release automation in a fork without a release token.");
110
+ return;
111
+ }
80
112
  throw new Error("Input required and not supplied: token.");
81
113
  }
82
114
  const versionaryVersion = getInput("versionary-version") || "0.7.0";
@@ -120,25 +152,8 @@ function main() {
120
152
  // recovery cycle.
121
153
  const releaseCanPublish = hasVersionaryReleaseMarker(cwd, sha) && isAncestor(cwd, sha, remoteSha);
122
154
  if (!releaseCanPublish) {
123
- const staleMessage = `Skipping stale push run for ${sha.slice(0, 7)}; ` +
124
- `${ref} now points to ${remoteSha.slice(0, 7)}.`;
125
- const stalePayload = {
126
- action: "stale-run-skipped",
127
- message: staleMessage,
128
- releaseCreated: false,
129
- tagNames: [],
130
- };
131
- process.stdout.write(`${JSON.stringify(stalePayload)}\n`);
132
- setOutput("action", stalePayload.action);
133
- setOutput("message", stalePayload.message);
134
- setOutput("release_created", "false");
135
- setOutput("tag_name", "");
136
- setOutput("tag_names", "[]");
137
- setOutput("release_targets", "[]");
138
- setOutput("review_url", "");
139
- setOutput("review_requests", "[]");
140
- setOutput("branch", "");
141
- setOutput("title", "");
155
+ skipRun("stale-run-skipped", `Skipping stale push run for ${sha.slice(0, 7)}; ` +
156
+ `${ref} now points to ${remoteSha.slice(0, 7)}.`);
142
157
  return;
143
158
  }
144
159
  }
package/dist/cli/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { loadConfig } from "../config/load-config.js";
4
4
  import { prependChangelog, renderReleasePlanChangelog, } from "../release/changelog.js";
5
+ import { runDirectRelease } from "../release/direct.js";
5
6
  import { createReleasePlan } from "../release/plan.js";
6
7
  import { closeStaleReviewRequestIfExists, isReleaseCommitMessage, openOrUpdateReviewRequest, preparePendingReleasePr, preparePendingSeparateReleasePrs, prepareReleasePr, prepareSeparateReleasePrs, pushReleaseBranch, reconcileSeparateReviewRequests, resolveReleaseHighlights, } from "../release/pr.js";
7
8
  import { runRelease, runReleaseDetailed } from "../release/release.js";
@@ -194,6 +195,36 @@ async function main() {
194
195
  const flags = parseFlags(args);
195
196
  const logger = flags.json ? undefined : console;
196
197
  if (!command || command === "run") {
198
+ if (loadConfig(process.cwd()).config["review-mode"] === "direct") {
199
+ const release = await runDirectRelease(process.cwd(), {
200
+ logger,
201
+ "dry-run": flags["dry-run"],
202
+ });
203
+ const message = release.action === "release-skipped" ? release.reason : release.message;
204
+ if (flags.json) {
205
+ emitJson({
206
+ action: release.action,
207
+ message,
208
+ releaseCreated: release.action === "release-published" &&
209
+ release.releases.length > 0,
210
+ tagNames: release.action === "release-published"
211
+ ? release.releases.map((target) => target.tag)
212
+ : release.action === "release-dry-run"
213
+ ? release.targets.map((target) => target.tag)
214
+ : [],
215
+ ...("releaseTargets" in release
216
+ ? { releaseTargets: release.releaseTargets }
217
+ : {}),
218
+ ...(release.action === "release-dry-run"
219
+ ? { targets: release.targets }
220
+ : {}),
221
+ });
222
+ }
223
+ else {
224
+ console.log(message);
225
+ }
226
+ return 0;
227
+ }
197
228
  const commitMessage = execFileSync("git", ["log", "-1", "--pretty=%B"], {
198
229
  encoding: "utf8",
199
230
  stdio: ["ignore", "pipe", "ignore"],
@@ -0,0 +1,5 @@
1
+ import { type RunReleaseOptions, type RunReleaseResult } from "./release.js";
2
+ export declare function runDirectRelease(cwd?: string, options?: RunReleaseOptions): Promise<RunReleaseResult | {
3
+ action: "noop";
4
+ message: string;
5
+ }>;
@@ -0,0 +1,80 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { loadConfig } from "../config/load-config.js";
3
+ import { createReleasePlan } from "./plan.js";
4
+ import { buildReleaseTargets, isReleaseCommitMessage, preparePendingReleasePr, prepareReleasePr, } from "./pr.js";
5
+ import { runReleaseDetailed, } from "./release.js";
6
+ import { hasFullyUntaggedPendingRelease, hasReleaseStateChangeAtHead, readPendingReleaseTargets, } from "./state.js";
7
+ import { releaseTargetHandoff } from "./targets.js";
8
+ function git(cwd, ...args) {
9
+ return execFileSync("git", args, {
10
+ cwd,
11
+ encoding: "utf8",
12
+ stdio: ["ignore", "pipe", "pipe"],
13
+ }).trim();
14
+ }
15
+ function resolveDirectBranch(cwd) {
16
+ const ref = process.env.GITHUB_REF ?? "";
17
+ const branch = process.env.VERSIONARY_BASE_BRANCH ||
18
+ (ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : "") ||
19
+ git(cwd, "branch", "--show-current");
20
+ if (!branch) {
21
+ throw new Error("Direct releases require a branch. Check out the release base branch or set VERSIONARY_BASE_BRANCH.");
22
+ }
23
+ git(cwd, "check-ref-format", `refs/heads/${branch}`);
24
+ return branch;
25
+ }
26
+ function pushDirectRelease(cwd, branch) {
27
+ git(cwd, "fetch", "origin", `refs/heads/${branch}`);
28
+ try {
29
+ // An earlier release may finish after a newer commit has reached trunk.
30
+ git(cwd, "merge-base", "--is-ancestor", "HEAD", "FETCH_HEAD");
31
+ return;
32
+ }
33
+ catch {
34
+ // A normal push rejects concurrent divergent updates without rewriting trunk.
35
+ }
36
+ try {
37
+ git(cwd, "push", "origin", `HEAD:refs/heads/${branch}`);
38
+ }
39
+ catch (error) {
40
+ throw new Error(`Failed pushing direct release commit to ${branch}; no release was published. Check branch permissions and whether the branch advanced, then retry.`, { cause: error });
41
+ }
42
+ }
43
+ export async function runDirectRelease(cwd = process.cwd(), options = {}) {
44
+ const releaseContext = isReleaseCommitMessage(git(cwd, "log", "-1", "--pretty=%B")) ||
45
+ hasReleaseStateChangeAtHead(cwd);
46
+ if (!releaseContext) {
47
+ const pending = hasFullyUntaggedPendingRelease(cwd);
48
+ const plan = pending ? undefined : createReleasePlan(cwd);
49
+ if (plan && !plan.nextVersion) {
50
+ return {
51
+ action: "noop",
52
+ message: "No releasable commits found. Nothing to do.",
53
+ };
54
+ }
55
+ if (options["dry-run"]) {
56
+ const targets = plan
57
+ ? buildReleaseTargets(cwd, plan, loadConfig(cwd).config)
58
+ : readPendingReleaseTargets(cwd);
59
+ return {
60
+ action: "release-dry-run",
61
+ message: `Dry run: would ${pending ? "recover and publish" : "prepare and publish"} releases ${targets.map((target) => target.tag).join(", ")}`,
62
+ targets: targets.map(({ tag, version }) => ({ tag, version })),
63
+ releaseTargets: releaseTargetHandoff(targets, options.logger),
64
+ };
65
+ }
66
+ const branch = resolveDirectBranch(cwd);
67
+ if (pending) {
68
+ preparePendingReleasePr(cwd, { logger: options.logger, branch });
69
+ }
70
+ else {
71
+ prepareReleasePr(cwd, { logger: options.logger, branch });
72
+ }
73
+ pushDirectRelease(cwd, branch);
74
+ }
75
+ else if (!options["dry-run"]) {
76
+ // A retry may start at a release commit whose previous branch push failed.
77
+ pushDirectRelease(cwd, resolveDirectBranch(cwd));
78
+ }
79
+ return runReleaseDetailed(cwd, options);
80
+ }
@@ -1,3 +1,4 @@
1
+ import { loadConfig } from "../config/load-config.js";
1
2
  import type { ParsedCommit } from "../git/commits.js";
2
3
  import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
3
4
  import type { VersionaryPluginContext } from "../types/plugins.js";
@@ -21,6 +22,7 @@ export declare function splitSafeDirtyFiles(files: string[]): {
21
22
  ignored: string[];
22
23
  blocking: string[];
23
24
  };
25
+ export declare function buildReleaseTargets(cwd: string, plan: ReleasePlan, loadedConfig: ReturnType<typeof loadConfig>["config"]): ReleaseTargetState[];
24
26
  export interface PendingReleasePrResult {
25
27
  branch: string;
26
28
  title: string;
@@ -36,6 +38,7 @@ export declare function renderPendingReleaseReviewRequestBody(targets: ReleaseTa
36
38
  */
37
39
  export declare function preparePendingReleasePr(cwd?: string, options?: {
38
40
  logger?: VersionaryPluginContext["logger"];
41
+ branch?: string;
39
42
  }): PendingReleasePrResult;
40
43
  /**
41
44
  * Recreate each unpublished package cohort on its own corrected-base branch.
@@ -48,6 +51,7 @@ export declare function preparePendingSeparateReleasePrs(cwd?: string, options?:
48
51
  }): SeparateReviewCandidate[];
49
52
  export declare function prepareReleasePr(cwd?: string, options?: {
50
53
  logger?: VersionaryPluginContext["logger"];
54
+ branch?: string;
51
55
  }): {
52
56
  branch: string;
53
57
  title: string;
@@ -133,7 +133,7 @@ function fetchRemoteReleaseBranch(cwd, branch) {
133
133
  });
134
134
  return remoteRef;
135
135
  }
136
- function buildReleaseTargets(cwd, plan, loadedConfig) {
136
+ export function buildReleaseTargets(cwd, plan, loadedConfig) {
137
137
  const releasingPaths = new Set(plan.packages?.filter((pkg) => pkg.nextVersion).map((pkg) => pkg.path) ?? [
138
138
  ".",
139
139
  ]);
@@ -237,7 +237,7 @@ export function preparePendingReleasePr(cwd = process.cwd(), options = {}) {
237
237
  }
238
238
  ensureCleanWorktree(cwd, options.logger);
239
239
  const loaded = loadConfig(cwd);
240
- const branch = loaded.config["release-branch"] ?? "versionary/release";
240
+ const branch = options.branch ?? loaded.config["release-branch"] ?? "versionary/release";
241
241
  const title = formatReleaseCommitTitle(targets);
242
242
  const releaseBaselineSha = execFileSync("git", ["rev-parse", "HEAD"], {
243
243
  cwd,
@@ -466,7 +466,7 @@ export function prepareReleasePr(cwd = process.cwd(), options = {}) {
466
466
  updatedChangelogFiles.push(packageChangelogPath);
467
467
  }
468
468
  const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
469
- const branch = plan.releaseBranchPrefix;
469
+ const branch = options.branch ?? plan.releaseBranchPrefix;
470
470
  const title = formatReleaseCommitTitle(releaseTargets);
471
471
  const hasRemoteReleaseBranch = remoteReleaseBranchExists(cwd, branch);
472
472
  const remoteReleaseRef = hasRemoteReleaseBranch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",
@@ -39,7 +39,7 @@
39
39
  "typescript": "^7.0.2",
40
40
  "vite": "^8.0.0",
41
41
  "vitepress": "^1.6.4",
42
- "vitest": "^4.1.4"
42
+ "vitest": "^5.0.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",