Skip to content

Commit b8767a9

Browse files
authored
Merge pull request #188 from morri-son/enhance-controller-release-v2
move inline script for tag creation to external script for better mai…
2 parents 43165c5 + 5a24a3a commit b8767a9

File tree

3 files changed

+433
-44
lines changed

3 files changed

+433
-44
lines changed

.github/scripts/create-tag.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// @ts-check
2+
import { execFileSync } from "child_process";
3+
import fs from "fs";
4+
5+
// --------------------------
6+
// Shared helpers
7+
// --------------------------
8+
9+
/**
10+
* Check whether a git tag exists locally.
11+
*
12+
* @param {string} tag - The tag name to check.
13+
* @param {function} [execGit] - Git executor (for testing).
14+
* @returns {boolean}
15+
*/
16+
export function tagExists(tag, execGit = defaultExecGit) {
17+
try {
18+
execGit(["rev-parse", `refs/tags/${tag}`]);
19+
return true;
20+
} catch {
21+
return false;
22+
}
23+
}
24+
25+
/**
26+
* Resolve the commit SHA a tag points to (peeling through annotated tags).
27+
*
28+
* @param {string} tag - The tag to resolve.
29+
* @param {function} [execGit] - Git executor (for testing).
30+
* @returns {string} The commit SHA.
31+
* @throws {Error} If the tag cannot be resolved.
32+
*/
33+
export function resolveTagCommit(tag, execGit = defaultExecGit) {
34+
const sha = execGit(["rev-parse", `refs/tags/${tag}^{commit}`]);
35+
if (!sha) {
36+
throw new Error(`Could not resolve commit for tag ${tag}`);
37+
}
38+
return sha;
39+
}
40+
41+
/**
42+
* Create an annotated tag and push it to origin.
43+
*
44+
* @param {object} opts
45+
* @param {string} opts.tag - Tag name to create.
46+
* @param {string} opts.commit - Commit SHA to tag (use "HEAD" for current).
47+
* @param {string} opts.message - Tag annotation message.
48+
* @param {function} [opts.execGit] - Git executor (for testing).
49+
*/
50+
export function createAndPushTag({ tag, commit, message, execGit = defaultExecGit }) {
51+
if (commit === "HEAD") {
52+
execGit(["tag", "-a", tag, "-m", message]);
53+
} else {
54+
execGit(["tag", "-a", tag, commit, "-m", message]);
55+
}
56+
execGit(["push", "origin", `refs/tags/${tag}`]);
57+
}
58+
59+
/**
60+
* Default git executor using child_process.execFileSync.
61+
*
62+
* @param {string[]} args - Git arguments.
63+
* @returns {string} Trimmed stdout.
64+
*/
65+
function defaultExecGit(args) {
66+
return execFileSync("git", args, { encoding: "utf-8", stdio: "pipe" }).trim();
67+
}
68+
69+
// --------------------------
70+
// RC tag entrypoint
71+
// --------------------------
72+
73+
/**
74+
* Create an RC tag with the changelog as annotation.
75+
* Idempotent: skips if the tag already exists.
76+
* Sets output `pushed=true` on success or idempotent skip.
77+
*
78+
* Expects env vars: TAG, CHANGELOG_FILE
79+
*
80+
* @param {object} args
81+
* @param {object} args.core - GitHub Actions core module.
82+
* @param {function} [args.execGit] - Git executor (for testing).
83+
*/
84+
export async function createRcTag({ core, execGit = defaultExecGit }) {
85+
const { TAG: tag, CHANGELOG_FILE: changelogFile } = process.env;
86+
87+
if (!tag || !changelogFile) {
88+
core.setFailed("Missing TAG or CHANGELOG_FILE environment variables");
89+
return;
90+
}
91+
92+
if (tagExists(tag, execGit)) {
93+
core.info(`Tag ${tag} already exists, skipping (idempotent)`);
94+
core.setOutput("pushed", "true");
95+
return;
96+
}
97+
98+
const message = readChangelog(changelogFile);
99+
createAndPushTag({ tag, commit: "HEAD", message, execGit });
100+
core.setOutput("pushed", "true");
101+
core.info(`✅ Created RC tag ${tag}`);
102+
}
103+
104+
/**
105+
* Read changelog file content.
106+
*
107+
* @param {string} filePath
108+
* @returns {string}
109+
*/
110+
function readChangelog(filePath) {
111+
return fs.readFileSync(filePath, "utf8");
112+
}
113+
114+
// --------------------------
115+
// Final tag entrypoint
116+
// --------------------------
117+
118+
/**
119+
* Create a final tag pointing to the same commit as the RC tag.
120+
* Idempotent: succeeds if the final tag already points to the correct commit.
121+
* Fails if the final tag exists but points to a different commit.
122+
*
123+
* Expects env vars: RC_TAG, FINAL_TAG
124+
*
125+
* @param {object} args
126+
* @param {object} args.core - GitHub Actions core module.
127+
* @param {function} [args.execGit] - Git executor (for testing).
128+
*/
129+
export async function createFinalTag({ core, execGit = defaultExecGit }) {
130+
const { RC_TAG: rcTag, FINAL_TAG: finalTag } = process.env;
131+
132+
if (!rcTag || !finalTag) {
133+
core.setFailed("Missing RC_TAG or FINAL_TAG environment variables");
134+
return;
135+
}
136+
137+
let rcSha;
138+
try {
139+
rcSha = resolveTagCommit(rcTag, execGit);
140+
} catch (err) {
141+
core.setFailed(err.message);
142+
return;
143+
}
144+
145+
if (tagExists(finalTag, execGit)) {
146+
let existingSha;
147+
try {
148+
existingSha = resolveTagCommit(finalTag, execGit);
149+
} catch (err) {
150+
core.setFailed(err.message);
151+
return;
152+
}
153+
154+
if (existingSha === rcSha) {
155+
core.info(
156+
`Tag ${finalTag} already exists at expected commit ${rcSha.substring(0, 7)}, continuing (idempotent rerun)`,
157+
);
158+
return;
159+
}
160+
161+
core.setFailed(
162+
`Tag ${finalTag} already exists but points to ${existingSha.substring(0, 7)}, expected ${rcSha.substring(0, 7)}`,
163+
);
164+
return;
165+
}
166+
167+
createAndPushTag({
168+
tag: finalTag,
169+
commit: rcSha,
170+
message: `Promote ${rcTag} to ${finalTag}`,
171+
execGit,
172+
});
173+
core.info(`✅ Created final tag ${finalTag} from ${rcTag} (${rcSha.substring(0, 7)})`);
174+
}

0 commit comments

Comments
 (0)