@quiltdata/benchling-webhook 0.21.2-20261007T162722Z → 0.21.3

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,16 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * Version management script - bumps version numbers and creates release tags
4
+ * Version management and release-tag tooling.
5
5
  *
6
- * Usage:
7
- * node bin/version.ts # Show all three version files
8
- * node bin/version.ts patch # 0.4.7 -> 0.4.8
9
- * node bin/version.ts minor # 0.4.7 -> 0.5.0
10
- * node bin/version.ts major # 0.4.7 -> 1.0.0
11
- * node bin/version.ts sync # Force TOML and YAML to match JSON version
12
- * node bin/version.ts tag # Create and push production release tag
13
- * node bin/version.ts tag dev # Create and push dev release tag
6
+ * Version bumps remain explicit commands. Release commands share tag construction,
7
+ * local/remote target verification, and annotated-tag creation semantics.
14
8
  */
15
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
10
  if (k2 === undefined) k2 = k;
@@ -46,13 +40,453 @@ var __importStar = (this && this.__importStar) || (function () {
46
40
  };
47
41
  })();
48
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.GitReleaseRepository = exports.defaultCommandRunner = exports.AUTOMATIC_RELEASE_TAGGER_EMAIL = exports.AUTOMATIC_RELEASE_TAGGER_NAME = void 0;
44
+ exports.isSemanticVersion = isSemanticVersion;
45
+ exports.parseReleaseVersion = parseReleaseVersion;
46
+ exports.compareReleaseVersions = compareReleaseVersions;
47
+ exports.productionTag = productionTag;
48
+ exports.devTag = devTag;
49
+ exports.annotatedTagMessage = annotatedTagMessage;
50
+ exports.parseTag = parseTag;
51
+ exports.isAutomaticReleaseTag = isAutomaticReleaseTag;
52
+ exports.runAutomaticRelease = runAutomaticRelease;
53
+ exports.validateReleaseContext = validateReleaseContext;
54
+ exports.runProductionGate = runProductionGate;
55
+ exports.main = main;
49
56
  const fs = __importStar(require("fs"));
50
57
  const path = __importStar(require("path"));
51
58
  const child_process_1 = require("child_process");
