Files
DenozordecandCursor 7ed269cf2c
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 4m25s
Docker images / frontend-image (push) Successful in 2m10s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 45s
Docker images / publish-release (push) Successful in 6s
ci: переключить registry на git.shx.one
Обновить Gitea Actions, compose, updater и ссылки на образы.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 16:43:40 +07:00

284 lines
7.2 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.shx.one").replace(/\/$/, "")
const MANIFEST_COMMIT_LIMIT = Number(process.env.RELEASE_MANIFEST_COMMIT_LIMIT ?? 6)
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 parseCommitLog(raw)
}
function getRecentCommits(limit = MANIFEST_COMMIT_LIMIT) {
const raw = git(
"log",
"-n",
String(limit),
"HEAD",
"--pretty=format:%H%x1f%h%x1f%an%x1f%s%x1f%b%x1e",
)
if (!raw) return []
return parseCommitLog(raw)
}
function parseCommitLog(raw) {
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 = getRecentCommits()
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(getRecentCommits()),
}
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()