Skip to content

Commit f317c2b

Browse files
konardclaude
andcommitted
feat: add badges to GitHub releases for both JS and Rust via CI/CD
- Add scripts/format-rust-release.mjs: appends crates.io version and docs.rs badges to Rust GitHub release notes after each publish - Wire format-rust-release.mjs into both auto-release and manual-release jobs in .github/workflows/rust.yml - Fix scripts/format-release-notes.mjs to accept named CLI flags (--release-id, --release-version, --repository, --commit-sha) so the npm badge injection called from format-github-release.mjs works correctly - Add changelog fragment rust/changelog.d/badges-in-github-releases.md Closes #25 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d806a41 commit f317c2b

4 files changed

Lines changed: 153 additions & 3 deletions

File tree

.github/workflows/rust.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,13 @@ jobs:
335335
working-directory: .
336336
run: node scripts/create-github-release.mjs --release-version "${{ steps.current_version.outputs.version }}" --repository "${{ github.repository }}"
337337

338+
- name: Format GitHub release notes
339+
if: steps.check.outputs.should_release == 'true'
340+
env:
341+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
342+
working-directory: .
343+
run: node scripts/format-rust-release.mjs --release-version "${{ steps.current_version.outputs.version }}" --repository "${{ github.repository }}"
344+
338345
# === MANUAL INSTANT RELEASE ===
339346
# Manual release via workflow_dispatch - only after CI passes
340347
manual-release:
@@ -403,6 +410,13 @@ jobs:
403410
working-directory: .
404411
run: node scripts/create-github-release.mjs --release-version "${{ steps.version.outputs.new_version }}" --repository "${{ github.repository }}"
405412

413+
- name: Format GitHub release notes
414+
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
415+
env:
416+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
417+
working-directory: .
418+
run: node scripts/format-rust-release.mjs --release-version "${{ steps.version.outputs.new_version }}" --repository "${{ github.repository }}"
419+
406420
# === MANUAL CHANGELOG PR ===
407421
changelog-pr:
408422
name: Create Changelog PR
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
bump: patch
3+
---
4+
5+
### Added
6+
- `scripts/format-rust-release.mjs` — new CI/CD script that appends crates.io version and docs.rs badges to Rust GitHub release notes after each release
7+
- Rust CI/CD workflow now calls `format-rust-release.mjs` in both `auto-release` and `manual-release` jobs so every published Rust release automatically includes badge links
8+
- Fixed `scripts/format-release-notes.mjs` to accept named CLI flags (`--release-id`, `--release-version`, `--repository`) in addition to legacy positional arguments, enabling proper npm badge injection for JS releases

scripts/format-release-notes.mjs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
11
#!/usr/bin/env node
22

33
/**
4-
* Script to format GitHub release notes with proper formatting:
4+
* Script to format JavaScript GitHub release notes with proper formatting:
55
* - Fix special characters like \n
66
* - Add link to PR with changeset (if available)
77
* - Add shields.io NPM version badge
88
* - Format nicely with proper markdown
9+
*
10+
* Usage (named flags – preferred):
11+
* node scripts/format-release-notes.mjs \
12+
* --release-id <id> \
13+
* --release-version <version> \
14+
* --repository <owner/repo> \
15+
* [--commit-sha <sha>]
16+
*
17+
* Legacy positional usage (deprecated):
18+
* node scripts/format-release-notes.mjs <releaseId> <version> <repository>
919
*/
1020

1121
import { execSync } from 'child_process';
1222

13-
const [, , releaseId, version, repository] = process.argv;
23+
// ---------------------------------------------------------------------------
24+
// Argument parsing – support both named flags and legacy positional args
25+
// ---------------------------------------------------------------------------
26+
const rawArgs = process.argv.slice(2);
27+
const getNamedArg = (name) => {
28+
const idx = rawArgs.indexOf(`--${name}`);
29+
return idx >= 0 && rawArgs[idx + 1] ? rawArgs[idx + 1] : null;
30+
};
31+
32+
// Named flags take priority; fall back to positional args for compatibility
33+
const releaseId =
34+
getNamedArg('release-id') ??
35+
(rawArgs[0] && !rawArgs[0].startsWith('--') ? rawArgs[0] : null);
36+
const version =
37+
getNamedArg('release-version') ??
38+
(rawArgs[1] && !rawArgs[1].startsWith('--') ? rawArgs[1] : null);
39+
const repository =
40+
getNamedArg('repository') ??
41+
(rawArgs[2] && !rawArgs[2].startsWith('--') ? rawArgs[2] : null);
1442

