@varlock/bumpy 1.12.0 → 1.13.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/README.md CHANGED
@@ -88,20 +88,40 @@ _examples use bun, but works with Node.js_
88
88
  ### PR check workflow
89
89
 
90
90
  ```yaml
91
- # .github/workflows/bumpy-check.yml
91
+ # .github/workflows/bumpy-check.yaml
92
+ #
93
+ # ⚠️ Uses `pull_request_target` so fork PR comments work — runs with write
94
+ # perms and secrets, so it MUST NOT execute PR code (no `bun install`, no
95
+ # PR-defined scripts). Bumpy only reads files; its version is resolved from
96
+ # the base branch's package.json. See docs/github-actions.md for details.
92
97
  name: Bumpy Check
93
- on: pull_request
98
+ on: pull_request_target
99
+
100
+ permissions:
101
+ pull-requests: write
102
+ contents: read
94
103
 
95
104
  jobs:
96
105
  check:
97
106
  runs-on: ubuntu-latest
98
- permissions:
99
- pull-requests: write
100
107
  steps:
101
108
  - uses: actions/checkout@v6
109
+ with:
110
+ ref: ${{ github.event.pull_request.head.sha }}
102
111
  - uses: oven-sh/setup-bun@v2
103
- - run: bun install
104
- - run: bunx @varlock/bumpy ci check
112
+
113
+ # Resolve bumpy's version from the base branch (trusted) — not the PR's
114
+ # package.json (which a fork PR could swap to a malicious version).
115
+ # Change "main" to your base branch if different.
116
+ - name: Resolve bumpy version from base
117
+ run: |
118
+ git fetch origin main --depth=1
119
+ VERSION=$(git show "origin/main:package.json" \
120
+ | jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' \
121
+ | sed 's/[\^~]//')
122
+ echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"
123
+
124
+ - run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check
105
125
  env:
106
126
  GH_TOKEN: ${{ github.token }}
107
127
  ```