52
- const packagePath = path.join(__dirname, "..", "package.json");
53
- const pyprojectPath = path.join(__dirname, "..", "docker", "pyproject.toml");
54
- const appManifestPath = path.join(__dirname, "..", "docker", "app-manifest.yaml");
55
- const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
59
+ const projectRoot = path.join(__dirname, "..");
60
+ const packagePath = path.join(projectRoot, "package.json");
61
+ const pyprojectPath = path.join(projectRoot, "docker", "pyproject.toml");
62
+ const appManifestPath = path.join(projectRoot, "docker", "app-manifest.yaml");
63
+ exports.AUTOMATIC_RELEASE_TAGGER_NAME = "github-actions[bot]";
64
+ exports.AUTOMATIC_RELEASE_TAGGER_EMAIL = "41898282+github-actions[bot]@users.noreply.github.com";
65
+ const CORE_VERSION_PATTERN = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)";
66
+ const PLAIN_VERSION_PATTERN = new RegExp(`^${CORE_VERSION_PATTERN}$`);
67
+ const TIMESTAMP_VERSION_PATTERN = new RegExp(`^${CORE_VERSION_PATTERN}-(\\d{8}T\\d{6}Z)$`);
68
+ const SEMVER_PATTERN = new RegExp(`^${CORE_VERSION_PATTERN}(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
69
+ const defaultCommandRunner = (command, args, options) => {
70
+ const result = (0, child_process_1.spawnSync)(command, [...args], {
71
+ cwd: options.cwd,
72
+ encoding: "utf8",
73
+ stdio: options.inherit ? "inherit" : "pipe",
74
+ });
75
+ return {
76
+ status: result.status ?? 127,
77
+ stdout: result.stdout?.toString() ?? "",
78
+ stderr: [result.stderr?.toString(), result.error?.message].filter(Boolean).join("\n"),
79
+ };
80
+ };
81
+ exports.defaultCommandRunner = defaultCommandRunner;
82
+ function commandFailure(command, args, result) {
83
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit status ${result.status}`;
84
+ return new Error(`${command} ${args.join(" ")} failed: ${detail}`);
85
+ }
86
+ function runChecked(runner, command, args, cwd, inherit = false) {
87
+ const result = runner(command, args, { cwd, inherit });
88
+ if (result.status !== 0) {
89
+ throw commandFailure(command, args, result);
90
+ }
91
+ return result;
92
+ }
93
+ function normalizeSha(value, label) {
94
+ const sha = value.trim().toLowerCase();
95
+ if (!/^[0-9a-f]{40,64}$/.test(sha)) {
96
+ throw new Error(`${label} is not a full commit SHA: ${value || "<empty>"}`);
97
+ }
98
+ return sha;
99
+ }
100
+ function parseVersionParts(match) {
101
+ return {
102
+ major: Number.parseInt(match[1], 10),
103
+ minor: Number.parseInt(match[2], 10),
104
+ patch: Number.parseInt(match[3], 10),
105
+ };
106
+ }
107
+ function timestampIsValid(timestamp) {
108
+ const match = timestamp.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
109
+ if (!match) {
110
+ return false;
111
+ }
112
+ const iso = `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}.000Z`;
113
+ const date = new Date(iso);
114
+ return !Number.isNaN(date.valueOf()) && date.toISOString() === iso;
115
+ }
116
+ function isSemanticVersion(version) {
117
+ const match = version.match(SEMVER_PATTERN);
118
+ if (!match) {
119
+ return false;
120
+ }
121
+ const prerelease = match[4];
122
+ if (!prerelease) {
123
+ return true;
124
+ }
125
+ return prerelease.split(".").every((identifier) => !/^\d+$/.test(identifier) || identifier === "0" || !identifier.startsWith("0"));
126
+ }
127
+ function parseReleaseVersion(version) {
128
+ const stableMatch = version.match(PLAIN_VERSION_PATTERN);
129
+ if (stableMatch) {
130
+ return {
131
+ ...parseVersionParts(stableMatch),
132
+ raw: version,
133
+ prerelease: false,
134
+ };
135
+ }
136
+ const timestampMatch = version.match(TIMESTAMP_VERSION_PATTERN);
137
+ if (timestampMatch && timestampIsValid(timestampMatch[4])) {
138
+ return {
139
+ ...parseVersionParts(timestampMatch),
140
+ raw: version,
141
+ prerelease: true,
142
+ timestamp: timestampMatch[4],
143
+ };
144
+ }
145
+ throw new Error(`Unsupported release version: ${version}`);
146
+ }
147
+ function compareReleaseVersions(left, right) {
148
+ const a = parseReleaseVersion(left);
149
+ const b = parseReleaseVersion(right);
150
+ for (const key of ["major", "minor", "patch"]) {
151
+ if (a[key] !== b[key]) {
152
+ return a[key] < b[key] ? -1 : 1;
153
+ }
154
+ }
155
+ if (a.prerelease !== b.prerelease) {
156
+ return a.prerelease ? -1 : 1;
157
+ }
158
+ if (!a.prerelease) {
159
+ return 0;
160
+ }
161
+ return a.timestamp === b.timestamp ? 0 : a.timestamp < b.timestamp ? -1 : 1;
162
+ }
163
+ function productionTag(version) {
164
+ const parsed = parseReleaseVersion(version);
165
+ if (parsed.prerelease) {
166
+ throw new Error(`Production tags require a plain semantic version: ${version}`);
167
+ }
168
+ return `v${version}`;
169
+ }
170
+ function devTag(version, now = new Date()) {
171
+ productionTag(version);
172
+ const timestamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
173
+ return `v${version}-${timestamp}`;
174
+ }
175
+ function annotatedTagMessage(tag) {
176
+ const release = parseTag(tag);
177
+ return release.isPrerelease
178
+ ? `Development pre-release (dev) ${tag}\n\nThis is a pre-release for testing purposes.`
179
+ : `Release ${tag}`;
180
+ }
181
+ function parseTag(tag) {
182
+ if (!tag.startsWith("v")) {
183
+ throw new Error(`Release tag must start with v: ${tag}`);
184
+ }
185
+ const version = tag.slice(1);
186
+ const release = parseReleaseVersion(version);
187
+ const baseVersion = `${release.major}.${release.minor}.${release.patch}`;
188
+ return {
189
+ tag,
190
+ version,
191
+ baseVersion,
192
+ isPrerelease: release.prerelease,
193
+ };
194
+ }
195
+ class GitReleaseRepository {
196
+ constructor(cwd, runner = exports.defaultCommandRunner) {
197
+ this.cwd = cwd;
198
+ this.runner = runner;
199
+ }
200
+ git(args, inherit = false) {
201
+ return this.runner("git", args, { cwd: this.cwd, inherit });
202
+ }
203
+ resolveCommit(ref) {
204
+ const args = ["rev-parse", `${ref}^{commit}`];
205
+ const result = this.git(args);
206
+ if (result.status !== 0) {
207
+ throw commandFailure("git", args, result);
208
+ }
209
+ return normalizeSha(result.stdout, ref);
210
+ }
211
+ commitExists(ref) {
212
+ const args = ["cat-file", "-e", `${ref}^{commit}`];
213
+ const result = this.git(args);
214
+ if (result.status === 0) {
215
+ return true;
216
+ }
217
+ if (result.status === 1 || result.status === 128) {
218
+ return false;
219
+ }
220
+ throw commandFailure("git", args, result);
221
+ }
222
+ readFileAtCommit(commit, file) {
223
+ const args = ["show", `${commit}:${file}`];
224
+ const result = this.git(args);
225
+ if (result.status !== 0) {
226
+ throw commandFailure("git", args, result);
227
+ }
228
+ return result.stdout;
229
+ }
230
+ localTagTarget(tag) {
231
+ const ref = `refs/tags/${tag}`;
232
+ const checkArgs = ["show-ref", "--verify", "--quiet", ref];
233
+ const check = this.git(checkArgs);
234
+ if (check.status === 1) {
235
+ return null;
236
+ }
237
+ if (check.status !== 0) {
238
+ throw commandFailure("git", checkArgs, check);
239
+ }
240
+ return this.resolveCommit(ref);
241
+ }
242
+ fetchedTagAnnotation(tag) {
243
+ const typeArgs = ["cat-file", "-t", "FETCH_HEAD"];
244
+ const typeResult = this.git(typeArgs);
245
+ if (typeResult.status !== 0) {
246
+ throw commandFailure("git", typeArgs, typeResult);
247
+ }
248
+ const objectType = typeResult.stdout.trim();
249
+ if (objectType === "commit") {
250
+ return null;
251
+ }
252
+ if (objectType !== "tag") {
253
+ throw new Error(`Remote tag ${tag} points to unsupported object type ${objectType}`);
254
+ }
255
+ const contentArgs = ["cat-file", "tag", "FETCH_HEAD"];
256
+ const contentResult = this.git(contentArgs);
257
+ if (contentResult.status !== 0) {
258
+ throw commandFailure("git", contentArgs, contentResult);
259
+ }
260
+ const separator = contentResult.stdout.indexOf("\n\n");
261
+ if (separator < 0) {
262
+ throw new Error(`Remote annotated tag ${tag} has malformed content`);
263
+ }
264
+ const headers = contentResult.stdout.slice(0, separator).split("\n");
265
+ if (!headers.includes(`tag ${tag}`)) {
266
+ throw new Error(`Remote annotated tag ${tag} has a mismatched internal tag name`);
267
+ }
268
+ const taggerHeader = headers.find((line) => line.startsWith("tagger "));
269
+ const tagger = taggerHeader?.match(/^tagger (.*) <([^<>]+)> \d+ [+-]\d{4}$/);
270
+ if (!tagger) {
271
+ throw new Error(`Remote annotated tag ${tag} has a malformed tagger identity`);
272
+ }
273
+ return {
274
+ message: contentResult.stdout.slice(separator + 2).replace(/\n+$/, ""),
275
+ taggerName: tagger[1],
276
+ taggerEmail: tagger[2],
277
+ };
278
+ }
279
+ remoteTag(tag) {
280
+ const ref = `refs/tags/${tag}`;
281
+ const lookupArgs = ["ls-remote", "--exit-code", "--refs", "origin", ref];
282
+ const lookup = this.git(lookupArgs);
283
+ if (lookup.status === 2) {
284
+ return null;
285
+ }
286
+ if (lookup.status !== 0) {
287
+ throw commandFailure("git", lookupArgs, lookup);
288
+ }
289
+ const lines = lookup.stdout.trim().split("\n").filter(Boolean);
290
+ if (lines.length !== 1 || !lines[0].endsWith(`\t${ref}`)) {
291
+ throw new Error(`Remote lookup for ${tag} returned ambiguous data`);
292
+ }
293
+ const fetchArgs = ["fetch", "--force", "--no-tags", "origin", ref];
294
+ const fetch = this.git(fetchArgs);
295
+ if (fetch.status !== 0) {
296
+ throw commandFailure("git", fetchArgs, fetch);
297
+ }
298
+ return {
299
+ targetSha: this.resolveCommit("FETCH_HEAD"),
300
+ annotation: this.fetchedTagAnnotation(tag),
301
+ };
302
+ }
303
+ remoteTagTarget(tag) {
304
+ return this.remoteTag(tag)?.targetSha ?? null;
305
+ }
306
+ createAnnotatedTag(tag, message, targetSha) {
307
+ runChecked(this.runner, "git", ["tag", "-a", tag, "-m", message, targetSha], this.cwd, true);
308
+ }
309
+ pushTag(tag) {
310
+ const ref = `refs/tags/${tag}`;
311
+ return this.git(["push", "origin", `${ref}:${ref}`], true).status === 0;
312
+ }
313
+ isClean() {
314
+ const args = ["status", "--porcelain", "--untracked-files=normal"];
315
+ const result = this.git(args);
316
+ if (result.status !== 0) {
317
+ throw commandFailure("git", args, result);
318
+ }
319
+ return result.stdout.trim() === "";
320
+ }
321
+ }
322
+ exports.GitReleaseRepository = GitReleaseRepository;
323
+ function packageVersionFromContent(content, label) {
324
+ let value;
325
+ try {
326
+ value = JSON.parse(content);
327
+ }
328
+ catch (error) {
329
+ throw new Error(`Could not parse ${label}: ${error instanceof Error ? error.message : String(error)}`);
330
+ }
331
+ if (!value || typeof value !== "object" || typeof value.version !== "string") {
332
+ throw new Error(`${label} does not contain a string version`);
333
+ }
334
+ return value.version;
335
+ }
336
+ function assertSemanticVersion(version, label) {
337
+ if (!isSemanticVersion(version)) {
338
+ throw new Error(`${label} is malformed: ${version}`);
339
+ }
340
+ }
341
+ function assertExpectedTarget(actual, expected, label) {
342
+ if (normalizeSha(actual, label) !== expected) {
343
+ throw new Error(`${label} points to ${actual}, expected ${expected}`);
344
+ }
345
+ }
346
+ function resolveBeforeCommit(repository, beforeSha) {
347
+ const supplied = beforeSha?.trim();
348
+ if (supplied && !/^0+$/.test(supplied) && repository.commitExists(supplied)) {
349
+ return repository.resolveCommit(supplied);
350
+ }
351
+ return repository.resolveCommit("HEAD^");
352
+ }
353
+ function verifiedResult(outcome, releaseRequired, tag, targetSha) {
354
+ return {
355
+ outcome,
356
+ releaseRequired,
357
+ verified: true,
358
+ tag,
359
+ targetSha,
360
+ };
361
+ }
362
+ function isAutomaticReleaseTag(tag, remote) {
363
+ const annotation = remote.annotation;
364
+ return annotation !== null
365
+ && annotation.message === annotatedTagMessage(tag)
366
+ && annotation.taggerName === exports.AUTOMATIC_RELEASE_TAGGER_NAME
367
+ && annotation.taggerEmail === exports.AUTOMATIC_RELEASE_TAGGER_EMAIL;
368
+ }
369
+ function inspectExistingTag(repository, tag, targetSha) {
370
+ const local = repository.localTagTarget(tag);
371
+ if (local) {
372
+ assertExpectedTarget(local, targetSha, `Local tag ${tag}`);
373
+ }
374
+ const remote = repository.remoteTag(tag);
375
+ if (remote) {
376
+ assertExpectedTarget(remote.targetSha, targetSha, `Remote tag ${tag}`);
377
+ }
378
+ if (local && !remote) {
379
+ throw new Error(`Local tag ${tag} exists but the exact remote tag is absent`);
380
+ }
381
+ return { local, remote };
382
+ }
383
+ function runVerifiedProductionGate(options, targetSha) {
384
+ options.runProductionGate();
385
+ if (!options.repository.isClean()) {
386
+ throw new Error("Production gate left uncommitted changes in the working tree");
387
+ }
388
+ assertExpectedTarget(options.repository.resolveCommit("HEAD"), targetSha, "HEAD after production gate");
389
+ }
390
+ function runAutomaticRelease(options) {
391
+ const targetSha = normalizeSha(options.repository.resolveCommit("HEAD"), "HEAD");
392
+ const githubSha = normalizeSha(options.githubSha, "GITHUB_SHA");
393
+ if (targetSha !== githubSha) {
394
+ throw new Error(`Checked-out HEAD ${targetSha} does not match GITHUB_SHA ${githubSha}`);
395
+ }
396
+ const beforeCommit = resolveBeforeCommit(options.repository, options.beforeSha);
397
+ const previousVersion = packageVersionFromContent(options.repository.readFileAtCommit(beforeCommit, "package.json"), `package.json at ${beforeCommit}`);
398
+ assertSemanticVersion(previousVersion, "Previous package version");
399
+ assertSemanticVersion(options.currentVersion, "Current package version");
400
+ if (previousVersion === options.currentVersion) {
401
+ return {
402
+ outcome: "no-version-change",
403
+ releaseRequired: false,
404
+ verified: false,
405
+ tag: "",
406
+ targetSha,
407
+ };
408
+ }
409
+ if (!PLAIN_VERSION_PATTERN.test(options.currentVersion)) {
410
+ return {
411
+ outcome: "non-production-version",
412
+ releaseRequired: false,
413
+ verified: false,
414
+ tag: "",
415
+ targetSha,
416
+ };
417
+ }
418
+ const tag = productionTag(options.currentVersion);
419
+ const initial = inspectExistingTag(options.repository, tag, targetSha);
420
+ if (initial.remote) {
421
+ if (!isAutomaticReleaseTag(tag, initial.remote)) {
422
+ return verifiedResult("verified-existing-tag", false, tag, targetSha);
423
+ }
424
+ runVerifiedProductionGate(options, targetSha);
425
+ const recovered = inspectExistingTag(options.repository, tag, targetSha);
426
+ if (!recovered.remote || !isAutomaticReleaseTag(tag, recovered.remote)) {
427
+ throw new Error(`Automatic release tag ${tag} changed during recovery verification`);
428
+ }
429
+ return verifiedResult("verified-recovered-tag", true, tag, targetSha);
430
+ }
431
+ runVerifiedProductionGate(options, targetSha);
432
+ const afterGate = inspectExistingTag(options.repository, tag, targetSha);
433
+ if (afterGate.remote) {
434
+ return verifiedResult("verified-race", true, tag, targetSha);
435
+ }
436
+ if (afterGate.local) {
437
+ throw new Error(`Local tag ${tag} appeared without a verified remote tag`);
438
+ }
439
+ options.repository.createAnnotatedTag(tag, annotatedTagMessage(tag), targetSha);
440
+ const localTarget = options.repository.localTagTarget(tag);
441
+ if (!localTarget) {
442
+ throw new Error(`Created tag ${tag} cannot be resolved locally`);
443
+ }
444
+ assertExpectedTarget(localTarget, targetSha, `Created tag ${tag}`);
445
+ const pushed = options.repository.pushTag(tag);
446
+ const remote = options.repository.remoteTag(tag);
447
+ if (!remote) {
448
+ throw new Error(`Tag push ${pushed ? "reported success" : "failed"}, but remote tag ${tag} is absent`);
449
+ }
450
+ assertExpectedTarget(remote.targetSha, targetSha, `Remote tag ${tag} after push`);
451
+ return verifiedResult(pushed ? "created" : "verified-race", true, tag, targetSha);
452
+ }
453
+ function validateReleaseContext(repository, tag, packageVersion, githubSha) {
454
+ const parsed = parseTag(tag);
455
+ assertSemanticVersion(packageVersion, "Current package version");
456
+ if (parsed.baseVersion !== packageVersion) {
457
+ throw new Error(`Tag ${tag} does not match package version ${packageVersion}`);
458
+ }
459
+ const targetSha = normalizeSha(repository.resolveCommit("HEAD"), "HEAD");
460
+ const expectedSha = normalizeSha(githubSha, "GITHUB_SHA");
461
+ if (targetSha !== expectedSha) {
462
+ throw new Error(`Checked-out HEAD ${targetSha} does not match GITHUB_SHA ${expectedSha}`);
463
+ }
464
+ const local = repository.localTagTarget(tag);
465
+ if (!local) {
466
+ throw new Error(`Release tag ${tag} is not present locally`);
467
+ }
468
+ assertExpectedTarget(local, targetSha, `Local release tag ${tag}`);
469
+ const remote = repository.remoteTag(tag);
470
+ if (!remote) {
471
+ throw new Error(`Release tag ${tag} is not present on origin`);
472
+ }
473
+ assertExpectedTarget(remote.targetSha, targetSha, `Remote release tag ${tag}`);
474
+ return {
475
+ ...parsed,
476
+ verified: true,
477
+ targetSha,
478
+ };
479
+ }
480
+ function runProductionGate(repository, cwd = projectRoot, runner = exports.defaultCommandRunner) {
481
+ runChecked(runner, "npm", ["run", "version:verify"], cwd, true);
482
+ runChecked(runner, "npm", ["test"], cwd, true);
483
+ if (!repository.isClean()) {
484
+ throw new Error("Full npm test gate left uncommitted changes in the working tree");
485
+ }
486
+ }
487
+ function readPackage() {
488
+ return JSON.parse(fs.readFileSync(packagePath, "utf8"));
489
+ }
56
490
  function readPyprojectVersion() {
57
491
  const content = fs.readFileSync(pyprojectPath, "utf8");
58
492
  const match = content.match(/^version\s*=\s*"([^"]+)"/m);
