Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m45s
Docker images / frontend-image (push) Successful in 1m37s
Docker images / updater-image (push) Successful in 37s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 6s
- Replaced the public directory creation step with a direct copy in the Dockerfile for improved build efficiency. - Enhanced the compute-release script to retrieve all tags and manage release manifests more effectively, including better handling of published timestamps and commit details. - Updated the ReleasesPage component to conditionally format the published date based on the build type, improving user clarity on release status.
266 lines
6.8 KiB
JavaScript
266 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from "node:child_process"
|
|
import { mkdirSync, writeFileSync } from "node:fs"
|
|
import { dirname, join } from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const repoRoot = join(__dirname, "..", "..")
|
|
const baseVersion = process.env.BASE_VERSION ?? "1.0.0"
|
|
const outputDir = process.env.RELEASE_OUTPUT_DIR
|
|
? join(repoRoot, process.env.RELEASE_OUTPUT_DIR)
|
|
: join(repoRoot, ".ci", "release")
|
|
const repository = process.env.GITEA_REPOSITORY ?? process.env.GITHUB_REPOSITORY ?? ""
|
|
const serverUrl = (process.env.GITEA_SERVER_URL ?? "https://git.shts.su").replace(/\/$/, "")
|
|
|
|
const CONVENTIONAL_RE =
|
|
/^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*(.+)$/i
|
|
const MINOR_TYPES = new Set(["feat"])
|
|
const PATCH_TYPES = new Set([
|
|
"fix",
|
|
"chore",
|
|
"docs",
|
|
"refactor",
|
|
"style",
|
|
"test",
|
|
"build",
|
|
"ci",
|
|
])
|
|
|
|
function git(...args) {
|
|
return execFileSync("git", args, {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
}).trim()
|
|
}
|
|
|
|
function parseSemver(version) {
|
|
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version)
|
|
if (!match) {
|
|
throw new Error(`Invalid semver: ${version}`)
|
|
}
|
|
return {
|
|
major: Number(match[1]),
|
|
minor: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
}
|
|
}
|
|
|
|
function formatSemver(parts) {
|
|
return `${parts.major}.${parts.minor}.${parts.patch}`
|
|
}
|
|
|
|
function bumpVersion(version, bump) {
|
|
const parts = parseSemver(version)
|
|
if (bump === "minor") {
|
|
parts.minor += 1
|
|
parts.patch = 0
|
|
return formatSemver(parts)
|
|
}
|
|
parts.patch += 1
|
|
return formatSemver(parts)
|
|
}
|
|
|
|
function parseCommit(subject, body = "") {
|
|
const match = CONVENTIONAL_RE.exec(subject.trim())
|
|
const breaking =
|
|
/BREAKING CHANGE/i.test(body) ||
|
|
(match?.[1] && subject.includes(`${match[1]}!:`))
|
|
|
|
if (!match) {
|
|
return { type: "other", scope: null, description: subject.trim(), bump: "patch" }
|
|
}
|
|
|
|
const type = match[1].toLowerCase()
|
|
const scope = match[2]?.slice(1, -1) ?? null
|
|
const description = match[3].trim()
|
|
let bump = "patch"
|
|
|
|
if (breaking || MINOR_TYPES.has(type)) {
|
|
bump = "minor"
|
|
} else if (PATCH_TYPES.has(type)) {
|
|
bump = "patch"
|
|
}
|
|
|
|
return { type, scope, description, bump }
|
|
}
|
|
|
|
function getAllTags() {
|
|
try {
|
|
return git("tag", "--list", "v*.*.*", "--sort=-v:refname")
|
|
.split("\n")
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
function getPreviousTag(tag) {
|
|
const tags = getAllTags()
|
|
const index = tags.indexOf(tag)
|
|
return index >= 0 && index + 1 < tags.length ? tags[index + 1] : null
|
|
}
|
|
|
|
function getTagPublishedAt(tag) {
|
|
try {
|
|
return git("log", "-1", "--format=%aI", tag)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function toManifestCommits(commits) {
|
|
return commits.map((commit) => ({
|
|
sha: commit.sha,
|
|
shortSha: commit.shortSha,
|
|
subject: commit.subject,
|
|
author: commit.author,
|
|
type: commit.type,
|
|
}))
|
|
}
|
|
|
|
function writeManifest(manifest) {
|
|
mkdirSync(outputDir, { recursive: true })
|
|
writeFileSync(join(outputDir, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
|
}
|
|
|
|
function getLatestTag() {
|
|
return getAllTags()[0] ?? null
|
|
}
|
|
|
|
function getCommitsSince(tag) {
|
|
const range = tag ? `${tag}..HEAD` : "HEAD"
|
|
const raw = git(
|
|
"log",
|
|
range,
|
|
"--pretty=format:%H%x1f%h%x1f%an%x1f%s%x1f%b%x1e",
|
|
)
|
|
if (!raw) return []
|
|
|
|
return raw
|
|
.split("\x1e")
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
.map((entry) => {
|
|
const [sha, shortSha, author, subject, body] = entry.split("\x1f")
|
|
const parsed = parseCommit(subject, body)
|
|
return {
|
|
sha,
|
|
shortSha,
|
|
author,
|
|
subject,
|
|
type: parsed.type,
|
|
scope: parsed.scope,
|
|
bump: parsed.bump,
|
|
}
|
|
})
|
|
}
|
|
|
|
function groupCommits(commits) {
|
|
const groups = new Map()
|
|
for (const commit of commits) {
|
|
const key = commit.type
|
|
if (!groups.has(key)) groups.set(key, [])
|
|
groups.get(key).push(commit)
|
|
}
|
|
return groups
|
|
}
|
|
|
|
function renderReleaseNotes({ tag, version, commits }) {
|
|
const lines = [`# ${tag}`, "", `Версия **${version}**.`, ""]
|
|
const groups = groupCommits(commits)
|
|
const order = ["feat", "fix", "chore", "docs", "refactor", "style", "test", "build", "ci", "other"]
|
|
|
|
for (const type of order) {
|
|
const items = groups.get(type)
|
|
if (!items?.length) continue
|
|
lines.push(`## ${type}`, "")
|
|
for (const item of items) {
|
|
lines.push(`- ${item.subject} (\`${item.shortSha}\`, ${item.author})`)
|
|
}
|
|
lines.push("")
|
|
}
|
|
|
|
return `${lines.join("\n").trim()}\n`
|
|
}
|
|
|
|
function buildReleaseUrl(tag) {
|
|
if (!repository) return ""
|
|
const [owner, repo] = repository.split("/")
|
|
if (!owner || !repo) return ""
|
|
return `${serverUrl}/${owner}/${repo}/releases/tag/${encodeURIComponent(tag)}`
|
|
}
|
|
|
|
function writeGithubOutput(values) {
|
|
const outputFile = process.env.GITHUB_OUTPUT
|
|
if (!outputFile) return
|
|
const lines = Object.entries(values)
|
|
.map(([key, value]) => `${key}=${String(value).replace(/\r?\n/g, "%0A")}`)
|
|
.join("\n")
|
|
writeFileSync(outputFile, `${lines}\n`, { encoding: "utf8", flag: "a" })
|
|
}
|
|
|
|
function main() {
|
|
const latestTag = getLatestTag()
|
|
const baseFromTag = latestTag?.replace(/^v/, "") ?? baseVersion
|
|
const commits = getCommitsSince(latestTag)
|
|
|
|
if (commits.length === 0) {
|
|
const deployTag = latestTag ?? `v${baseFromTag}`
|
|
const deployVersion = baseFromTag
|
|
const displayCommits = latestTag ? getCommitsSince(getPreviousTag(latestTag)) : []
|
|
const publishedAt = latestTag ? getTagPublishedAt(latestTag) : null
|
|
|
|
mkdirSync(outputDir, { recursive: true })
|
|
writeFileSync(join(outputDir, "release_notes.md"), "", "utf8")
|
|
writeManifest({
|
|
version: deployVersion,
|
|
tag: deployTag,
|
|
publishedAt,
|
|
releaseUrl: latestTag ? buildReleaseUrl(latestTag) : "",
|
|
commits: toManifestCommits(displayCommits),
|
|
})
|
|
|
|
writeGithubOutput({
|
|
should_release: "false",
|
|
version: deployVersion,
|
|
tag: deployTag,
|
|
bump: "none",
|
|
release_url: latestTag ? buildReleaseUrl(latestTag) : "",
|
|
})
|
|
return
|
|
}
|
|
|
|
const bump = commits.some((commit) => commit.bump === "minor") ? "minor" : "patch"
|
|
const version = bumpVersion(baseFromTag, bump)
|
|
const tag = `v${version}`
|
|
const publishedAt = new Date().toISOString()
|
|
const releaseUrl = buildReleaseUrl(tag)
|
|
const releaseNotes = renderReleaseNotes({ tag, version, commits })
|
|
const manifest = {
|
|
version,
|
|
tag,
|
|
publishedAt,
|
|
releaseUrl,
|
|
commits: toManifestCommits(commits),
|
|
}
|
|
|
|
mkdirSync(outputDir, { recursive: true })
|
|
writeFileSync(join(outputDir, "release_notes.md"), releaseNotes, "utf8")
|
|
writeManifest(manifest)
|
|
|
|
writeGithubOutput({
|
|
should_release: "true",
|
|
version,
|
|
tag,
|
|
bump,
|
|
release_url: releaseUrl,
|
|
})
|
|
|
|
process.stdout.write(`${tag}\n`)
|
|
}
|
|
|
|
main()
|