Skip to content

Commit 4e441ce

Browse files
chore: add npm release automation
1 parent b3691e4 commit 4e441ce

7 files changed

Lines changed: 232 additions & 2 deletions

File tree

.github/workflows/release.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
id-token: write
12+
13+
jobs:
14+
release:
15+
name: Version, changelog, and publish
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Create or finalize release
20+
id: release
21+
uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4
22+
with:
23+
config-file: release-please-config.json
24+
manifest-file: .release-please-manifest.json
25+
26+
- name: Check out repository
27+
if: ${{ steps.release.outputs.release_created == 'true' }}
28+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
with:
30+
fetch-depth: 0
31+
persist-credentials: false
32+
33+
- name: Set up Node.js
34+
if: ${{ steps.release.outputs.release_created == 'true' }}
35+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
36+
with:
37+
node-version: 24
38+
registry-url: https://registry.npmjs.org
39+
40+
- name: Install dependencies
41+
if: ${{ steps.release.outputs.release_created == 'true' }}
42+
run: npm ci
43+
44+
- name: Test package
45+
if: ${{ steps.release.outputs.release_created == 'true' }}
46+
run: npm test
47+
48+
- name: Generate OpenCode release diff summary
49+
if: ${{ steps.release.outputs.release_created == 'true' }}
50+
continue-on-error: true
51+
env:
52+
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
53+
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
54+
RELEASE_TAG_NAME: ${{ steps.release.outputs.tag_name }}
55+
run: node scripts/summarize-release-diff.mjs
56+
57+
- name: Append OpenCode summary to GitHub release
58+
if: ${{ steps.release.outputs.release_created == 'true' }}
59+
env:
60+
GH_TOKEN: ${{ github.token }}
61+
RELEASE_TAG_NAME: ${{ steps.release.outputs.tag_name }}
62+
run: |
63+
if [ ! -s opencode-release-summary.md ]; then
64+
echo "No OpenCode release summary was generated."
65+
exit 0
66+
fi
67+
68+
gh release view "$RELEASE_TAG_NAME" --json body --jq '.body // ""' > release-notes.md
69+
{
70+
cat release-notes.md
71+
printf '\n\n## OpenCode Diff Summary\n\n'
72+
cat opencode-release-summary.md
73+
} > release-notes-with-opencode.md
74+
gh release edit "$RELEASE_TAG_NAME" --notes-file release-notes-with-opencode.md
75+
76+
- name: Publish to npm
77+
if: ${{ steps.release.outputs.release_created == 'true' }}
78+
run: npm publish --access public

.release-please-manifest.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
".": "0.0.0"
3+
}

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ The role of this file is to describe common mistakes and confusion points that a
77
If you encounter something surprising, tell the developer and add the lesson here so future agents do not rediscover the same failure.
88

99
- For number-based MIL-STD-2525D/APP-6D SIDCs rendered by `milsymbol`, digits 9-10 are the echelon/mobility field and digits 11-20 are the function ID. Keep curated land-unit fixtures aligned with `milsymbol`'s number-SIDC metadata and land-unit function table.
10+
- Release automation uses Release Please plus npm trusted publishing in `.github/workflows/release.yml`. Keep release-driving commits in Conventional Commit format so version bumps and `CHANGELOG.md` stay automated.
11+
- The release workflow optionally uses `OPENCODE_API_KEY` through `scripts/summarize-release-diff.mjs` to append an OpenCode-generated diff summary to GitHub release notes. Keep that step non-blocking so npm publishing is not blocked by LLM availability.

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ V0 intentionally supports only a tiny curated set of 30-digit SIDCs. It does not
1313

1414
## Install
1515

16-
This package is private while the V0 API is being validated. Clone the repository and install dependencies locally:
16+
Install from npm after the first release is published:
17+
18+
```sh
19+
npm install sidc-kit
20+
```
21+
22+
For local development, clone the repository and install dependencies:
1723