@@ -64,42 +498,38 @@ function readAppManifestVersion() {
64
498
  return match ? match[1].trim() : null;
65
499
  }
66
500
  function parseVersion(version) {
67
- const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
501
+ const match = version.match(PLAIN_VERSION_PATTERN);
68
502
  if (!match) {
69
503
  throw new Error(`Invalid version format: ${version}`);
70
504
  }
71
- return {
72
- major: parseInt(match[1], 10),
73
- minor: parseInt(match[2], 10),
74
- patch: parseInt(match[3], 10),
75
- };
505
+ return parseVersionParts(match);
76
506
  }
77
- function formatVersion(ver) {
78
- return `${ver.major}.${ver.minor}.${ver.patch}`;
507
+ function formatVersion(version) {
508
+ return `${version.major}.${version.minor}.${version.patch}`;
79
509
  }
80
510
  function bumpVersion(currentVersion, bumpType) {
81
- const ver = parseVersion(currentVersion);
511
+ const version = parseVersion(currentVersion);
82
512
  switch (bumpType) {
83
513
  case "major":
84
- ver.major++;
85
- ver.minor = 0;
86
- ver.patch = 0;
514
+ version.major++;
515
+ version.minor = 0;
516
+ version.patch = 0;
87
517
  break;
88
518
  case "minor":
89
- ver.minor++;
90
- ver.patch = 0;
519
+ version.minor++;
520
+ version.patch = 0;
91
521
  break;
92
522
  case "patch":
93
- ver.patch++;
523
+ version.patch++;
94
524
  break;
95
525
  default:
96
526
  throw new Error(`Unknown bump type: ${bumpType}`);
97
527
  }
98
- return formatVersion(ver);
528
+ return formatVersion(version);
99
529
  }
100
- function updatePackageVersion(newVersion) {
530
+ function updatePackageVersion(pkg, newVersion) {
101
531
  pkg.version = newVersion;
102
- fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n");
532
+ fs.writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
103
533
  console.log(`✅ Updated package.json to version ${newVersion}`);
104
534
  }
105
535
  function updatePyprojectVersion(newVersion) {
@@ -107,17 +537,12 @@ function updatePyprojectVersion(newVersion) {
107
537
  content = content.replace(/^version\s*=\s*"[^"]+"/m, `version = "${newVersion}"`);
108
538
  fs.writeFileSync(pyprojectPath, content);
109
539
  console.log(`✅ Updated docker/pyproject.toml to version ${newVersion}`);
110
- // Run uv sync to update uv.lock
111
- try {
112
- const dockerDir = path.join(__dirname, "..", "docker");
113
- console.log("🔄 Running uv sync to update uv.lock...");
114
- (0, child_process_1.execSync)("uv sync", { cwd: dockerDir, stdio: "inherit" });
115
- console.log("✅ Updated docker/uv.lock");
540
+ const result = (0, exports.defaultCommandRunner)("uv", ["sync"], { cwd: path.join(projectRoot, "docker"), inherit: true });
541
+ if (result.status !== 0) {
542
+ console.error("⚠️ Warning: Failed to run uv sync; run it manually in docker/");
116
543
  }
117
- catch (error) {
118
- const message = error instanceof Error ? error.message : String(error);
119
- console.error("⚠️ Warning: Failed to run uv sync:", message);
120
- console.error(" You may need to run 'cd docker && uv sync' manually");
544
+ else {
545
+ console.log("✅ Updated docker/uv.lock");
121
546
  }
122
547
  }
123
548
  function updateAppManifestVersion(newVersion) {
@@ -126,215 +551,149 @@ function updateAppManifestVersion(newVersion) {
126
551
  fs.writeFileSync(appManifestPath, content);
127
552
  console.log(`✅ Updated docker/app-manifest.yaml to version ${newVersion}`);
128
553
  }
129
- function createGitTag(version, isDev, noPush) {
130
- let tagName = `v${version}`;
131
- // For dev releases, append timestamp to make unique
132
- if (isDev) {
133
- const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
134
- tagName = `v${version}-${timestamp}`;
554
+ function writeActionsOutputs(values) {
555
+ const lines = Object.entries(values).map(([key, value]) => `${key}=${String(value)}\n`).join("");
556
+ const outputPath = process.env.GITHUB_OUTPUT;
557
+ if (outputPath) {
558
+ fs.appendFileSync(outputPath, lines);
135
559
  }
136
- const tagType = isDev ? "pre-release (dev)" : "release";
137
- // Check if tag already exists
138
- try {
139
- (0, child_process_1.execSync)(`git rev-parse ${tagName}`, { stdio: "ignore" });
140
- console.error(`❌ Tag ${tagName} already exists`);
141
- process.exit(1);
142
- }
143
- catch {
144
- // Tag doesn't exist, continue
145
- }
146
- // Create tag
147
- const message = isDev
148
- ? `Development ${tagType} ${tagName}\n\nThis is a pre-release for testing purposes.`
149
- : `Release ${tagName}`;
150
- (0, child_process_1.execSync)(`git tag -a ${tagName} -m "${message}"`, { stdio: "inherit" });
151
- console.log(`✅ Created git tag ${tagName}`);
152
- // Push tag unless --no-push is specified
153
- if (!noPush) {
154
- console.log("");
155
- console.log(`Pushing tag ${tagName} to origin...`);
156
- try {
157
- (0, child_process_1.execSync)(`git push origin ${tagName}`, { stdio: "inherit" });
158
- console.log(`✅ Pushed tag ${tagName} to origin`);
159
- console.log("");
160
- console.log("CI/CD pipeline will now:");
161
- console.log(" - Run all tests");
162
- console.log(" - Build and push Docker image to ECR");
163
- console.log(" - Create GitHub release");
164
- if (!isDev) {
165
- console.log(" - Publish to NPM (production releases only)");
166
- }
167
- console.log(" - Publish to GitHub Packages");
168
- console.log("");
169
- console.log("Monitor progress at: https://github.com/quiltdata/benchling-webhook/actions");
170
- }
171
- catch {
172
- console.error(`❌ Failed to push tag ${tagName}`);
173
- console.error("You can manually push with: git push origin " + tagName);
174
- process.exit(1);
175
- }
560
+ console.log(lines.trim());
561
+ }
562
+ function createManualTag(repository, version, isDev, noPush) {
563
+ const tag = isDev ? devTag(version) : productionTag(version);
564
+ if (!repository.isClean()) {
565
+ throw new Error("You have uncommitted changes");
176
566
  }
177
- else {
178
- console.log("");
179
- console.log("Tag created but not pushed (--no-push specified)");
180
- console.log(`To push later: git push origin ${tagName}`);
181
- }
182
- }
183
- function main() {
184
- const args = process.argv.slice(2);
185
- // No args: display all three versions
186
- if (args.length === 0) {
187
- const jsonVersion = pkg.version;
188
- const tomlVersion = readPyprojectVersion();
189
- const yamlVersion = readAppManifestVersion();
190
- console.log("Version files:");
191
- console.log(` package.json: ${jsonVersion}`);
192
- console.log(` docker/pyproject.toml: ${tomlVersion}`);
193
- console.log(` docker/app-manifest.yaml: ${yamlVersion}`);
194
- if (jsonVersion === tomlVersion && jsonVersion === yamlVersion) {
195
- console.log("\n✅ All versions are in sync");
567
+ if (repository.localTagTarget(tag)) {
568
+ throw new Error(`Tag ${tag} already exists locally`);
569
+ }
570
+ if (repository.remoteTag(tag)) {
571
+ throw new Error(`Tag ${tag} already exists on origin`);
572
+ }
573
+ const targetSha = repository.resolveCommit("HEAD");
574
+ repository.createAnnotatedTag(tag, annotatedTagMessage(tag), targetSha);
575
+ if (noPush) {
576
+ console.log(`✅ Created ${tag}; push later with: git push origin ${tag}`);
577
+ return tag;
578
+ }
579
+ const pushed = repository.pushTag(tag);
580
+ const remote = repository.remoteTag(tag);
581
+ if (!remote) {
582
+ throw new Error(`Failed to push tag ${tag}; the exact remote tag is absent`);
583
+ }
584
+ assertExpectedTarget(remote.targetSha, normalizeSha(targetSha, "HEAD"), `Remote tag ${tag}`);
585
+ console.log(`✅ ${pushed ? "Pushed" : "Verified concurrent push of"} tag ${tag}`);
586
+ return tag;
587
+ }
588
+ function printHelp(pkg) {
589
+ console.log(`Current version: ${pkg.version}\n`);
590
+ console.log("Usage: npm run version -- [command] [options]\n");
591
+ console.log("Version commands: major, minor, patch, sync");
592
+ console.log("Release commands: tag [dev] [--no-push], auto-tag, release-context");
593
+ console.log("Verification command: verify-clean-git");
594
+ }
595
+ function main(args = process.argv.slice(2)) {
596
+ const pkg = readPackage();
597
+ const repository = new GitReleaseRepository(projectRoot);
598
+ const command = args[0];
599
+ try {
600
+ if (!command) {
601
+ const tomlVersion = readPyprojectVersion();
602
+ const yamlVersion = readAppManifestVersion();
603
+ console.log("Version files:");
604
+ console.log(` package.json: ${pkg.version}`);
605
+ console.log(` docker/pyproject.toml: ${tomlVersion}`);
606
+ console.log(` docker/app-manifest.yaml: ${yamlVersion}`);
607
+ console.log(pkg.version === tomlVersion && pkg.version === yamlVersion
608
+ ? "\n✅ All versions are in sync"
609
+ : "\n⚠️ Versions are out of sync! Run npm run version -- sync to fix.");
610
+ return 0;
196
611
  }
197
- else {
198
- console.log("\n⚠️ Versions are out of sync! Run \"node bin/version.ts sync\" to fix.");
612
+ if (args.includes("--help") || args.includes("-h")) {
613
+ printHelp(pkg);
614
+ return 0;
199
615
  }
200
- process.exit(0);
201
- }
202
- // Help
203
- if (args.includes("--help") || args.includes("-h")) {
204
- console.log("Current version:", pkg.version);
205
- console.log("");
206
- console.log("Usage: node bin/version.ts [command] [options]");
207
- console.log("");
208
- console.log("Version Management Commands:");
209
- console.log(" (no args) - Display all three version files");
210
- console.log(" major - Bump major version (1.0.0 -> 2.0.0)");
211
- console.log(" minor - Bump minor version (0.4.7 -> 0.5.0)");
212
- console.log(" patch - Bump patch version (0.4.7 -> 0.4.8)");
213
- console.log(" sync - Force TOML and YAML to match JSON version");
214
- console.log("");
215
- console.log("Release Tagging Commands:");
216
- console.log(" tag - Create and push production release tag");
217
- console.log(" tag dev - Create and push dev release tag with timestamp");
218
- console.log(" verify-clean-git - Check for uncommitted changes (pre-flight check)");
219
- console.log("");
220
- console.log("Options:");
221
- console.log(" --no-push - Create tag but do not push to origin (tag command only)");
222
- console.log("");
223
- console.log("This script manages version numbers in:");
224
- console.log(" - package.json");
225
- console.log(" - docker/pyproject.toml");
226
- console.log(" - docker/app-manifest.yaml");
227
- console.log(" - docker/uv.lock (via uv sync)");
228
- console.log("");
229
- console.log("Examples:");
230
- console.log(" npm run version # Show versions");
231
- console.log(" npm run version patch # Bump patch version");
232
- console.log(" npm run version:tag # Create production release tag");
233
- console.log(" npm run version:tag:dev # Create dev release tag");
234
- process.exit(0);
235
- }
236
- const bumpType = args[0];
237
- // Verify clean git command - check for uncommitted changes
238
- if (bumpType === "verify-clean-git") {
239
- try {
240
- (0, child_process_1.execSync)("git diff-index --quiet HEAD --", { stdio: "ignore" });
616
+ if (command === "verify-clean-git") {
617
+ if (!repository.isClean()) {
618
+ throw new Error("You have uncommitted changes");
619
+ }
241
620
  console.log("✅ Git working directory is clean");
242
- process.exit(0);
243
- }
244
- catch {
245
- console.error("❌ You have uncommitted changes");
246
- console.error(" Commit or stash your changes before creating a release");
247
- process.exit(1);
621
+ return 0;
248
622
  }
249
- }
250
- // Sync command - force TOML and YAML to match JSON
251
- if (bumpType === "sync") {
252
- const jsonVersion = pkg.version;
253
- const tomlVersion = readPyprojectVersion();
254
- const yamlVersion = readAppManifestVersion();
255
- console.log("Syncing versions to match package.json:");
256
- console.log(` package.json: ${jsonVersion}`);
257
- console.log(` docker/pyproject.toml: ${tomlVersion} -> ${jsonVersion}`);
258
- console.log(` docker/app-manifest.yaml: ${yamlVersion} -> ${jsonVersion}`);
259
- console.log("");
260
- try {
261
- updatePyprojectVersion(jsonVersion);
262
- updateAppManifestVersion(jsonVersion);
263
- // Check if version files have changes
264
- let hasChanges = false;
623
+ if (command === "auto-tag") {
265
624
  try {
266
- (0, child_process_1.execSync)("git diff --quiet docker/pyproject.toml docker/app-manifest.yaml docker/uv.lock", { stdio: "ignore" });
267
- }
268
- catch {
269
- hasChanges = true;
625
+ const result = runAutomaticRelease({
626
+ repository,
627
+ currentVersion: pkg.version,
628
+ beforeSha: process.env.GITHUB_EVENT_BEFORE,
629
+ githubSha: process.env.GITHUB_SHA ?? "",
630
+ runProductionGate: () => runProductionGate(repository),
631
+ });
632
+ writeActionsOutputs({
633
+ outcome: result.outcome,
634
+ release_required: result.releaseRequired,
635
+ verified: result.verified,
636
+ tag: result.tag,
637
+ target_sha: result.targetSha,
638
+ });
639
+ return 0;
270
640
  }
271
- if (hasChanges) {
272
- (0, child_process_1.execSync)("git add docker/pyproject.toml docker/app-manifest.yaml docker/uv.lock", { stdio: "inherit" });
273
- (0, child_process_1.execSync)(`git commit -m "chore: sync versions to ${jsonVersion}"`, { stdio: "inherit" });
274
- console.log("✅ Committed version sync");
275
- }
276
- else {
277
- console.log("✅ All versions already in sync (no changes to commit)");
641
+ catch (error) {
642
+ writeActionsOutputs({
643
+ outcome: "failure",
644
+ release_required: false,
645
+ verified: false,
646
+ tag: "",
647
+ target_sha: "",
648
+ });
649
+ throw error;
278
650
  }
279
651
  }
280
- catch (error) {
281
- const message = error instanceof Error ? error.message : String(error);
282
- console.error("❌ Error:", message);
283
- process.exit(1);
652
+ if (command === "release-context") {
653
+ const tag = args[1] ?? process.env.RELEASE_TAG ?? "";
654
+ const context = validateReleaseContext(repository, tag, pkg.version, process.env.GITHUB_SHA ?? "");
655
+ writeActionsOutputs({
656
+ verified: context.verified,
657
+ tag: context.tag,
658
+ version: context.version,
659
+ base_version: context.baseVersion,
660
+ target_sha: context.targetSha,
661
+ is_prerelease: context.isPrerelease,
662
+ });
663
+ return 0;
284
664
  }
285
- process.exit(0);
286
- }
287
- // Tag command - create and push git tags
288
- if (bumpType === "tag") {
289
- // Check for uncommitted changes
290
- try {
291
- (0, child_process_1.execSync)("git diff-index --quiet HEAD --", { stdio: "ignore" });
665
+ if (command === "sync") {
666
+ updatePyprojectVersion(pkg.version);
667
+ updateAppManifestVersion(pkg.version);
668
+ const diff = (0, exports.defaultCommandRunner)("git", ["diff", "--quiet", "docker/pyproject.toml", "docker/app-manifest.yaml", "docker/uv.lock"], { cwd: projectRoot });
669
+ if (diff.status !== 0) {
670
+ runChecked(exports.defaultCommandRunner, "git", ["add", "docker/pyproject.toml", "docker/app-manifest.yaml", "docker/uv.lock"], projectRoot, true);
671
+ runChecked(exports.defaultCommandRunner, "git", ["commit", "-m", `chore: sync versions to ${pkg.version}`], projectRoot, true);
672
+ }
673
+ return 0;
292
674
  }
293
- catch {
294
- console.error("❌ You have uncommitted changes");
295
- console.error(" Commit or stash your changes before creating a release");
296
- process.exit(1);
675
+ if (command === "tag") {
676
+ createManualTag(repository, pkg.version, args.includes("dev"), args.includes("--no-push"));
677
+ return 0;
297
678
  }
298
- const isDev = args.includes("dev");
299
- const noPush = args.includes("--no-push");
300
- const version = pkg.version;
301
- console.log(`Creating ${isDev ? "dev" : "production"} release from version: ${version}`);
302
- console.log("");
303
- createGitTag(version, isDev, noPush);
304
- process.exit(0);
305
- }
306
- // Check for uncommitted changes (for version bump commands)
307
- try {
308
- (0, child_process_1.execSync)("git diff-index --quiet HEAD --", { stdio: "ignore" });
309
- }
310
- catch {
311
- console.error("❌ You have uncommitted changes");
312
- console.error(" Commit or stash your changes before bumping version");
313
- process.exit(1);
314
- }
315
- try {
316
- const currentVersion = pkg.version;
317
- const newVersion = bumpVersion(currentVersion, bumpType);
318
- console.log(`Bumping version: ${currentVersion} -> ${newVersion}`);
319
- console.log("");
320
- // Update all version files
321
- updatePackageVersion(newVersion);
679
+ if (!repository.isClean()) {
680
+ throw new Error("You have uncommitted changes");
681
+ }
682
+ const newVersion = bumpVersion(pkg.version, command);
683
+ console.log(`Bumping version: ${pkg.version} -> ${newVersion}`);
684
+ updatePackageVersion(pkg, newVersion);
322
685
  updatePyprojectVersion(newVersion);
323
686
  updateAppManifestVersion(newVersion);
324
- // Commit the changes
325
- (0, child_process_1.execSync)("git add package.json docker/pyproject.toml docker/app-manifest.yaml docker/uv.lock", { stdio: "inherit" });
326
- (0, child_process_1.execSync)(`git commit -m "chore: bump version to ${newVersion}"`, { stdio: "inherit" });
327
- console.log("✅ Committed version change");
328
- console.log("");
329
- console.log("Next steps:");
330
- console.log(" 1. Push changes: git push");
331
- console.log(" 2. Create release tag: npm run version:tag (or npm run version:tag:dev for dev release)");
687
+ runChecked(exports.defaultCommandRunner, "git", ["add", "package.json", "docker/pyproject.toml", "docker/app-manifest.yaml", "docker/uv.lock"], projectRoot, true);
688
+ runChecked(exports.defaultCommandRunner, "git", ["commit", "-m", `chore: bump version to ${newVersion}`], projectRoot, true);
689
+ return 0;
332
690
  }
333
691
  catch (error) {
334
- const message = error instanceof Error ? error.message : String(error);
335
- console.error("❌ Error:", message);
336
- process.exit(1);
692
+ console.error(`❌ ${error instanceof Error ? error.message : String(error)}`);
693
+ return 1;
337
694
  }
338
695
  }
339
- main();
696
+ if (require.main === module) {
697
+ process.exitCode = main();
698
+ }
340
699
  //# sourceMappingURL=version.js.map