This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**/*.md"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build and verify
|
||||
run: bun run test
|
||||
|
||||
- name: Package release
|
||||
run: bun run release:package
|
||||
|
||||
- name: Publish release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: bun scripts/publish-release.ts
|
||||
@@ -2,5 +2,6 @@
|
||||
.github-theme
|
||||
.gitea-data
|
||||
dist
|
||||
release
|
||||
node_modules
|
||||
.playwright-mcp
|
||||
|
||||
@@ -18,8 +18,15 @@ bun run dev
|
||||
bun run preview
|
||||
bun run locale:sync
|
||||
bun run test
|
||||
bun run release:package
|
||||
```
|
||||
|
||||
构建会生成 `dist/theme-github-dev.css` 和 `dist/theme-github-dev-dark.css`,并同步输出到 `.gitea/custom/public/assets/css/` 供 Gitea custom 目录使用。Docker 预览当前默认加载 `github-dev-dark`,便于检查未登录页面 dark 模式。
|
||||
|
||||
locale 只在 `options/locale-overrides/` 维护增量 key。构建会按当前 Gitea 版本从官方仓库获取完整 locale,合并增量后输出到 `.gitea/custom/options/locale/`。
|
||||
|
||||
## 发布
|
||||
|
||||
`.github/workflows/release.yml` 会在非纯文档提交时自动构建并发布当前 `package.json` 版本。发布前会删除同名 `v{version}` release/tag,再用当前提交重新创建,适合手动维护版本号后持续覆盖当前版本。
|
||||
|
||||
默认使用平台提供的 `GITHUB_TOKEN`。如果 Gitea Actions 的默认 token 权限不够,可以配置 `RELEASE_TOKEN` secret。
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"scripts": {
|
||||
"build": "bun scripts/build.ts",
|
||||
"locale:sync": "bun scripts/locale.ts",
|
||||
"release:package": "bun run build:min && bun scripts/package-release.ts",
|
||||
"release:publish": "bun scripts/publish-release.ts",
|
||||
"dev": "bun scripts/build.ts --watch",
|
||||
"preview": "bun run build && bun scripts/preview.ts",
|
||||
"build:min": "bun scripts/build.ts --minify",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { cp, mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { $ } from "bun";
|
||||
|
||||
const root = process.cwd();
|
||||
const packageJson = JSON.parse(await Bun.file(join(root, "package.json")).text()) as { version: string };
|
||||
const version = packageJson.version;
|
||||
const name = `gitea-github-theme-v${version}`;
|
||||
const releaseRoot = join(root, "release");
|
||||
const packageRoot = join(releaseRoot, name);
|
||||
|
||||
await rm(releaseRoot, { recursive: true, force: true });
|
||||
await mkdir(join(packageRoot, "custom", "public", "assets", "css"), { recursive: true });
|
||||
await mkdir(join(packageRoot, "custom", "templates"), { recursive: true });
|
||||
await mkdir(join(packageRoot, "custom", "options"), { recursive: true });
|
||||
|
||||
await cp(join(root, "dist", "theme-github-dev.css"), join(packageRoot, "custom", "public", "assets", "css", "theme-github-dev.css"));
|
||||
await cp(
|
||||
join(root, "dist", "theme-github-dev-dark.css"),
|
||||
join(packageRoot, "custom", "public", "assets", "css", "theme-github-dev-dark.css"),
|
||||
);
|
||||
await cp(join(root, ".gitea", "custom", "templates"), join(packageRoot, "custom", "templates"), { recursive: true });
|
||||
await cp(join(root, ".gitea", "custom", "options"), join(packageRoot, "custom", "options"), { recursive: true });
|
||||
await cp(join(root, "README.md"), join(packageRoot, "README.md"));
|
||||
|
||||
await $`tar -C ${releaseRoot} -czf ${releaseRoot}/${name}.tar.gz ${name}`;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
await $`powershell -NoProfile -Command Compress-Archive -Path ${packageRoot} -DestinationPath ${join(releaseRoot, `${name}.zip`)} -Force`;
|
||||
} else {
|
||||
await $`zip -qr ${join(releaseRoot, `${name}.zip`)} ${name}`.cwd(releaseRoot);
|
||||
}
|
||||
|
||||
const artifacts = [`${name}.tar.gz`, `${name}.zip`];
|
||||
const checksumLines = [];
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const file = Bun.file(join(releaseRoot, artifact));
|
||||
const hash = new Bun.CryptoHasher("sha256");
|
||||
hash.update(await file.arrayBuffer());
|
||||
checksumLines.push(`${hash.digest("hex")} ${artifact}`);
|
||||
}
|
||||
|
||||
await Bun.write(join(releaseRoot, "checksums.txt"), `${checksumLines.join("\n")}\n`);
|
||||
|
||||
console.log(`packaged release ${name}`);
|
||||
@@ -0,0 +1,129 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
|
||||
interface Release {
|
||||
id: number;
|
||||
tag_name: string;
|
||||
upload_url?: string;
|
||||
}
|
||||
|
||||
const root = process.cwd();
|
||||
const packageJson = JSON.parse(await Bun.file(join(root, "package.json")).text()) as { version: string };
|
||||
const version = packageJson.version;
|
||||
const tag = `v${version}`;
|
||||
const repository = process.env.GITHUB_REPOSITORY;
|
||||
const token = process.env.RELEASE_TOKEN || process.env.GITHUB_TOKEN;
|
||||
const apiUrl = (process.env.GITHUB_API_URL || `${process.env.GITHUB_SERVER_URL ?? ""}/api/v1`).replace(/\/$/, "");
|
||||
const releaseDir = join(root, "release");
|
||||
const releaseName = `gitea-github-theme-v${version}`;
|
||||
|
||||
if (!repository) {
|
||||
throw new Error("GITHUB_REPOSITORY is required");
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Error("GITHUB_TOKEN or RELEASE_TOKEN is required");
|
||||
}
|
||||
|
||||
const [owner, repo] = repository.split("/");
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
function resolveApiUrl(pathOrUrl: string) {
|
||||
if (/^https?:\/\//.test(pathOrUrl)) {
|
||||
return pathOrUrl;
|
||||
}
|
||||
|
||||
return `${apiUrl}${pathOrUrl}`;
|
||||
}
|
||||
|
||||
async function request(pathOrUrl: string, init: RequestInit = {}) {
|
||||
const url = resolveApiUrl(pathOrUrl);
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
...headers,
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${init.method ?? "GET"} ${url} failed: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function releaseAssetUploadUrl(release: Release, artifact: string) {
|
||||
if (!release.upload_url) {
|
||||
return `/repos/${owner}/${repo}/releases/${release.id}/assets?name=${encodeURIComponent(artifact)}`;
|
||||
}
|
||||
|
||||
const baseUrl = release.upload_url.replace(/\{.*\}$/, "");
|
||||
const separator = baseUrl.includes("?") ? "&" : "?";
|
||||
|
||||
return `${baseUrl}${separator}name=${encodeURIComponent(artifact)}`;
|
||||
}
|
||||
|
||||
async function ignoreMissing(path: string, init: RequestInit = {}) {
|
||||
try {
|
||||
await request(path, init);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("404")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentRelease = (await request(`/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`)) as Release | null;
|
||||
|
||||
if (currentRelease) {
|
||||
await ignoreMissing(`/repos/${owner}/${repo}/releases/${currentRelease.id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
await ignoreMissing(`/repos/${owner}/${repo}/tags/${encodeURIComponent(tag)}`, { method: "DELETE" });
|
||||
await ignoreMissing(`/repos/${owner}/${repo}/git/refs/tags/${encodeURIComponent(tag)}`, { method: "DELETE" });
|
||||
|
||||
const release = (await request(`/repos/${owner}/${repo}/releases`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
tag_name: tag,
|
||||
target_commitish: process.env.GITHUB_SHA,
|
||||
name: releaseName,
|
||||
body: [
|
||||
`Gitea GitHub theme build for ${tag}.`,
|
||||
"",
|
||||
"Install by extracting the archive and copying `custom/` into your Gitea custom path.",
|
||||
].join("\n"),
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
}),
|
||||
})) as Release;
|
||||
|
||||
const artifacts = (await readdir(releaseDir)).filter((file) => file.endsWith(".zip") || file.endsWith(".tar.gz") || file === "checksums.txt");
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const path = join(releaseDir, artifact);
|
||||
const file = Bun.file(path);
|
||||
await request(releaseAssetUploadUrl(release, basename(path)), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
body: file,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`published release ${tag} with ${artifacts.length} assets`);
|
||||
Reference in New Issue
Block a user