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 { appendFileSync } from "node:fs"
|
||||
import { pathToFileURL } from "node:url"
|
||||
|
||||
// Configuration
|
||||
const RELEASE_PATTERNS = {
|
||||
desktop: /release\(desktop\): Release (v\d+\.\d+\.\d+(-[0-9A-Z-.]+)?)/i,
|
||||
mobile: /release\(mobile\): 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-.]+)?)(?: \(#\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 = {
|
||||
SUCCESS: 0,
|
||||
GIT_ERROR: 2,
|
||||
|
|
@ -57,7 +65,6 @@ function setGitHubOutput(key, value) {
|
|||
|
||||
/**
|
||||
* Get the latest commit message
|
||||
* @returns {string} Latest commit message
|
||||
*/
|
||||
function getLatestCommitMessage() {
|
||||
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
|
||||
* @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) {
|
||||
for (const [platform, regex] of Object.entries(RELEASE_PATTERNS)) {
|
||||
const match = commitMessage.match(regex)
|
||||
if (match) {
|
||||
const version = match[1]
|
||||
const tagName = `${platform}/${version}`
|
||||
export function extractReleaseInfo(commitMessage, refName = process.env.GITHUB_REF_NAME) {
|
||||
const [commitSubject = "", ...commitBodyLines] = commitMessage.split(/\r?\n/)
|
||||
const expectedPlatform = refName ? RELEASE_PLATFORM_BY_REF[refName] : undefined
|
||||
|
||||
return {
|
||||
platform,
|
||||
version,
|
||||
tagName,
|
||||
if (refName && !expectedPlatform) {
|
||||
return null
|
||||
}
|
||||
|
||||
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: |
|
||||
export NODE_OPTIONS="--max_old_space_size=16384"
|
||||
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
|
||||
run: npm exec turbo run test
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ jobs:
|
|||
- name: Extract release information
|
||||
id: extract_info
|
||||
run: .github/scripts/extract-release-info.mjs
|
||||
continue-on-error: true
|
||||
|
||||
- name: Expose release outputs
|
||||
id: release_info
|
||||
|
|
|
|||
Loading…
Reference in New Issue