Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 0 additions & 98 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,101 +90,3 @@ jobs:
PACKAGE_VERSION="${{ steps.package.outputs.version }}"
echo "Publishing ${PACKAGE_VERSION} with dist-tag ${NPM_DIST_TAG}."
npm publish --tag "${NPM_DIST_TAG}"

- name: Extract release notes from CHANGELOG.md
id: release-notes
env:
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: |
node --input-type=module - <<'NODE'
import fs from 'node:fs';

const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
const version = process.env.PACKAGE_VERSION;
const headings = [`## ${version}`, `## v${version}`];

let releaseBody = '';
for (const heading of headings) {
const start = changelog.indexOf(`${heading}\n`);
if (start === -1) {
continue;
}

const contentStart = start + heading.length + 1;
const remaining = changelog.slice(contentStart);
const nextHeading = remaining.search(/\n##\s+/);
releaseBody = remaining
.slice(0, nextHeading === -1 ? undefined : nextHeading)
.trim();
break;
}

fs.writeFileSync(
'release-notes.md',
releaseBody
? `# ${version}\n\n${releaseBody}\n`
: `# ${version}\n\nNo matching CHANGELOG.md section was found for this package version.\n`,
);
NODE

if grep -q "No matching CHANGELOG.md section" release-notes.md; then
echo "found=false" >> "$GITHUB_OUTPUT"
else
echo "found=true" >> "$GITHUB_OUTPUT"
fi

- name: Create or update GitHub Release
uses: actions/github-script@v7
env:
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
PACKAGE_PRERELEASE: ${{ steps.package.outputs.prerelease }}
CHANGELOG_FOUND: ${{ steps.release-notes.outputs.found }}
with:
script: |
const fs = require('node:fs');

const owner = context.repo.owner;
const repo = context.repo.repo;
const version = process.env.PACKAGE_VERSION;
const tag = `v${version}`;
const prerelease = process.env.PACKAGE_PRERELEASE === 'true';
const body = fs.readFileSync('release-notes.md', 'utf8');
const releasePayload = {
owner,
repo,
tag_name: tag,
name: tag,
body,
prerelease,
draft: false,
target_commitish: context.sha,
};

if (process.env.CHANGELOG_FOUND !== 'true') {
core.warning(`No matching CHANGELOG.md section was found for ${version}. The GitHub Release will include a placeholder note.`);
}

try {
const existing = await github.rest.repos.getReleaseByTag({
owner,
repo,
tag,
});

await github.rest.repos.updateRelease({
owner,
repo,
release_id: existing.data.id,
tag_name: tag,
name: tag,
body,
prerelease,
draft: false,
});
} catch (error) {
if (error.status !== 404) {
throw error;
}

await github.rest.repos.createRelease(releasePayload);
}
26 changes: 26 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Release Please

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: write
pull-requests: write

concurrency:
group: release-please-${{ github.ref }}
cancel-in-progress: false

jobs:
release-please:
runs-on: ubuntu-latest

steps:
- name: Create or update release pull request
uses: googleapis/release-please-action@v4
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.2"
}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Updated release preparation to keep the README package status version aligned with `package.json`.
- Added Release Please configuration to generate changelog entries and release pull requests from Conventional Commits.

## 0.1.2 - 2026-05-02

- Added named node ports with `.port(id, { side })` and `connect(..., { sourcePort, targetPort })`.
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Browser-first TypeScript library for interactive node canvases.

## Package Status

`0.1.1` is an early public release. The current API is ready for use and feedback, but it is not a finalized `1.0` contract yet.
`0.1.2` is an early public release. The current API is ready for use and feedback, but it is not a finalized `1.0` contract yet. <!-- x-release-please-version -->

Supported in this alpha:

Expand Down Expand Up @@ -278,7 +278,17 @@ npm run check

### Release Automation

Publishing stays in GitHub Actions through npm trusted publishing. The local release script prepares version files and changelog entries, but it does not commit, tag, push, or publish.
Release Please creates release pull requests from Conventional Commit messages. Those pull requests update package versions, `CHANGELOG.md`, and the README package status. When a Release Please PR is merged, Release Please creates the GitHub Release and the existing npm workflow publishes the already-versioned package through trusted publishing.

Use commit prefixes consistently so Release Please can calculate the right version:

```bash
feat: add packet payload model
fix: correct packet route validation
docs: update port examples
```

The local release script remains available as a manual fallback. It prepares version files, the README package status, and changelog entries, but it does not commit, tag, push, or publish.

```bash
npm run release:prepare -- patch --yes
Expand All @@ -293,7 +303,7 @@ Useful options:
- `--allow-empty`: create a release section even when `Unreleased` has no entries
- `--dispatch`: trigger the publish workflow after release prep

Recommended release flow:
Manual fallback release flow:

```bash
npm run release:prepare -- patch --yes
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- Packet routing through required waypoint nodes
- Named ports and connect-to-specific-port API
- Release preparation script and CI workflow improvements
- Release Please automation for changelog and release pull requests
- Documentation and usage examples

