|
| 1 | +#!/usr/bin/env node |
| 2 | +import assert from "node:assert/strict"; |
| 3 | +import { execFileSync } from "node:child_process"; |
| 4 | +import { readFileSync } from "node:fs"; |
| 5 | +import { join } from "node:path"; |
| 6 | +import { fileURLToPath } from "node:url"; |
| 7 | + |
| 8 | +const root = fileURLToPath(new URL("..", import.meta.url)); |
| 9 | +const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); |
| 10 | +const version = packageJson.version; |
| 11 | +const releaseBranchName = process.env.RELEASE_BRANCH_NAME; |
| 12 | +const releaseTargetSha = process.env.RELEASE_TARGET_SHA; |
| 13 | +const semverPattern = |
| 14 | + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z.-]+)?$/; |
| 15 | + |
| 16 | +function git(args, options = {}) { |
| 17 | + assert.ok(Array.isArray(args), "git args must be an array"); |
| 18 | + assert.ok(args.every((arg) => typeof arg === "string"), "git args must be strings"); |
| 19 | + return execFileSync("git", args, { |
| 20 | + cwd: root, |
| 21 | + encoding: "utf8", |
| 22 | + stdio: ["ignore", "pipe", options.allowFailure ? "pipe" : "inherit"], |
| 23 | + }).trim(); |
| 24 | +} |
| 25 | + |
| 26 | +function tryGit(args) { |
| 27 | + try { |
| 28 | + return git(args, { allowFailure: true }); |
| 29 | + } catch { |
| 30 | + return undefined; |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +assert.equal(typeof version, "string", "package.json version must be a string"); |
| 35 | +assert.match(version, semverPattern, `package.json version must be semver: ${version}`); |
| 36 | + |
| 37 | +const tagName = `v${version}`; |
| 38 | + |
| 39 | +if (releaseBranchName !== undefined && releaseBranchName !== "") { |
| 40 | + assert.equal( |
| 41 | + releaseBranchName, |
| 42 | + `release/${tagName}`, |
| 43 | + `release branch ${releaseBranchName} must match package version ${tagName}`, |
| 44 | + ); |
| 45 | +} |
| 46 | + |
| 47 | +const targetSha = releaseTargetSha?.trim() || git(["rev-parse", "HEAD"]); |
| 48 | +assert.match(targetSha, /^[0-9a-f]{40}$/i, `release target SHA must be a full commit SHA: ${targetSha}`); |
| 49 | + |
| 50 | +const currentHead = git(["rev-parse", "HEAD"]); |
| 51 | +assert.equal(currentHead, targetSha, "checked-out HEAD must match the release target SHA"); |
| 52 | + |
| 53 | +const remoteTagSha = tryGit(["ls-remote", "--tags", "origin", `refs/tags/${tagName}`]); |
| 54 | +if (remoteTagSha) { |
| 55 | + const [existingSha, existingRef] = remoteTagSha.split(/\s+/, 2); |
| 56 | + assert.equal(existingRef, `refs/tags/${tagName}`, `unexpected remote tag ref for ${tagName}`); |
| 57 | + assert.equal(existingSha, targetSha, `remote tag ${tagName} already exists at ${existingSha}, not ${targetSha}`); |
| 58 | + console.log(JSON.stringify({ tagName, targetSha, created: false }, null, 2)); |
| 59 | + process.exit(0); |
| 60 | +} |
| 61 | + |
| 62 | +git(["tag", tagName, targetSha]); |
| 63 | +git(["push", "origin", `refs/tags/${tagName}`]); |
| 64 | +console.log(JSON.stringify({ tagName, targetSha, created: true }, null, 2)); |
0 commit comments