fix(ci): scope release detection to target branch (#5065)
This commit is contained in:
parent
3bc8336562
commit
3846c90b67
|
|
@ -7,13 +7,21 @@
|
||||||
|
|
||||||
import { execSync } from "node:child_process"
|
import { execSync } from "node:child_process"
|
||||||
import { appendFileSync } from "node:fs"
|
import { appendFileSync } from "node:fs"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const RELEASE_PATTERNS = {
|
const RELEASE_PATTERNS = {
|
||||||
desktop: /release\(desktop\): Release (v\d+\.\d+\.\d+(-[0-9A-Z-.]+)?)/i,
|
desktop: /^release\(desktop\): Release (v\d+\.\d+\.\d+(?:-[0-9A-Z-.]+)?)(?: \(#\d+\))?$/i,
|
||||||
mobile: /release\(mobile\): Release (v\d+\.\d+\.\d+(-[0-9A-Z-.]+)?)/i,
|
mobile: /^release\(mobile\): Release (v\d+\.\d+\.\d+(?:-[0-9A-Z-.]+)?)(?: \(#\d+\))?$/i,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RELEASE_PLATFORM_BY_REF = {
|
||||||
|
main: "desktop",
|
||||||
|
"mobile-main": "mobile",
|
||||||
|
}
|
||||||
|
|
||||||
|
const GITHUB_MERGE_SUBJECT_PATTERN = /^Merge pull request #\d+ from /i
|
||||||
|
|
||||||
const EXIT_CODES = {
|
const EXIT_CODES = {
|
||||||
SUCCESS: 0,
|
SUCCESS: 0,
|
||||||
GIT_ERROR: 2,
|
GIT_ERROR: 2,
|
||||||
|
|
@ -57,7 +65,6 @@ function setGitHubOutput(key, value) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the latest commit message
|
* Get the latest commit message
|
||||||
* @returns {string} Latest commit message
|
|
||||||
*/
|
*/
|
||||||
function getLatestCommitMessage() {
|
function getLatestCommitMessage() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -69,21 +76,45 @@ function getLatestCommitMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract release information from commit message
|
* Extract release information from a commit message.
|
||||||
|
* Prefer the subject. For a standard GitHub merge commit, fall back only to the first non-empty body
|
||||||
|
* line, where GitHub stores the PR title. When a GitHub ref is available, only the platform released
|
||||||
|
* from that branch is considered. Other body lines are ignored so stale release commits cannot
|
||||||
|
* retrigger a release.
|
||||||
* @param {string} commitMessage - Git commit message
|
* @param {string} commitMessage - Git commit message
|
||||||
* @returns {Object|null} Release information or null if no release found
|
* @param {string|undefined} refName - GitHub ref name
|
||||||
|
* @returns {{platform: string, version: string, tagName: string}|null} Release information or null
|
||||||
*/
|
*/
|
||||||
function extractReleaseInfo(commitMessage) {
|
export function extractReleaseInfo(commitMessage, refName = process.env.GITHUB_REF_NAME) {
|
||||||
for (const [platform, regex] of Object.entries(RELEASE_PATTERNS)) {
|
const [commitSubject = "", ...commitBodyLines] = commitMessage.split(/\r?\n/)
|
||||||
const match = commitMessage.match(regex)
|
const expectedPlatform = refName ? RELEASE_PLATFORM_BY_REF[refName] : undefined
|
||||||
if (match) {
|
|
||||||
const version = match[1]
|
|
||||||
const tagName = `${platform}/${version}`
|
|
||||||
|
|
||||||
return {
|
if (refName && !expectedPlatform) {
|
||||||
platform,
|
return null
|
||||||
version,
|
}
|
||||||
tagName,
|
|
||||||
|
const platforms = expectedPlatform ? [expectedPlatform] : Object.keys(RELEASE_PATTERNS)
|
||||||
|
const candidates = [commitSubject.trim()]
|
||||||
|
|
||||||
|
if (GITHUB_MERGE_SUBJECT_PATTERN.test(commitSubject)) {
|
||||||
|
const pullRequestTitle = commitBodyLines.map((line) => line.trim()).find(Boolean)
|
||||||
|
if (pullRequestTitle) {
|
||||||
|
candidates.push(pullRequestTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
for (const platform of platforms) {
|
||||||
|
const match = candidate.match(RELEASE_PATTERNS[platform])
|
||||||
|
if (match) {
|
||||||
|
const version = match[1]
|
||||||
|
const tagName = `${platform}/${version}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform,
|
||||||
|
version,
|
||||||
|
tagName,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,4 +159,6 @@ function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { execFile } from "node:child_process"
|
||||||
|
import { mkdtemp, readFile, rm } from "node:fs/promises"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { promisify } from "node:util"
|
||||||
|
|
||||||
|
import { join } from "pathe"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { extractReleaseInfo } from "./extract-release-info.mjs"
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
|
describe("extractReleaseInfo", () => {
|
||||||
|
it("recognizes a mobile release from the subject and ignores old desktop markers in the body", () => {
|
||||||
|
const commitMessage = [
|
||||||
|
"release(mobile): Release v0.5.7 (#5061)",
|
||||||
|
"",
|
||||||
|
"* release(desktop): release v1.11.0",
|
||||||
|
"* docs(mobile): restore desktop release inputs",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
expect(extractReleaseInfo(commitMessage, "mobile-main")).toEqual({
|
||||||
|
platform: "mobile",
|
||||||
|
version: "v0.5.7",
|
||||||
|
tagName: "mobile/v0.5.7",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects a release subject on the wrong target branch", () => {
|
||||||
|
expect(extractReleaseInfo("release(mobile): Release v0.5.7", "main")).toBeNull()
|
||||||
|
expect(extractReleaseInfo("release(desktop): Release v1.12.0", "mobile-main")).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("recognizes the target platform release from a standard merge commit body", () => {
|
||||||
|
const commitMessage = [
|
||||||
|
"Merge pull request #5061 from RSSNext/release/mobile/0.5.7",
|
||||||
|
"",
|
||||||
|
"release(mobile): Release v0.5.7",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
expect(extractReleaseInfo(commitMessage, "mobile-main")).toEqual({
|
||||||
|
platform: "mobile",
|
||||||
|
version: "v0.5.7",
|
||||||
|
tagName: "mobile/v0.5.7",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ignores ordinary commits", () => {
|
||||||
|
expect(extractReleaseInfo("fix(mobile): restore release metadata", "mobile-main")).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ignores release markers for the wrong platform in a merge commit body", () => {
|
||||||
|
const commitMessage = [
|
||||||
|
"Merge pull request #5061 from RSSNext/release/mobile/0.5.7",
|
||||||
|
"",
|
||||||
|
"release(mobile): Release v0.5.7",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
expect(extractReleaseInfo(commitMessage, "main")).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ignores stale release markers later in an ordinary commit body", () => {
|
||||||
|
const commitMessage = [
|
||||||
|
"chore(sync): merge mobile-main into dev",
|
||||||
|
"",
|
||||||
|
"* release(mobile): Release v0.5.6",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
expect(extractReleaseInfo(commitMessage, "mobile-main")).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not use a stale body marker when another platform release is the subject", () => {
|
||||||
|
const commitMessage = [
|
||||||
|
"release(desktop): Release v1.6.0",
|
||||||
|
"",
|
||||||
|
"* release(mobile): Release v0.4.1",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
expect(extractReleaseInfo(commitMessage, "mobile-main")).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("recognizes a desktop release on main", () => {
|
||||||
|
expect(extractReleaseInfo("release(desktop): Release v1.12.0", "main")).toEqual({
|
||||||
|
platform: "desktop",
|
||||||
|
version: "v1.12.0",
|
||||||
|
tagName: "desktop/v1.12.0",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("falls back to subject-based platform detection without a GitHub ref", () => {
|
||||||
|
const originalRefName = process.env.GITHUB_REF_NAME
|
||||||
|
delete process.env.GITHUB_REF_NAME
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(extractReleaseInfo("release(mobile): Release v0.5.7")).toEqual({
|
||||||
|
platform: "mobile",
|
||||||
|
version: "v0.5.7",
|
||||||
|
tagName: "mobile/v0.5.7",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
if (originalRefName === undefined) {
|
||||||
|
delete process.env.GITHUB_REF_NAME
|
||||||
|
} else {
|
||||||
|
process.env.GITHUB_REF_NAME = originalRefName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("writes the existing GitHub environment and output values from the latest subject", async () => {
|
||||||
|
const repositoryDir = await mkdtemp(join(tmpdir(), "extract-release-info-"))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const githubEnvPath = join(repositoryDir, "github-env.txt")
|
||||||
|
const githubOutputPath = join(repositoryDir, "github-output.txt")
|
||||||
|
const scriptPath = fileURLToPath(new URL("./extract-release-info.mjs", import.meta.url))
|
||||||
|
|
||||||
|
await execFileAsync("git", ["init"], { cwd: repositoryDir })
|
||||||
|
await execFileAsync("git", ["config", "user.name", "Release Test"], {
|
||||||
|
cwd: repositoryDir,
|
||||||
|
})
|
||||||
|
await execFileAsync("git", ["config", "user.email", "release-test@example.com"], {
|
||||||
|
cwd: repositoryDir,
|
||||||
|
})
|
||||||
|
await execFileAsync(
|
||||||
|
"git",
|
||||||
|
[
|
||||||
|
"commit",
|
||||||
|
"--allow-empty",
|
||||||
|
"-m",
|
||||||
|
"release(mobile): Release v0.5.7 (#5061)",
|
||||||
|
"-m",
|
||||||
|
"* release(desktop): release v1.11.0",
|
||||||
|
],
|
||||||
|
{ cwd: repositoryDir },
|
||||||
|
)
|
||||||
|
|
||||||
|
await execFileAsync("node", [scriptPath], {
|
||||||
|
cwd: repositoryDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
GITHUB_ENV: githubEnvPath,
|
||||||
|
GITHUB_OUTPUT: githubOutputPath,
|
||||||
|
GITHUB_REF_NAME: "mobile-main",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const githubEnv = await readFile(githubEnvPath, "utf8")
|
||||||
|
const githubOutput = await readFile(githubOutputPath, "utf8")
|
||||||
|
|
||||||
|
expect(githubEnv).toContain("tag_version=mobile/v0.5.7")
|
||||||
|
expect(githubEnv).toContain("platform=mobile")
|
||||||
|
expect(githubEnv).toContain("version=v0.5.7")
|
||||||
|
expect(githubOutput).toContain("tag_version=mobile/v0.5.7")
|
||||||
|
expect(githubOutput).toContain("platform=mobile")
|
||||||
|
expect(githubOutput).toContain("version=v0.5.7")
|
||||||
|
} finally {
|
||||||
|
await rm(repositoryDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -52,5 +52,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
export NODE_OPTIONS="--max_old_space_size=16384"
|
export NODE_OPTIONS="--max_old_space_size=16384"
|
||||||
npm exec turbo run format:check typecheck lint
|
npm exec turbo run format:check typecheck lint
|
||||||
|
- name: Run release workflow tests
|
||||||
|
run: >-
|
||||||
|
pnpm exec vitest run
|
||||||
|
.github/scripts/extract-release-info.test.ts
|
||||||
|
.github/scripts/release-workflow-guards.test.ts
|
||||||
- name: Run test
|
- name: Run test
|
||||||
run: npm exec turbo run test
|
run: npm exec turbo run test
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ jobs:
|
||||||
- name: Extract release information
|
- name: Extract release information
|
||||||
id: extract_info
|
id: extract_info
|
||||||
run: .github/scripts/extract-release-info.mjs
|
run: .github/scripts/extract-release-info.mjs
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Expose release outputs
|
- name: Expose release outputs
|
||||||
id: release_info
|
id: release_info
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue