Skip to content

Commit 7de07f1

Browse files
ConardLicursoragent
andcommitted
fix(readme): drive download links from latest git tag, not manifest
The README "Download v… .zip" link must point at a version that users can actually download — i.e. the most recently *published* git tag for that skill — not whatever is currently sitting in manifest.json. Before: bumping manifest to 1.0.3 (without yet running release) made update-readme.mjs rewrite the README to a v1.0.3 URL that returns 404, and broke `npm run validate` (and CI) because readme:check disagreed with the manifest. After: update-readme.mjs reads the latest `<skill>-v*` tag via lastTagFor() and uses that as the version source. manifest.json is now free to live ahead of the published version (the natural "next release in flight" state) without breaking CI. When release-skill.yml later creates the new tag and re-runs readme:sync, the README naturally catches up. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 136624a commit 7de07f1

1 file changed

Lines changed: 51 additions & 11 deletions

File tree

scripts/release/update-readme.mjs

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
#!/usr/bin/env node
22
// Rewrites the <!-- DOWNLOAD:<skill>:start --> ... <!-- DOWNLOAD:<skill>:end -->
3-
// blocks in README.md and README.zh-CN.md so they always reflect the current
4-
// version declared in skills/<name>/manifest.json.
3+
// blocks in README.md and README.zh-CN.md so they always advertise the
4+
// most recently published version of each skill.
5+
//
6+
// IMPORTANT: the version source is the latest *git tag* for that skill
7+
// (`<skill>-v<semver>`), NOT skills/<name>/manifest.json. The manifest is
8+
// the "version under development"; the README must point at what users
9+
// can actually download right now. Otherwise bumping a manifest before
10+
// cutting a release would produce a 404 download link in the README.
511
//
612
// Idempotent: running it twice in a row produces no diff.
713
//
@@ -13,7 +19,14 @@
1319
import { readFile, writeFile } from "node:fs/promises";
1420
import path from "node:path";
1521
import process from "node:process";
16-
import { REPO_ROOT, loadAllManifests, buildTag, zipName } from "./lib/skills.mjs";
22+
import {
23+
REPO_ROOT,
24+
loadAllManifests,
25+
buildTag,
26+
zipName,
27+
lastTagFor,
28+
parseTag,
29+
} from "./lib/skills.mjs";
1730

1831
const DEFAULT_REPO = "ConardLi/garden-skills";
1932

@@ -23,26 +36,37 @@ const FILES = [
2336
];
2437

2538
const COPY = {
26-
en: { label: "Download v%V .zip" },
27-
zh: { label: "下载 v%V .zip" },
39+
en: { label: "Download v%V .zip", unreleased: "_(no release yet — coming soon)_" },
40+
zh: { label: "下载 v%V .zip", unreleased: "_(暂未发布)_" },
2841
};
2942

3043
// Per-skill block is intentionally a single inline link — appended to the
3144
// existing "Links:" / "链接:" row of each skill section. All other install
3245
// flavours (npx, marketplace, manual copy, submodule) live in the unified
3346
// Install section below.
3447
//
35-
// The URL points at the pinned zip for the current manifest version. It's
36-
// auto-rewritten on every release, so the README always advertises the most
37-
// recent immutable artifact for that skill — no rolling-tag plumbing needed.
48+
// The URL points at the pinned zip for the latest published version (read
49+
// from the latest git tag for this skill). Skills that have never been
50+
// released get a placeholder so the marker still round-trips cleanly.
3851
function buildBlock(skill, version, repo, lang) {
52+
if (!version) return COPY[lang].unreleased;
3953
const tag = buildTag(skill, version);
4054
const zip = zipName(skill, version);
4155
const url = `https://github.com/${repo}/releases/download/${tag}/${zip}`;
4256
const label = COPY[lang].label.replace("%V", version);
4357
return `[${label}](${url})`;
4458
}
4559

60+
// Resolves the version a README should advertise for a given skill.
61+
// Returns the semver string from the latest `<skill>-v<semver>` tag, or
62+
// null if the skill has never been tagged.
63+
function publishedVersionFor(skillName) {
64+
const tag = lastTagFor(skillName);
65+
if (!tag) return null;
66+
const parsed = parseTag(tag);
67+
return parsed?.version ?? null;
68+
}
69+
4670
function rewrite(content, blocks) {
4771
let out = content;
4872
for (const [skill, body] of Object.entries(blocks)) {
@@ -89,15 +113,31 @@ async function main() {
89113
}
90114

91115
const manifests = await loadAllManifests();
116+
// Resolve the published version for each skill ONCE here, so we don't
117+
// shell out to git per-file.
118+
const published = manifests.map((m) => ({
119+
name: m.name,
120+
manifestVersion: m.manifest.version,
121+
publishedVersion: publishedVersionFor(m.name),
122+
}));
92123
console.log(`[readme] repo=${args.repo} skills=${manifests.length}`);
124+
for (const p of published) {
125+
const note =
126+
p.publishedVersion === null
127+
? "no tag yet"
128+
: p.publishedVersion === p.manifestVersion
129+
? `v${p.publishedVersion}`
130+
: `v${p.publishedVersion} (manifest=${p.manifestVersion}, awaiting release)`;
131+
console.log(`[readme] ${p.name}: ${note}`);
132+
}
93133

94134
let drift = false;
95135
for (const file of FILES) {
96136
const original = await readFile(file.path, "utf8");
97137
const blocks = Object.fromEntries(
98-
manifests.map((m) => [
99-
m.name,
100-
buildBlock(m.name, m.manifest.version, args.repo, file.lang),
138+
published.map((p) => [
139+
p.name,
140+
buildBlock(p.name, p.publishedVersion, args.repo, file.lang),
101141
]),
102142
);
103143
const updated = rewrite(original, blocks);

0 commit comments

Comments
 (0)