## Next
Expand Down
15 changes: 15 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"packages": {
".": {
"extra-files": [
{
"path": "README.md",
"type": "generic"
}
],
"include-component-in-tag": false,
"package-name": "@darbsen/cnodes",
"release-type": "node"
}
}
}
16 changes: 16 additions & 0 deletions scripts/release.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function computeNextVersion(
version: string,
bump: string,
preid?: string,
): string;

export function promoteUnreleasedChangelog(
changelog: string,
version: string,
date: string,
options?: Readonly<{
allowEmpty?: boolean;
}>,
): string;

export function updateReadmeVersion(readme: string, version: string): string;
24 changes: 23 additions & 1 deletion scripts/release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ export function promoteUnreleasedChangelog(
].join('\n');
}

export function updateReadmeVersion(readme, version) {
const statusVersionPattern =
/`(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+\.\d+)?)` is an early public release\./;

if (!statusVersionPattern.test(readme)) {
throw new Error('README.md package status version marker was not found.');
}

return readme.replace(
statusVersionPattern,
`\`${version}\` is an early public release.`,
);
}

function parseVersion(version) {
const match = version.match(
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+)\.(\d+))?$/,
Expand Down Expand Up @@ -220,6 +234,8 @@ function prepareRelease(options) {
getToday(),
{ allowEmpty: options.allowEmpty },
);
const readme = readFileSync('README.md', 'utf8');
const nextReadme = updateReadmeVersion(readme, nextVersion);

if (options.dryRun) {
console.log(`Would prepare ${packageJson.name}@${nextVersion}.`);
Expand All @@ -234,6 +250,7 @@ function prepareRelease(options) {

run('npm', ['version', nextVersion, '--no-git-tag-version']);
writeFileSync('CHANGELOG.md', nextChangelog);
writeFileSync('README.md', nextReadme);
console.log(`Prepared ${packageJson.name}@${nextVersion}.`);

if (options.dispatch) {
Expand All @@ -253,7 +270,12 @@ function dispatchPublishWorkflow() {
}

function ensureRepositoryFiles() {
for (const file of ['package.json', 'package-lock.json', 'CHANGELOG.md']) {
for (const file of [
'package.json',
'package-lock.json',
'CHANGELOG.md',
'README.md',
]) {
if (!existsSync(file)) {
throw new Error(`Expected ${file} in the current working directory.`);
}
Expand Down
47 changes: 47 additions & 0 deletions test/release-please-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { readFileSync } from 'node:fs';

import { describe, expect, it } from 'vitest';

type ReleasePleaseConfig = Readonly<{
packages: Readonly<
Record<
string,
Readonly<{
'include-component-in-tag'?: boolean;
'package-name'?: string;
'release-type'?: string;
}>
>
>;
}>;

describe('release-please configuration', () => {
it('tracks the current package version in the release manifest', () => {
const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as {
version: string;
};
const manifest = JSON.parse(
readFileSync('.release-please-manifest.json', 'utf8'),
) as Record<string, string>;

expect(manifest['.']).toBe(packageJson.version);
});

it('configures the root node package without component-prefixed tags', () => {
const config = JSON.parse(
readFileSync('release-please-config.json', 'utf8'),
) as ReleasePleaseConfig;

expect(config.packages['.']).toMatchObject({
'include-component-in-tag': false,
'package-name': '@darbsen/cnodes',
'release-type': 'node',
});
});

it('marks the README package status version for release-please updates', () => {
const readme = readFileSync('README.md', 'utf8');

expect(readme).toContain('x-release-please-version');
});
});
33 changes: 31 additions & 2 deletions test/release-script.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { describe, expect, it } from 'vitest';

// @ts-expect-error The release helper is a Node ESM script, not TS source.
import { computeNextVersion, promoteUnreleasedChangelog } from '../scripts/release.mjs';
import {
computeNextVersion,
promoteUnreleasedChangelog,
updateReadmeVersion,
} from '../scripts/release.mjs';

describe('release script helpers', () => {
it('computes stable semver bumps', () => {
Expand Down Expand Up @@ -63,4 +66,30 @@ describe('release script helpers', () => {
'CHANGELOG.md has no Unreleased entries.',
);
});

it('updates the README package status version', () => {
const readme = [
'# cnodes',
'',
'## Package Status',
'',
'`0.1.1` is an early public release. The current API is ready for use and feedback, but it is not a finalized `1.0` contract yet.',
'',
].join('\n');

expect(updateReadmeVersion(readme, '0.1.2')).toBe([
'# cnodes',
'',
'## Package Status',
'',
'`0.1.2` is an early public release. The current API is ready for use and feedback, but it is not a finalized `1.0` contract yet.',
'',
].join('\n'));
});

it('throws when the README package status version marker is missing', () => {
expect(() => updateReadmeVersion('# cnodes\n', '0.1.2')).toThrowError(
'README.md package status version marker was not found.',
);
});
});
Loading