1824
```sh
1925
npm install
@@ -69,3 +75,24 @@ Renders a curated SIDC with `milsymbol` and returns SVG plus anchor and size met
6975
The current curated set includes a few common land-unit examples such as friendly infantry platoon, hostile infantry platoon, armor platoon, artillery platoon, reconnaissance platoon, and infantry company.
7076

7177
Image-based reverse lookup is intentionally deferred.
78+
79+
## Release Automation
80+
81+
Releases are managed by Release Please. Commits merged to `main` should use Conventional Commit prefixes:
82+
83+
- `fix:` creates a patch release
84+
- `feat:` creates a minor release
85+
- `feat!:` or `fix!:` creates a major release
86+
87+
On `main`, the release workflow opens or updates a release PR that contains the version bump and `CHANGELOG.md`. Merging that release PR creates the GitHub release and publishes the package to npm.
88+
89+
Npm publishing uses trusted publishing with GitHub Actions OIDC. Configure the package on npm with these trusted publisher values:
90+
91+
- owner: `the-Drunken-coder`
92+
- repository: `sidc-kit`
93+
- workflow filename: `release.yml`
94+
- allowed action: `npm publish`
95+
96+
No long-lived `NPM_TOKEN` is required for the release workflow.
97+
98+
If `OPENCODE_API_KEY` is configured as a GitHub Actions secret, the release workflow also asks OpenCode to summarize the diff between the new release tag and the previous release tag, then appends that summary to the GitHub release notes. Set the optional repository variable `OPENCODE_MODEL` to override the default `opencode/kimi-k2` model.

package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"name": "sidc-kit",
33
"version": "0.0.0",
4-
"private": true,
54
"description": "A small TypeScript toolkit for searching, explaining, building, and rendering curated SIDCs.",
65
"type": "module",
76
"main": "./dist/index.js",
@@ -16,6 +15,14 @@
1615
"dist",
1716
"README.md"
1817
],
18+
"repository": {
19+
"type": "git",
20+
"url": "git+https://github.com/the-Drunken-coder/sidc-kit.git"
21+
},
22+
"bugs": {
23+
"url": "https://github.com/the-Drunken-coder/sidc-kit/issues"
24+
},
25+
"homepage": "https://github.com/the-Drunken-coder/sidc-kit#readme",
1926
"scripts": {
2027
"build": "tsc -p tsconfig.json",
2128
"test": "npm run build && node --test"
@@ -28,6 +35,9 @@
2835
"milsymbol"
2936
],
3037
"license": "MIT",
38+
"publishConfig": {
39+
"access": "public"
40+
},
3141
"dependencies": {
3242
"milsymbol": "^3.0.4"
3343
},

release-please-config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"packages": {
3+
".": {
4+
"release-type": "node",
5+
"package-name": "sidc-kit",
6+
"include-component-in-tag": false
7+
}
8+
}
9+
}

scripts/summarize-release-diff.mjs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { execFileSync, spawnSync } from "node:child_process";
2+
import { writeFileSync } from "node:fs";
3+
4+
const tagName = process.env.RELEASE_TAG_NAME;
5+
const openCodeApiKey = process.env.OPENCODE_API_KEY;
6+
const model = process.env.OPENCODE_MODEL || "opencode/kimi-k2";
7+
const contextPath = ".release-diff-context.md";
8+
const outputPath = "opencode-release-summary.md";
9+
const maxPatchBytes = 120_000;
10+
11+
if (!tagName) {
12+
console.log("No release tag was provided; skipping OpenCode release summary.");
13+
process.exit(0);
14+
}
15+
16+
if (!openCodeApiKey) {
17+
console.log("OPENCODE_API_KEY is not configured; skipping OpenCode release summary.");
18+
process.exit(0);
19+
}
20+
21+
function git(args) {
22+
return execFileSync("git", args, { encoding: "utf8" }).trim();
23+
}
24+
25+
function optionalGit(args) {
26+
try {
27+
return git(args);
28+
} catch {
29+
return "";
30+
}
31+
}
32+
33+
const previousTag = optionalGit(["describe", "--tags", "--abbrev=0", `${tagName}^`]);
34+
const baseRef = previousTag || git(["rev-list", "--max-parents=0", tagName]);
35+
const range = `${baseRef}..${tagName}`;
36+
const diffStat = optionalGit(["diff", "--stat", range]);
37+
const nameStatus = optionalGit(["diff", "--name-status", range]);
38+
const commits = optionalGit(["log", "--oneline", "--no-decorate", range]);
39+
const patch = optionalGit(["diff", "--no-ext-diff", "--unified=40", range, "--", ".", ":(exclude)package-lock.json"]);
40+
const truncatedPatch =
41+
Buffer.byteLength(patch, "utf8") > maxPatchBytes
42+
? `${Buffer.from(patch).subarray(0, maxPatchBytes).toString("utf8")}\n\n[Diff truncated at ${maxPatchBytes} bytes.]`
43+
: patch;
44+
45+
writeFileSync(
46+
contextPath,
47+
[
48+
`# Release Diff Context`,
49+
``,
50+
`Current tag: ${tagName}`,
51+
`Previous tag: ${previousTag || "(none; comparing from initial commit)"}`,
52+
`Compared range: ${range}`,
53+
``,
54+
`## Commits`,
55+
commits || "(none)",
56+
``,
57+
`## Changed Files`,
58+
nameStatus || "(none)",
59+
``,
60+
`## Diff Stat`,
61+
diffStat || "(none)",
62+
``,
63+
`## Patch`,
64+
"```diff",
65+
truncatedPatch || "(none)",
66+
"```",
67+
""
68+
].join("\n")
69+
);
70+
71+
const prompt = [
72+
"Summarize this release diff for maintainers and npm users.",
73+
"Use concise Markdown.",
74+
"Focus on user-visible changes, migration notes, packaging changes, and risks.",
75+
"Do not invent changes that are not supported by the attached diff context.",
76+
"Do not include secrets, token values, or operational speculation.",
77+
"Return only the summary body."
78+
].join(" ");
79+
80+
const result = spawnSync(
81+
"npx",
82+
["-y", "opencode-ai@1.17.9", "run", "--pure", "--model", model, "--file", contextPath, prompt],
83+
{
84+
encoding: "utf8",
85+
env: process.env
86+
}
87+
);
88+
89+
if (result.status !== 0) {
90+
console.error(result.stderr || result.stdout || "OpenCode summary generation failed.");
91+
process.exit(result.status ?? 1);
92+
}
93+
94+
const summary = result.stdout.trim();
95+
if (!summary) {
96+
console.log("OpenCode returned an empty summary.");
97+
process.exit(0);
98+
}
99+
100+
writeFileSync(outputPath, `${summary}\n`);
101+
console.log(`Wrote OpenCode release summary to ${outputPath}.`);

0 commit comments

Comments
 (0)