@@ -24,6 +24,10 @@ const LEVELS = [
24
24
  * - Changed packages default to "patch", unchanged to "none"
25
25
  * - Enter to confirm
26
26
  * - Ctrl+C / Escape to cancel
27
+ *
28
+ * Renders a viewport that fits within the terminal so the list scrolls instead of
29
+ * overflowing — otherwise large package counts cause the redraw cursor-up to lose
30
+ * its anchor once content scrolls off-screen.
27
31
  */
28
32
  async function bumpSelectPrompt(items) {
29
33
  const changedEntries = items.map((item, idx) => ({
@@ -35,7 +39,44 @@ async function bumpSelectPrompt(items) {
35
39
  idx
36
40
  })).filter(({ item }) => !item.changed);
37
41
  const displayOrder = [...changedEntries, ...unchangedEntries];
42
+ const rows = [];
43
+ const itemRowIndex = [];
44
+ {
45
+ let displayIdx = 0;
46
+ if (changedEntries.length > 0) {
47
+ rows.push({
48
+ kind: "header",
49
+ text: "Changed"
50
+ });
51
+ for (const { idx } of changedEntries) {
52
+ itemRowIndex.push(rows.length);
53
+ rows.push({
54
+ kind: "item",
55
+ itemIdx: idx,
56
+ displayIdx
57
+ });
58
+ displayIdx++;
59
+ }
60
+ if (unchangedEntries.length > 0) rows.push({ kind: "separator" });
61
+ }
62
+ if (unchangedEntries.length > 0) {
63
+ rows.push({
64
+ kind: "header",
65
+ text: "Unchanged"
66
+ });
67
+ for (const { idx } of unchangedEntries) {
68
+ itemRowIndex.push(rows.length);
69
+ rows.push({
70
+ kind: "item",
71
+ itemIdx: idx,
72
+ displayIdx
73
+ });
74
+ displayIdx++;
75
+ }
76
+ }
77
+ }
38
78
  let cursor = 0;
79
+ let scroll = 0;
39
80
  const levels = items.map((item) => item.initialLevel !== void 0 ? item.initialLevel : item.changed ? "patch" : "skip");
40
81
  return new Promise((resolve) => {
41
82
  const { stdin, stdout } = process;
@@ -57,38 +98,77 @@ async function bumpSelectPrompt(items) {
57
98
  if (selected.length === 0) lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("(none selected)")}`);
58
99
  else for (const { item, idx } of selected) lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.cyan(item.name)} ${import_picocolors.default.dim("→")} ${import_picocolors.default.bold(levels[idx])}`);
59
100
  lines.push(import_picocolors.default.dim("│"));
101
+ const output = lines.join("\n") + "\n";
102
+ stdout.write(output);
103
+ renderedLines = lines.length;
104
+ return;
105
+ }
106
+ const headerChrome = [
107
+ `${import_picocolors.default.cyan("◆")} Select bump levels`,
108
+ `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("↑/↓ navigate · ←/→ change level · enter to confirm")}`,
109
+ `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("0 skip current · x skip all · r reset all to defaults")}`,
110
+ import_picocolors.default.dim("│")
111
+ ];
112
+ const selectedCount = levels.filter((l) => l !== "skip").length;
113
+ const footerChrome = [
114
+ import_picocolors.default.dim("│"),
115
+ `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`${selectedCount} package${selectedCount !== 1 ? "s" : ""} selected`)}`,
116
+ import_picocolors.default.dim("└")
117
+ ];
118
+ const termRows = stdout.rows || 24;
119
+ const chromeLines = headerChrome.length + footerChrome.length;
120
+ const MIN_BODY = 3;
121
+ const availableBody = Math.max(MIN_BODY, termRows - chromeLines - 1);
122
+ let visibleRows;
123
+ let topIndicator = null;
124
+ let bottomIndicator = null;
125
+ let stickyHeader = null;
126
+ if (rows.length <= availableBody) {
127
+ visibleRows = rows;
128
+ scroll = 0;
60
129
  } else {
61
- lines.push(`${import_picocolors.default.cyan("◆")} Select bump levels`);
62
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("↑/↓ navigate · ←/→ change level · enter to confirm")}`);
63
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("0 skip current · x skip all · r reset all to defaults")}`);
64
- lines.push(import_picocolors.default.dim("│"));
65
- let displayIdx = 0;
66
- if (changedEntries.length > 0) {
67
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.underline("Changed")}`);
68
- for (const { item, idx } of changedEntries) {
69
- lines.push(formatRow(item, levels[idx], cursor === displayIdx));
70
- displayIdx++;
71
- }
72
- if (unchangedEntries.length > 0) lines.push(import_picocolors.default.dim("│"));
130
+ let windowSize = Math.max(MIN_BODY, availableBody - 2);
131
+ const focusedRowIdx = itemRowIndex[cursor];
132
+ const adjustScroll = () => {
133
+ if (focusedRowIdx < scroll) scroll = focusedRowIdx;
134
+ else if (focusedRowIdx >= scroll + windowSize) scroll = focusedRowIdx - windowSize + 1;
135
+ scroll = Math.max(0, Math.min(scroll, rows.length - windowSize));
136
+ };
137
+ adjustScroll();
138
+ const section = getCurrentSection(cursor, changedEntries.length, unchangedEntries.length);
139
+ if (section !== null && section.headerRowIdx < scroll) {
140
+ windowSize = Math.max(MIN_BODY, windowSize - 1);
141
+ adjustScroll();
142
+ stickyHeader = `${import_picocolors.default.dim("│")} ${import_picocolors.default.underline(section.name)}`;
73
143
  }
74
- if (unchangedEntries.length > 0) {
75
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.underline("Unchanged")}`);
76
- for (const { item, idx } of unchangedEntries) {
77
- lines.push(formatRow(item, levels[idx], cursor === displayIdx));
78
- displayIdx++;
79
- }
80
- }
81
- lines.push(import_picocolors.default.dim("│"));
82
- const selectedCount = levels.filter((l) => l !== "skip").length;
83
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`${selectedCount} package${selectedCount !== 1 ? "s" : ""} selected`)}`);
84
- lines.push(`${import_picocolors.default.dim("")}`);
144
+ visibleRows = rows.slice(scroll, scroll + windowSize);
145
+ const above = scroll;
146
+ const below = rows.length - (scroll + windowSize);
147
+ if (above > 0) topIndicator = `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`▲ ${above} more`)}`;
148
+ if (below > 0) bottomIndicator = `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`▼ ${below} more`)}`;
149
+ }
150
+ lines.push(...headerChrome);
151
+ if (topIndicator !== null) lines.push(topIndicator);
152
+ if (stickyHeader !== null) lines.push(stickyHeader);
153
+ for (const row of visibleRows) if (row.kind === "separator") lines.push(import_picocolors.default.dim("│"));
154
+ else if (row.kind === "header") lines.push(`${import_picocolors.default.dim("")} ${import_picocolors.default.underline(row.text)}`);
155
+ else {
156
+ const item = items[row.itemIdx];
157
+ const isFocused = row.displayIdx === cursor;
158
+ lines.push(formatRow(item, levels[row.itemIdx], isFocused));
85
159
  }
160
+ if (bottomIndicator !== null) lines.push(bottomIndicator);
161
+ lines.push(...footerChrome);
86
162
  const output = lines.join("\n") + "\n";
87
163
  stdout.write(output);
88
164
  renderedLines = lines.length;
89
165
  }
166
+ function onResize() {
167
+ render();
168
+ }
90
169
  function cleanup() {
91
170
  stdin.removeListener("keypress", onKeypress);
171
+ stdout.removeListener("resize", onResize);
92
172
  rl.close();
93
173
  stdout.write("\x1B[?25h");
94
174
  if (stdin.isTTY) stdin.setRawMode(false);
@@ -141,8 +221,24 @@ async function bumpSelectPrompt(items) {
141
221
  render();
142
222
  }
143
223
  stdin.on("keypress", onKeypress);
224
+ stdout.on("resize", onResize);
144
225
  });
145
226
  }
227
+ /** Returns the section the focused item is in, plus the row index of its header. */
228
+ function getCurrentSection(cursor, changedCount, unchangedCount) {
229
+ if (cursor < changedCount) {
230
+ if (changedCount === 0) return null;
231
+ return {
232
+ headerRowIdx: 0,
233
+ name: "Changed"
234
+ };
235
+ }
236
+ if (unchangedCount === 0) return null;
237
+ return {
238
+ headerRowIdx: changedCount > 0 ? changedCount + 2 : 0,
239
+ name: "Unchanged"
240
+ };
241
+ }
146
242
  function formatRow(item, level, focused) {
147
243
  return `${import_picocolors.default.dim("│")} ${focused ? import_picocolors.default.cyan("›") : " "} ${focused ? import_picocolors.default.cyan(item.name) : item.name} ${import_picocolors.default.dim(`(${item.version})`)} ${formatLevel(level, focused)}`;
148
244
  }
@@ -8,7 +8,7 @@ import { f as withGitToken, i as getChangedFiles } from "./git-DJJ64SW9.mjs";
8
8
  import { t as randomName } from "./names-COooXAFg.mjs";
9
9
  import { n as findChangedPackages } from "./check-CsF0zh8r.mjs";
10
10
  import { t as resolveCommitMessage } from "./commit-message-CSWVKPJ-.mjs";
11
- import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
11
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { createHash } from "node:crypto";
13
13
  //#region src/commands/ci.ts
14
14
  /**
@@ -157,7 +157,7 @@ async function ciPlanCommand(rootDir) {
157
157
  packageNames: plan.releases.map((r) => r.name)
158
158
  };
159
159
  else {
160
- const { findUnpublishedPackages } = await import("./publish-BDA1bOZ7.mjs");
160
+ const { findUnpublishedPackages } = await import("./publish-h6rM58Cq.mjs");
161
161
  const unpublished = await findUnpublishedPackages(packages, config);
162
162
  if (unpublished.length > 0) output = {
163
163
  mode: "publish",
@@ -234,7 +234,7 @@ async function ciReleaseCommand(rootDir, opts) {
234
234
  const msg = bumpFiles.length === 0 ? "No pending bump files — checking for unpublished packages..." : "Bump files found but no packages would be released — checking for unpublished packages...";
235
235
  log.info(msg);
236
236
  const recoveredBumpFiles = recoverDeletedBumpFiles(rootDir);
237
- const { publishCommand } = await import("./publish-BDA1bOZ7.mjs");
237
+ const { publishCommand } = await import("./publish-h6rM58Cq.mjs");
238
238
  await publishCommand(rootDir, {
239
239
  tag: opts.tag,
240
240
  recoveredBumpFiles
@@ -287,7 +287,7 @@ async function autoPublish(rootDir, config, plan, tag) {
287
287
  ], { cwd: rootDir });
288
288
  }
289
289
  log.step("Running bumpy publish...");
290
- const { publishCommand } = await import("./publish-BDA1bOZ7.mjs");
290
+ const { publishCommand } = await import("./publish-h6rM58Cq.mjs");
291
291
  await publishCommand(rootDir, { tag });
292
292
  }
293
293
  /**
@@ -721,6 +721,7 @@ async function postOrUpdatePrComment(prNumber, body, rootDir) {
721
721
  }
722
722
  } catch (err) {
723
723
  log.warn(` Failed to comment on PR: ${err instanceof Error ? err.message : err}`);
724
+ if (process.env.GITHUB_EVENT_NAME === "pull_request" && isForkPr()) log.warn(" This PR is from a fork. Fork PRs running on `pull_request` get a read-only token\n and cannot post comments. Switch the workflow to `pull_request_target` —\n see https://bumpy.varlock.dev/docs/github-actions");
724
725
  }
725
726
  }
726
727
  function detectPrBranch(rootDir) {
@@ -736,9 +737,14 @@ function detectPrBranch(rootDir) {
736
737
  ], { cwd: rootDir })?.trim() || null;
737
738
  }
738
739
  function detectPrNumber() {
739
- if (process.env.GITHUB_EVENT_NAME === "pull_request") {
740
- const match = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\//);
741
- if (match) return match[1];
740
+ const eventName = process.env.GITHUB_EVENT_NAME;
741
+ if (eventName === "pull_request" || eventName === "pull_request_target") {
742
+ const num = readGitHubEventPayload()?.pull_request?.number;
743
+ if (typeof num === "number") return String(num);
744
+ if (eventName === "pull_request") {
745
+ const match = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\//);
746
+ if (match) return match[1];
747
+ }
742
748
  }
743
749
  const envPr = process.env.BUMPY_PR_NUMBER || process.env.PR_NUMBER || null;
744
750
  if (envPr && !/^\d+$/.test(envPr)) {
@@ -747,5 +753,21 @@ function detectPrNumber() {
747
753
  }
748
754
  return envPr;
749
755
  }
756
+ function readGitHubEventPayload() {
757
+ const path = process.env.GITHUB_EVENT_PATH;
758
+ if (!path) return null;
759
+ try {
760
+ return JSON.parse(readFileSync(path, "utf-8"));
761
+ } catch {
762
+ return null;
763
+ }
764
+ }
765
+ /** True when running on a PR whose head repo differs from the base repo (i.e. a fork). */
766
+ function isForkPr() {
767
+ const event = readGitHubEventPayload();
768
+ const headId = event?.pull_request?.head?.repo?.id;
769
+ const baseId = event?.pull_request?.base?.repo?.id;
770
+ return typeof headId === "number" && typeof baseId === "number" && headId !== baseId;
771
+ }
750
772
  //#endregion
751
773
  export { CI_PLAN_CACHE_PATH, ciCheckCommand, ciPlanCommand, ciReleaseCommand };
package/dist/cli.mjs CHANGED
@@ -31,7 +31,7 @@ async function main() {
31
31
  }
32
32
  case "add": {
33
33
  const rootDir = await findRoot();
34
- const { addCommand } = await import("./add-DQA1TVHA.mjs");
34
+ const { addCommand } = await import("./add-5how2kia.mjs");
35
35
  await addCommand(rootDir, {
36
36
  packages: flags.packages,
37
37
  message: flags.message,
@@ -89,17 +89,17 @@ async function main() {
89
89
  const subcommand = args[1];
90
90
  const ciFlags = parseFlags(args.slice(2));
91
91
  if (subcommand === "check") {
92
- const { ciCheckCommand } = await import("./ci-DBZW9k4S.mjs");
92
+ const { ciCheckCommand } = await import("./ci-CIamssoq.mjs");
93
93
  await ciCheckCommand(rootDir, {
94
94
  comment: ciFlags.comment !== void 0 ? ciFlags.comment === true : void 0,
95
95
  strict: ciFlags.strict === true,
96
96
  noFail: ciFlags["no-fail"] === true
97
97
  });
98
98
  } else if (subcommand === "plan") {
99
- const { ciPlanCommand } = await import("./ci-DBZW9k4S.mjs");
99
+ const { ciPlanCommand } = await import("./ci-CIamssoq.mjs");
100
100
  await ciPlanCommand(rootDir);
101
101
  } else if (subcommand === "release") {
102
- const { ciReleaseCommand } = await import("./ci-DBZW9k4S.mjs");
102
+ const { ciReleaseCommand } = await import("./ci-CIamssoq.mjs");
103
103
  const expectModeFlag = ciFlags["expect-mode"];
104
104
  const autoPublishFlag = ciFlags["auto-publish"] === true;
105
105
  if (expectModeFlag !== void 0 && expectModeFlag !== "version-pr" && expectModeFlag !== "publish") {
@@ -127,7 +127,7 @@ async function main() {
127
127
  }
128
128
  case "publish": {
129
129
  const rootDir = await findRoot();
130
- const { publishCommand } = await import("./publish-BDA1bOZ7.mjs");
130
+ const { publishCommand } = await import("./publish-h6rM58Cq.mjs");
131
131
  await publishCommand(rootDir, {
132
132
  dryRun: flags["dry-run"] === true,
133
133
  tag: flags.tag,
@@ -151,7 +151,7 @@ async function main() {
151
151
  }
152
152
  case "--version":
153
153
  case "-v":
154
- console.log(`bumpy 1.12.0`);
154
+ console.log(`bumpy 1.13.1`);
155
155
  break;
156
156
  case "help":
157
157
  case "--help":
@@ -171,7 +171,7 @@ async function main() {
171
171
  }
172
172
  function printHelp() {
173
173
  console.log(`
174
- ${colorize(`🐸 bumpy v1.12.0`, "bold")} - Modern monorepo versioning
174
+ ${colorize(`🐸 bumpy v1.13.1`, "bold")} - Modern monorepo versioning
175
175
 
176
176
  Usage: bumpy <command> [options]
177
177
 
@@ -7,7 +7,7 @@ import { r as runArgsAsync, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
7
7
  import { i as loadFormatter, n as generateChangelogEntry } from "./changelog-CbaET5V6.mjs";
8
8
  import { c as hasUncommittedChanges, l as pushWithTags } from "./git-DJJ64SW9.mjs";
9
9
  import { t as publishPackages } from "./publish-pipeline-DSj14dW6.mjs";
10
- import { CI_PLAN_CACHE_PATH } from "./ci-DBZW9k4S.mjs";
10
+ import { CI_PLAN_CACHE_PATH } from "./ci-CIamssoq.mjs";
11
11
  //#region src/core/github-release.ts
12
12
  /** Get the current HEAD commit SHA */
13
13
  function getHeadSha(rootDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varlock/bumpy",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "description": "Modern monorepo versioning and changelog tool",
5
5
  "keywords": [
6
6
  "bump",