Skip to content

Commit 6847558

Browse files
committed
docs: automate README download links after release
1 parent 69ee890 commit 6847558

5 files changed

Lines changed: 254 additions & 11 deletions

File tree

.github/workflows/release.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,30 @@ jobs:
263263
throw "No release assets found in $env:RELEASE_OUTPUT_DIR"
264264
}
265265
& gh release upload $env:RELEASE_TAG @assets --clobber
266+
267+
- name: Update README download links
268+
if: github.event_name == 'release' || github.event_name == 'push'
269+
shell: powershell
270+
run: |
271+
git fetch origin main
272+
git checkout -B main origin/main
273+
274+
node .\scripts\update-readme-downloads.mjs `
275+
--release-output-dir $env:RELEASE_OUTPUT_DIR `
276+
--repo $env:GITHUB_REPOSITORY `
277+
--tag $env:RELEASE_TAG `
278+
--version $env:CODEX_ZH_VERSION
279+
280+
$readmeStatus = git status --porcelain README.md
281+
if (!$readmeStatus) {
282+
Write-Host "README download links already match $env:RELEASE_TAG."
283+
$global:LASTEXITCODE = 0
284+
exit 0
285+
}
286+
287+
git config user.name "github-actions[bot]"
288+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
289+
git add README.md
290+
git commit -m "docs: update README download links for $env:RELEASE_TAG"
291+
git push origin HEAD:main
292+
$global:LASTEXITCODE = 0

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,24 @@ Codex-ZH 是面向中文 Windows 用户的 Codex 定制版。它默认使用中
1818

1919
## 下载
2020

