@pubm/plugin-brew 0.4.7 → 0.4.9

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,3 +1,3 @@
1
- import type { PubmPlugin } from "@pubm/core";
1
+ import { type PubmPlugin } from "@pubm/core";
2
2
  import type { BrewCoreOptions } from "./types.js";
3
3
  export declare function brewCore(options: BrewCoreOptions): PubmPlugin;
@@ -1,3 +1,3 @@
1
- import type { PubmPlugin } from "@pubm/core";
1
+ import { type PubmPlugin } from "@pubm/core";
2
2
  import type { BrewTapOptions } from "./types.js";
3
3
  export declare function brewTap(options: BrewTapOptions): PubmPlugin;
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { dirname, join, resolve } from "node:path";
8
+ import { resolvePhases } from "@pubm/core";
8
9
 
9
10
  // src/formula.ts
10
11
  function toClassName(name) {
@@ -204,8 +205,64 @@ function brewCore(options) {
204
205
  ]
205
206
  }
206
207
  ],
208
+ credentials: (ctx) => {
209
+ if (ctx.options.mode !== "ci")
210
+ return [];
211
+ return [
212
+ {
213
+ key: "brew-github-token",
214
+ env: "PUBM_BREW_GITHUB_TOKEN",
215
+ label: "GitHub PAT for Homebrew (homebrew-core)",
216
+ tokenUrl: "https://github.com/settings/tokens/new?scopes=repo,workflow",
217
+ tokenUrlLabel: "github.com",
218
+ ghSecretName: "PUBM_BREW_GITHUB_TOKEN",
219
+ required: true
220
+ }
221
+ ];
222
+ },
223
+ checks: (ctx) => {
224
+ const phases = resolvePhases(ctx.options);
225
+ if (!phases.includes("publish") && ctx.options.mode !== "ci")
226
+ return [];
227
+ if (ctx.options.mode === "ci") {
228
+ return [
229
+ {
230
+ title: "Checking Homebrew core token availability",
231
+ phase: "conditions",
232
+ task: async (ctx2, task) => {
233
+ const token = ctx2.runtime.pluginTokens?.["brew-github-token"];
234
+ if (!token) {
235
+ throw new Error("PUBM_BREW_GITHUB_TOKEN is required for homebrew-core publishing.");
236
+ }
237
+ task.output = "Homebrew core token verified";
238
+ }
239
+ }
240
+ ];
241
+ }
242
+ return [
243
+ {
244
+ title: "Checking GitHub CLI access for homebrew-core",
245
+ phase: "conditions",
246
+ task: async (_ctx, task) => {
247
+ const { execFileSync } = await import("node:child_process");
248
+ try {
249
+ execFileSync("gh", ["auth", "status"], { stdio: "pipe" });
250
+ task.output = "GitHub CLI authenticated";
251
+ } catch {
252
+ throw new Error("GitHub CLI is not authenticated. Run `gh auth login` first.");
253
+ }
254
+ try {
255
+ execFileSync("gh", ["repo", "view", "homebrew/homebrew-core", "--json", "name"], { stdio: "pipe" });
256
+ task.output = "Access to homebrew/homebrew-core verified";
257
+ } catch {
258
+ throw new Error("Cannot access homebrew/homebrew-core. Check your GitHub permissions.");
259
+ }
260
+ }
261
+ }
262
+ ];
263
+ },
207
264
  hooks: {
208
- afterRelease: async (_ctx, releaseCtx) => {
265
+ afterRelease: async (ctx, releaseCtx) => {
209
266
  if (options.packageName && releaseCtx.displayLabel !== options.packageName) {
210
267
  return;
211
268
  }
@@ -215,16 +272,26 @@ function brewCore(options) {
215
272
  const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf-8")) : {};
216
273
  const name = pkg.name?.replace(/^@[^/]+\//, "") ?? "my-tool";
217
274
  const formulaAssets = releaseAssetsToFormulaAssets(releaseCtx.assets, options.assetPlatforms);
275
+ const token = ctx.runtime.pluginTokens?.["brew-github-token"];
276
+ const ghEnv = token ? { env: { ...process.env, GH_TOKEN: token } } : {};
218
277
  try {
219
278
  execSync2("gh repo fork homebrew/homebrew-core --clone=false", {
220
- stdio: "pipe"
279
+ stdio: "pipe",
280
+ ...ghEnv
221
281
  });
222
282
  } catch {}
223
283
  const username = execSync2("gh api user --jq .login", {
224
- encoding: "utf-8"
284
+ encoding: "utf-8",
285
+ ...ghEnv
225
286
  }).trim();
226
287
  const tmpDir = join(tmpdir(), `pubm-brew-core-${Date.now()}`);
227
- execSync2(`git clone --depth 1 https://github.com/${username}/homebrew-core.git ${tmpDir}`, { stdio: "inherit" });
288
+ let cloneUrl = `https://github.com/${username}/homebrew-core.git`;
289
+ if (token) {
290
+ cloneUrl = `https://x-access-token:${token}@github.com/${username}/homebrew-core.git`;
291
+ }
292
+ execSync2(`git clone --depth 1 ${cloneUrl} ${tmpDir}`, {
293
+ stdio: "inherit"
294
+ });
228
295
  ensureGitIdentity(tmpDir);
229
296
  const formulaPath = join(tmpDir, "Formula", `${name}.rb`);
230
297
  let content;
@@ -251,10 +318,21 @@ function brewCore(options) {
251
318
  `git commit -m "${name} ${releaseCtx.version}"`,
252
319
  `git push origin ${branchName}`
253
320
  ].join(" && "), { stdio: "inherit" });
254
- execSync2([
321
+ const prUrl = execSync2([
255
322
  `cd ${tmpDir}`,
256
323
  `gh pr create --repo homebrew/homebrew-core --title "${name} ${releaseCtx.version}" --body "Update ${name} formula to version ${releaseCtx.version}"`
257
- ].join(" && "), { stdio: "inherit" });
324
+ ].join(" && "), { encoding: "utf-8", ...ghEnv }).trim();
325
+ const prNumber = prUrl.match(/\/pull\/(\d+)/)?.[1];
326
+ if (prNumber) {
327
+ ctx.runtime.rollback.add({
328
+ label: `Close homebrew-core PR #${prNumber}`,
329
+ fn: async () => {
330
+ const { execSync: execSyncRb } = await import("node:child_process");
331
+ execSyncRb(`gh pr close ${prNumber} --repo homebrew/homebrew-core --comment "Closed by pubm rollback"`, { stdio: "inherit", ...ghEnv });
332
+ },
333
+ confirm: true
334
+ });
335
+ }
258
336
  console.log(`PR created to homebrew/homebrew-core for ${name} ${releaseCtx.version}`);
259
337
  }
260
338
  }
@@ -263,6 +341,7 @@ function brewCore(options) {
263
341
  // src/brew-tap.ts
264
342
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
265
343
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
344
+ import { resolvePhases as resolvePhases2 } from "@pubm/core";
266
345
  function brewTap(options) {
267
346
  return {
268
347
  name: "@pubm/plugin-brew-tap",
@@ -299,8 +378,72 @@ function brewTap(options) {
299
378
  ]
300
379
  }
301
380
  ],
381
+ credentials: (ctx) => {
382
+ if (!options.repo || ctx.options.mode !== "ci")
383
+ return [];
384
+ return [
385
+ {
386
+ key: "brew-github-token",
387
+ env: "PUBM_BREW_GITHUB_TOKEN",
388
+ label: "GitHub PAT for Homebrew tap",
389
+ tokenUrl: "https://github.com/settings/tokens/new?scopes=repo",
390
+ tokenUrlLabel: "github.com",
391
+ ghSecretName: "PUBM_BREW_GITHUB_TOKEN",
392
+ required: true
393
+ }
394
+ ];
395
+ },
396
+ checks: (ctx) => {
397
+ const phases = resolvePhases2(ctx.options);
398
+ if (!phases.includes("publish") && ctx.options.mode !== "ci")
399
+ return [];
400
+ if (ctx.options.mode === "ci") {
401
+ if (!options.repo)
402
+ return [];
403
+ return [
404
+ {
405
+ title: "Checking Homebrew tap token availability",
406
+ phase: "conditions",
407
+ task: async (ctx2, task) => {
408
+ const token = ctx2.runtime.pluginTokens?.["brew-github-token"];
409
+ if (!token) {
410
+ throw new Error("PUBM_BREW_GITHUB_TOKEN is required for Homebrew tap publishing.");
411
+ }
412
+ task.output = "Homebrew tap token verified";
413
+ }
414
+ }
415
+ ];
416
+ }
417
+ if (!options.repo)
418
+ return [];
419
+ const targetRepo = options.repo;
420
+ return [
421
+ {
422
+ title: "Checking git/gh access for Homebrew tap",
423
+ phase: "conditions",
424
+ task: async (_ctx, task) => {
425
+ const { execFileSync } = await import("node:child_process");
426
+ try {
427
+ execFileSync("gh", ["auth", "status"], { stdio: "pipe" });
428
+ task.output = "GitHub CLI authenticated";
429
+ } catch {
430
+ throw new Error("GitHub CLI is not authenticated. Run `gh auth login` first.");
431
+ }
432
+ const repoName = /^[^/]+\/[^/]+$/.test(targetRepo) ? targetRepo : targetRepo.match(/github\.com[/:]([^/]+\/[^/.]+)/)?.[1];
433
+ if (repoName) {
434
+ try {
435
+ execFileSync("gh", ["repo", "view", repoName, "--json", "name"], { stdio: "pipe" });
436
+ task.output = `Access to ${repoName} verified`;
437
+ } catch {
438
+ throw new Error(`Cannot access tap repository '${targetRepo}'. Check your GitHub permissions.`);
439
+ }
440
+ }
441
+ }
442
+ }
443
+ ];
444
+ },
302
445
  hooks: {
303
- afterRelease: async (_ctx, releaseCtx) => {
446
+ afterRelease: async (ctx, releaseCtx) => {
304
447
  if (options.packageName && releaseCtx.displayLabel !== options.packageName) {
305
448
  return;
306
449
  }
@@ -327,7 +470,7 @@ function brewTap(options) {
327
470
  mkdirSync2(dirname2(formulaPath), { recursive: true });
328
471
  writeFileSync2(formulaPath, content);
329
472
  console.log(`Formula updated at ${options.formula}`);
330
- if (!options.repo) {
473
+ {
331
474
  const { execSync: execSync2 } = await import("node:child_process");
332
475
  ensureGitIdentity();
333
476
  execSync2(`git add ${formulaPath}`, { stdio: "inherit" });
@@ -338,16 +481,24 @@ function brewTap(options) {
338
481
  const branch = `pubm/brew-formula-v${releaseCtx.version}`;
339
482
  execSync2(`git checkout -b ${branch}`, { stdio: "inherit" });
340
483
  execSync2(`git push origin ${branch}`, { stdio: "inherit" });
341
- execSync2(`gh pr create --title "chore(brew): update formula to ${releaseCtx.version}" --body "Automated formula update by pubm"`, { stdio: "inherit" });
484
+ const prUrl = execSync2(`gh pr create --title "chore(brew): update formula to ${releaseCtx.version}" --body "Automated formula update by pubm"`, { encoding: "utf-8" }).trim();
485
+ const prNumber = prUrl.match(/\/pull\/(\d+)/)?.[1];
486
+ if (prNumber) {
487
+ ctx.runtime.rollback.add({
488
+ label: `Close Homebrew tap PR #${prNumber}`,
489
+ fn: async () => {
490
+ execSync2(`gh pr close ${prNumber} --comment "Closed by pubm rollback"`, { stdio: "inherit" });
491
+ },
492
+ confirm: true
493
+ });
494
+ }
342
495
  console.log(`Created PR on branch ${branch}`);
343
496
  }
344
- return;
345
497
  }
346
498
  if (options.repo) {
347
499
  const { tmpdir: tmpdir2 } = await import("node:os");
348
500
  const { basename, join: join2 } = await import("node:path");
349
501
  const { execSync: execSync2 } = await import("node:child_process");
350
- const { resolveGitHubToken } = await import("@pubm/core");
351
502
  const tmpDir = join2(tmpdir2(), `pubm-brew-tap-${Date.now()}`);
352
503
  const formulaFile = basename(formulaPath);
353
504
  const isShorthand = /^[^/]+\/[^/]+$/.test(options.repo);
@@ -355,10 +506,11 @@ function brewTap(options) {
355
506
  const ownerRepoMatch = repoUrl.match(/github\.com[/:]([^/]+\/[^/.]+?)(?:\.git)?$/);
356
507
  const ownerRepo = ownerRepoMatch?.[1] ?? options.repo;
357
508
  let cloneUrl = repoUrl;
358
- const tokenResult = resolveGitHubToken();
359
- if (tokenResult?.token && repoUrl.startsWith("https://github.com/")) {
360
- cloneUrl = repoUrl.replace("https://github.com/", `https://x-access-token:${tokenResult.token}@github.com/`);
509
+ const token = ctx.runtime.pluginTokens?.["brew-github-token"];
510
+ if (token && repoUrl.startsWith("https://github.com/")) {
511
+ cloneUrl = repoUrl.replace("https://github.com/", `https://x-access-token:${token}@github.com/`);
361
512
  }
513
+ const ghEnv = token ? { env: { ...process.env, GH_TOKEN: token } } : {};
362
514
  execSync2(`git clone --depth 1 ${cloneUrl} ${tmpDir}`, {
363
515
  stdio: "inherit"
364
516
  });
@@ -381,7 +533,18 @@ function brewTap(options) {
381
533
  execSync2(`cd ${tmpDir} && git push origin ${branch}`, {
382
534
  stdio: "inherit"
383
535
  });
384
- execSync2(`gh pr create --repo ${ownerRepo} --title "chore(brew): update formula to ${releaseCtx.version}" --body "Automated formula update by pubm"`, { stdio: "inherit" });
536
+ const prUrl = execSync2(`gh pr create --repo ${ownerRepo} --title "chore(brew): update formula to ${releaseCtx.version}" --body "Automated formula update by pubm"`, { encoding: "utf-8", ...ghEnv }).trim();
537
+ const prNumber = prUrl.match(/\/pull\/(\d+)/)?.[1];
538
+ if (prNumber) {
539
+ const repoFlag = ownerRepo;
540
+ ctx.runtime.rollback.add({
541
+ label: `Close Homebrew tap PR #${prNumber} (${repoFlag})`,
542
+ fn: async () => {
543
+ execSync2(`gh pr close ${prNumber} --repo ${repoFlag} --comment "Closed by pubm rollback"`, { stdio: "inherit", ...ghEnv });
544
+ },
545
+ confirm: true
546
+ });
547
+ }
385
548
  console.log(`Created PR on branch ${branch}`);
386
549
  }
387
550
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubm/plugin-brew",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "type": "module",
5
5
  "description": "pubm plugin for Homebrew formula publishing workflows",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "license": "Apache-2.0",
25
25
  "peerDependencies": {
26
- "@pubm/core": ">=0.4.3"
26
+ "@pubm/core": ">=0.4.8"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"