1543
if (!releaseId || !version || !repository) {
1644
console.error(
17-
'Usage: format-release-notes.mjs <releaseId> <version> <repository>'
45+
'Usage: format-release-notes.mjs --release-id <id> --release-version <version> --repository <owner/repo>'
1846
);
1947
process.exit(1);
2048
}

scripts/format-rust-release.mjs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Format Rust GitHub release notes with shields.io badges.
5+
*
6+
* Adds to the release body:
7+
* - crates.io version badge
8+
* - docs.rs badge
9+
*
10+
* Usage:
11+
* node scripts/format-rust-release.mjs \
12+
* --release-version <version> \
13+
* --repository <owner/repo> \
14+
* [--crate-name <name>]
15+
*
16+
* Environment variables:
17+
* VERSION – fallback for --release-version
18+
* REPOSITORY – fallback for --repository
19+
* CRATE_NAME – fallback for --crate-name
20+
*/
21+
22+
import { execSync } from 'child_process';
23+
24+
// ---------------------------------------------------------------------------
25+
// Argument parsing (named flags)
26+
// ---------------------------------------------------------------------------
27+
const args = process.argv.slice(2);
28+
const getArg = (name, defaultValue = '') => {
29+
const idx = args.indexOf(`--${name}`);
30+
return idx >= 0 && args[idx + 1] ? args[idx + 1] : defaultValue;
31+
};
32+
33+
const version = getArg('release-version', process.env.VERSION || '');
34+
const repository = getArg('repository', process.env.REPOSITORY || '');
35+
// Default crate name derived from repo name (last segment)
36+
const defaultCrateName = repository.split('/').pop() || '';
37+
const crateName = getArg('crate-name', process.env.CRATE_NAME || defaultCrateName);
38+
39+
if (!version || !repository) {
40+
console.error('Error: Missing required arguments');
41+
console.error(
42+
'Usage: node scripts/format-rust-release.mjs --release-version <version> --repository <owner/repo>'
43+
);
44+
process.exit(1);
45+
}
46+
47+
const versionWithoutV = version.replace(/^v/, '');
48+
const tag = `v${versionWithoutV}`;
49+
50+
// ---------------------------------------------------------------------------
51+
// Shields.io badges for Rust
52+
// ---------------------------------------------------------------------------
53+
const cratesIoBadge = `[![crates.io](https://img.shields.io/crates/v/${crateName}.svg)](https://crates.io/crates/${crateName})`;
54+
const docsRsBadge = `[![docs.rs](https://img.shields.io/docsrs/${crateName})](https://docs.rs/${crateName})`;
55+
const badgeLine = `${cratesIoBadge} ${docsRsBadge}`;
56+
const packageLinks =
57+
`📦 **View on crates.io:** https://crates.io/crates/${crateName}/${versionWithoutV}\n` +
58+
`📖 **API docs:** https://docs.rs/${crateName}/${versionWithoutV}`;
59+
60+
// ---------------------------------------------------------------------------
61+
// Fetch and update the release
62+
// ---------------------------------------------------------------------------
63+
try {
64+
// Look up the release by tag
65+
let releaseData;
66+
try {
67+
releaseData = JSON.parse(
68+
execSync(`gh api repos/${repository}/releases/tags/${tag}`, {
69+
encoding: 'utf8',
70+
})
71+
);
72+
} catch {
73+
console.log(`⚠️ Could not find release for ${tag}`);
74+
process.exit(0);
75+
}
76+
77+
const { id: releaseId, body: currentBody = '' } = releaseData;
78+
79+
// Skip if already formatted
80+
if (currentBody.includes('img.shields.io')) {
81+
console.log('ℹ️ Release notes already contain badges – skipping');
82+
process.exit(0);
83+
}
84+
85+
const formattedBody =
86+
`${currentBody.trimEnd()}\n\n---\n\n${badgeLine}\n\n${packageLinks}`;
87+
88+
const updatePayload = JSON.stringify({ body: formattedBody });
89+
execSync(
90+
`gh api repos/${repository}/releases/${releaseId} -X PATCH --input -`,
91+
{ encoding: 'utf8', input: updatePayload }
92+
);
93+
94+
console.log(`✅ Formatted Rust release notes for ${tag}`);
95+
console.log(' - Added crates.io version badge');
96+
console.log(' - Added docs.rs badge');
97+
} catch (error) {
98+
console.error('❌ Error formatting Rust release notes:', error.message);
99+
process.exit(1);
100+
}

0 commit comments

Comments
 (0)