21-
请到 [Releases](https://github.com/focuxdot/codex-zh/releases) 下载最新版。
21+
<!-- codex-zh-downloads:start -->
22+
当前最新版:v0.1.1
2223

23-
在 Release 页面里,下载文件名包含 `Codex-ZH`、并且以 `.exe` 结尾的安装包,例如:
24-
25-
```text
26-
OpenAI.Codex-<official-version>+Codex-ZH-<version>-win-x64.exe
27-
```
24+
| 你的系统 | 下载哪个版本 |
25+
| --- | --- |
26+
| Windows 10 / Windows 11(64 位) | [下载 Codex-ZH 0.1.1 Windows x64 安装包](https://github.com/focuxdot/codex-zh/releases/download/v0.1.1/OpenAI.Codex-26.608.1337.0%2BCodex-ZH-0.1.1-win-x64.exe) |
27+
| macOS | 暂不提供 Codex-ZH 安装包,不要下载 Windows 版 |
28+
| Linux | 暂不提供 Codex-ZH 安装包,不要下载 Windows 版 |
2829

29-
如果你看到 `Source code (zip)` `Source code (tar.gz)`,那是源码包,不是给普通用户安装的软件
30+
普通用户只需要下载上面的 `.exe` 文件。不要下载 GitHub 页面里的 `Source code`,那是源码,不是安装包
3031

31-
同一个 Release 里可能还会有 `.sha256` 文件,它是安装包校验文件。普通用户可以不下载。
32+
校验文件:[`OpenAI.Codex-26.608.1337.0+Codex-ZH-0.1.1-win-x64.exe.sha256`](https://github.com/focuxdot/codex-zh/releases/download/v0.1.1/OpenAI.Codex-26.608.1337.0%2BCodex-ZH-0.1.1-win-x64.exe.sha256)
33+
SHA256:`54aadeb761320de0267a5636552ca1df90488b449f5c9a96781c92a8d6114651`
34+
<!-- codex-zh-downloads:end -->
3235

3336
## 快速开始
3437

35-
1. [Releases](https://github.com/focuxdot/codex-zh/releases) 下载最新版 `.exe` 安装包。
38+
1. 在上面的“下载”表格里,按你的系统下载 `.exe` 安装包。
3639
2. 双击安装并打开 `Codex-ZH`
3740
3. 在配置向导里选择模板,填写中转站地址、API Key、模型名。
3841
4. 点击“测试连接”。
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { pathToFileURL } from "node:url";
4+
5+
export const DOWNLOADS_START = "<!-- codex-zh-downloads:start -->";
6+
export const DOWNLOADS_END = "<!-- codex-zh-downloads:end -->";
7+
8+
function parseArgs(argv) {
9+
const args = {};
10+
for (let index = 0; index < argv.length; index += 1) {
11+
const name = argv[index];
12+
if (!name.startsWith("--")) {
13+
throw new Error(`Unexpected argument: ${name}`);
14+
}
15+
const key = name.slice(2);
16+
const value = argv[index + 1];
17+
if (!value || value.startsWith("--")) {
18+
throw new Error(`Missing value for --${key}`);
19+
}
20+
args[key] = value;
21+
index += 1;
22+
}
23+
return args;
24+
}
25+
26+
function requireValue(value, name) {
27+
if (!value) {
28+
throw new Error(`Missing required ${name}.`);
29+
}
30+
return value;
31+
}
32+
33+
function normalizeVersion(versionOrTag) {
34+
return requireValue(versionOrTag, "version").replace(/^v/u, "");
35+
}
36+
37+
export function buildAssetDownloadUrl({ repo, tag, assetName }) {
38+
requireValue(repo, "repo");
39+
requireValue(tag, "tag");
40+
requireValue(assetName, "assetName");
41+
return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(assetName)}`;
42+
}
43+
44+
export function findReleaseAssets(outputDir) {
45+
const files = readdirSync(outputDir, { withFileTypes: true })
46+
.filter((entry) => entry.isFile())
47+
.map((entry) => entry.name)
48+
.sort();
49+
const installerName = files.find((name) => name.endsWith(".exe"));
50+
if (!installerName) {
51+
throw new Error(`Could not find a Windows installer exe in ${outputDir}.`);
52+
}
53+
54+
const expectedShaName = `${installerName}.sha256`;
55+
const sha256Name = files.includes(expectedShaName)
56+
? expectedShaName
57+
: files.find((name) => name.endsWith(".sha256"));
58+
if (!sha256Name) {
59+
throw new Error(`Could not find a sha256 file in ${outputDir}.`);
60+
}
61+
62+
const sha256Text = readFileSync(join(outputDir, sha256Name), "utf8");
63+
const sha256 = sha256Text.match(/\b[a-f0-9]{64}\b/iu)?.[0].toLowerCase();
64+
if (!sha256) {
65+
throw new Error(`Could not find a SHA-256 hash in ${join(outputDir, sha256Name)}.`);
66+
}
67+
68+
return { installerName, sha256Name, sha256 };
69+
}
70+
71+
export function generateDownloadBlock({ repo, tag, version, installerName, sha256Name, sha256 }) {
72+
const cleanVersion = normalizeVersion(version);
73+
const installerUrl = buildAssetDownloadUrl({ repo, tag, assetName: installerName });
74+
const sha256Url = buildAssetDownloadUrl({ repo, tag, assetName: sha256Name });
75+
76+
return `${DOWNLOADS_START}
77+
当前最新版:v${cleanVersion}
78+
79+
| 你的系统 | 下载哪个版本 |
80+
| --- | --- |
81+
| Windows 10 / Windows 11(64 位) | [下载 Codex-ZH ${cleanVersion} Windows x64 安装包](${installerUrl}) |
82+
| macOS | 暂不提供 Codex-ZH 安装包,不要下载 Windows 版 |
83+
| Linux | 暂不提供 Codex-ZH 安装包,不要下载 Windows 版 |
84+
85+
普通用户只需要下载上面的 \`.exe\` 文件。不要下载 GitHub 页面里的 \`Source code\`,那是源码,不是安装包。
86+
87+
校验文件:[\`${sha256Name}\`](${sha256Url})
88+
SHA256:\`${sha256}\`
89+
${DOWNLOADS_END}`;
90+
}
91+
92+
export function updateDownloadsSection(readme, block) {
93+
const markedSection = new RegExp(`${DOWNLOADS_START}[\\s\\S]*?${DOWNLOADS_END}`, "u");
94+
if (markedSection.test(readme)) {
95+
return readme.replace(markedSection, block);
96+
}
97+
98+
const headingMatch = readme.match(/^## \s*$/mu);
99+
if (!headingMatch || headingMatch.index === undefined) {
100+
throw new Error("Could not find README section: ## 下载");
101+
}
102+
103+
const sectionStart = headingMatch.index + headingMatch[0].length;
104+
const nextHeadingMatch = readme.slice(sectionStart).match(/\n## /u);
105+
if (!nextHeadingMatch || nextHeadingMatch.index === undefined) {
106+
throw new Error("Could not find the section after README downloads.");
107+
}
108+
109+
const sectionEnd = sectionStart + nextHeadingMatch.index;
110+
return `${readme.slice(0, sectionStart)}\n\n${block}\n${readme.slice(sectionEnd)}`;
111+
}
112+
113+
export function updateReadmeDownloads({
114+
readmePath = "README.md",
115+
outputDir,
116+
repo,
117+
tag,
118+
version,
119+
}) {
120+
const assets = findReleaseAssets(requireValue(outputDir, "outputDir"));
121+
const block = generateDownloadBlock({
122+
repo: requireValue(repo, "repo"),
123+
tag: requireValue(tag, "tag"),
124+
version: requireValue(version, "version"),
125+
...assets,
126+
});
127+
const original = readFileSync(readmePath, "utf8");
128+
const updated = updateDownloadsSection(original, block);
129+
if (updated !== original) {
130+
writeFileSync(readmePath, updated, "utf8");
131+
return true;
132+
}
133+
return false;
134+
}
135+
136+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
137+
const args = parseArgs(process.argv.slice(2));
138+
const changed = updateReadmeDownloads({
139+
readmePath: args.readme ?? "README.md",
140+
outputDir: args["release-output-dir"] ?? process.env.RELEASE_OUTPUT_DIR,
141+
repo: args.repo ?? process.env.GITHUB_REPOSITORY,
142+
tag: args.tag ?? process.env.RELEASE_TAG ?? process.env.GITHUB_REF_NAME,
143+
version: args.version ?? process.env.CODEX_ZH_VERSION ?? process.env.RELEASE_TAG ?? process.env.GITHUB_REF_NAME,
144+
});
145+
console.log(changed ? "README downloads updated." : "README downloads already up to date.");
146+
}

test/readme-downloads.test.mjs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test } from "node:test";
2+
import assert from "node:assert/strict";
3+
4+
import {
5+
DOWNLOADS_END,
6+
DOWNLOADS_START,
7+
buildAssetDownloadUrl,
8+
generateDownloadBlock,
9+
updateDownloadsSection,
10+
} from "../scripts/update-readme-downloads.mjs";
11+
12+
test("download asset URLs encode installer names for GitHub release links", () => {
13+
assert.equal(
14+
buildAssetDownloadUrl({
15+
repo: "focuxdot/codex-zh",
16+
tag: "v0.1.1",
17+
assetName: "OpenAI.Codex-26.608.1337.0+Codex-ZH-0.1.1-win-x64.exe",
18+
}),
19+
"https://github.com/focuxdot/codex-zh/releases/download/v0.1.1/OpenAI.Codex-26.608.1337.0%2BCodex-ZH-0.1.1-win-x64.exe",
20+
);
21+
});
22+
23+
test("generateDownloadBlock gives non-technical users a direct Windows installer choice", () => {
24+
const block = generateDownloadBlock({
25+
repo: "focuxdot/codex-zh",
26+
tag: "v0.1.1",
27+
version: "0.1.1",
28+
installerName: "OpenAI.Codex-26.608.1337.0+Codex-ZH-0.1.1-win-x64.exe",
29+
sha256Name: "OpenAI.Codex-26.608.1337.0+Codex-ZH-0.1.1-win-x64.exe.sha256",
30+
sha256: "54aadeb761320de0267a5636552ca1df90488b449f5c9a96781c92a8d6114651",
31+
});
32+
33+
assert.match(block, /Windows 10 \/ Windows 1164 /u);
34+
assert.match(block, / Codex-ZH 0\.1\.1 Windows x64 /u);
35+
assert.match(block, / GitHub `Source code`/u);
36+
assert.match(block, /54aadeb761320de0267a5636552ca1df90488b449f5c9a96781c92a8d6114651/u);
37+
});
38+
39+
test("updateDownloadsSection replaces only the marked README downloads block", () => {
40+
const readme = `# Codex-ZH 中文版
41+
42+
## 下载
43+
44+
${DOWNLOADS_START}
45+
旧下载内容
46+
${DOWNLOADS_END}
47+
48+
## 快速开始
49+
50+
1. 打开安装包。
51+
`;
52+
const block = `${DOWNLOADS_START}
53+
新下载内容
54+
${DOWNLOADS_END}`;
55+
56+
const updated = updateDownloadsSection(readme, block);
57+
58+
assert.match(updated, //u);
59+
assert.doesNotMatch(updated, //u);
60+
assert.match(updated, /## /u);
61+
assert.match(updated, /1\. /u);
62+
});

test/release-workflow.test.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,16 @@ test("release workflow packages Windows installer after GitHub Release publish",
3737
assert.match(workflow, /Failed to inspect release \$env:RELEASE_TAG/u);
3838
assert.match(workflow, /gh release create \$env:RELEASE_TAG/u);
3939
assert.match(workflow, /gh release upload \$env:RELEASE_TAG @assets --clobber/u);
40+
assert.match(workflow, /node \.\\scripts\\update-readme-downloads\.mjs/u);
41+
assert.match(workflow, /git checkout -B main origin\/main/u);
42+
assert.match(workflow, /docs: update README download links for \$env:RELEASE_TAG/u);
43+
assert.match(workflow, /git push origin HEAD:main/u);
4044
});
4145

4246
test("release workflow keeps source app binary out of the repository", () => {
4347
assert.match(workflow, /Invoke-WebRequest -Uri \$sourceUrl -OutFile \$env:SOURCE_ARCHIVE/u);
4448
assert.match(workflow, /Expand-Archive -LiteralPath \$env:SOURCE_ARCHIVE/u);
45-
assert.doesNotMatch(workflow, /git add/u);
46-
assert.doesNotMatch(workflow, /git commit/u);
49+
assert.doesNotMatch(workflow, /git add .*SOURCE_ARCHIVE/u);
50+
assert.doesNotMatch(workflow, /git add .*RELEASE_OUTPUT_DIR/u);
51+
assert.match(workflow, /git add README\.md/u);
4752
});

0 commit comments

Comments
 (0)