feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,293 @@
|
||||
#!/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 === "major") {
|
||||
parts.major += 1
|
||||
parts.minor = 0
|
||||
parts.patch = 0
|
||||
return formatSemver(parts)
|
||||
}
|
||||
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 !== null && /^[^:]+!:\s/.test(subject.trim()))
|
||||
|
||||
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) {
|
||||
bump = "major"
|
||||
} else if (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 === "major")
|
||||
? "major"
|
||||
: 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()
|
||||
Reference in new issue
Block a user