1const MINIMUM_RELEASE_GAP_MS = 6 * 60 * 60 * 1000;
2
3function repositoryDefaultBranch(context) {
4 const branch = context.payload.repository?.default_branch;
5 if (!branch) {
6 throw new Error("GitHub did not provide the repository default branch.");
7 }
8 return branch;
9}
10
11async function assertCommitOnDefaultBranch({ github, context, sha }) {
12 const defaultBranch = repositoryDefaultBranch(context);
13 const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
14 ...context.repo,
15 basehead: `${sha}...${defaultBranch}`,
16 per_page: 1,
17 });
18 if (comparison.status !== "ahead" && comparison.status !== "identical") {
19 throw new Error(
20 `Release commit ${sha} is not contained in ${defaultBranch} (${comparison.status}).`,
21 );
22 }
23}
24
25async function assertReleaseSource({ github, context, releaseChannel }) {
26 if (releaseChannel === "preview") return;
27 if (releaseChannel !== "stable" && releaseChannel !== "nightly") {
28 throw new Error(`Unsupported release channel: ${releaseChannel}`);
29 }
30
31 const defaultBranch = repositoryDefaultBranch(context);
32 if (context.eventName === "workflow_dispatch" && context.ref !== `refs/heads/${defaultBranch}`) {
33 throw new Error(
34 `${releaseChannel} releases must be dispatched from ${defaultBranch}; selected ${context.ref}. Use the preview channel for branch builds.`,
35 );
36 }
37
38 await assertCommitOnDefaultBranch({ github, context, sha: context.sha });
39}
40
41const isNightlyTag = (tag) => /^v.*-nightly\./.test(tag) || tag.startsWith("nightly-v");
42
43// Newest published nightly by publication time, or undefined when none exists.
44async function findLatestNightly({ github, context }) {
45 const releases = await github.paginate(github.rest.repos.listReleases, {
46 ...context.repo,
47 per_page: 100,
48 });
49 return releases
50 .filter((release) => !release.draft && release.published_at && isNightlyTag(release.tag_name))
51 .sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
52}
53
54// Runs after the workflow acquires the nightly concurrency lock.
55async function shouldReleaseNightly({ github, context, core, now = Date.now() }) {
56 const lastNightly = await findLatestNightly({ github, context });
57
58 if (!lastNightly) {
59 core.info("No published nightly found. Proceeding with release.");
60 return true;
61 }
62
63 if (now - Date.parse(lastNightly.published_at) < MINIMUM_RELEASE_GAP_MS) {
64 core.info(`Nightly ${lastNightly.tag_name} was published less than six hours ago. Skipping.`);
65 return false;
66 }
67
68 const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
69 ...context.repo,
70 basehead: `${lastNightly.tag_name}...${context.sha}`,
71 per_page: 1,
72 });
73 if (comparison.status !== "ahead") {
74 core.info(
75 `Candidate commit is ${comparison.status} relative to ${lastNightly.tag_name}. Skipping.`,
76 );
77 return false;
78 }
79
80 core.info(`New commits since ${lastNightly.tag_name}, and the six-hour gap has passed.`);
81 return true;
82}
83
84// Stable releases build the commit the latest nightly shipped, so the stable
85// build is one nightly users already ran. Returns the nightly tag, its commit,
86// and the stable version that nightly was a preview of.
87async function resolveLatestNightlyCommit({ github, context, core }) {
88 const lastNightly = await findLatestNightly({ github, context });
89 if (!lastNightly) {
90 throw new Error("No published nightly found. Stable releases build the latest nightly commit.");
91 }
92
93 const tag = lastNightly.tag_name;
94 // repos.getCommit dereferences annotated tags, so this is the commit either way.
95 const { data: commit } = await github.rest.repos.getCommit({ ...context.repo, ref: tag });
96 const version = /^(?:nightly-)?v(\d+\.\d+\.\d+)-nightly\./.exec(tag)?.[1];
97 if (!version) {
98 throw new Error(`Cannot derive a stable version from nightly tag ${tag}.`);
99 }
100
101 core.info(`Latest nightly ${tag} shipped ${commit.sha} as a preview of ${version}.`);
102 return { tag, sha: commit.sha, version };
103}
104
105module.exports = {
106 assertCommitOnDefaultBranch,
107 assertReleaseSource,
108 shouldReleaseNightly,
109 resolveLatestNightlyCommit,
110};