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
99 changes: 99 additions & 0 deletions .github/workflows/npm-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: NPM Release

on:
workflow_dispatch:
inputs:
enable_publish:
description: Set to true only after RELEASE.md is complete and package privacy has been intentionally changed.
required: true
default: false
type: boolean
expected_version:
description: Optional package.json version expected for this release.
required: false
type: string

permissions:
contents: read
id-token: write

jobs:
release:
name: Verify npm release artifact
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 10.24.0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use npm 11+ in release workflow

The publish path is configured around npm Trusted Publishing, but this job pins node-version: 22, and Node 22 currently ships npm 10.x; npm’s trusted-publisher flow requires npm CLI 11.5.1+ to exchange OIDC tokens, so npm publish can fail with auth errors once token-based publishing is disabled as described in RELEASE.md. This means the workflow can pass all verification gates and still be unable to publish in the intended secure mode unless npm is explicitly upgraded (or Node 24 is used).

Useful? React with 👍 / 👎.

cache: pnpm
registry-url: https://registry.npmjs.org

- name: Install
run: pnpm install --frozen-lockfile

- name: Test
run: pnpm test

- name: Typecheck
run: pnpm typecheck

- name: Lint
run: pnpm lint

- name: Build
run: pnpm build

- name: Verify packed CLI
run: pnpm verify:packed

- name: Verify release metadata
env:
EXPECTED_VERSION: ${{ inputs.expected_version }}
run: |
node <<'NODE'
const packageJson = require('./package.json');
if (packageJson.name !== '@mattbaconz/kernel') {
throw new Error(`Unexpected package name: ${packageJson.name}`);
}
if (packageJson.publishConfig?.access !== 'public') {
throw new Error('Expected publishConfig.access to be public.');
}
if (process.env.EXPECTED_VERSION && packageJson.version !== process.env.EXPECTED_VERSION) {
throw new Error(`Expected version ${process.env.EXPECTED_VERSION}, got ${packageJson.version}.`);
}
console.log(`package: ${packageJson.name}@${packageJson.version}`);
console.log(`private: ${String(packageJson.private)}`);
NODE

- name: Pack dry run
run: npm pack --dry-run --json

- name: Refuse publish by default
if: ${{ inputs.enable_publish != true }}
run: |
echo "Publication disabled. Set enable_publish=true only after RELEASE.md is complete."
echo "No npm publish command was run."

- name: Verify publish gate
if: ${{ inputs.enable_publish == true }}
run: |
node <<'NODE'
const packageJson = require('./package.json');
if (packageJson.private) {
throw new Error('Refusing to publish while package.json private=true. Remove private:true in a separate release task.');
}
NODE

- name: Publish to npm
if: ${{ inputs.enable_publish == true }}
run: npm publish --access public
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This project follows semantic versioning once public releases begin.
- Schema discovery commands.
- Release-readiness integration tests and packed CLI verification.
- GitHub CI workflow.
- npm release-readiness checklist and gated manual release workflow skeleton.

### Notes

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ pnpm verify:packed

CI runs the same core checks on `main` and pull requests.

## Release Readiness

Kernel is not published to npm yet. Release gates, trusted-publishing setup notes, provenance expectations, and rollback checks are documented in `RELEASE.md`.

## JSON Outputs And Schemas

Machine-readable command outputs include `schemaVersion: 1`.
Expand Down
96 changes: 96 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Release Checklist

Kernel is not published to npm yet. This checklist prepares `@mattbaconz/kernel` for a future public npm release without enabling publication now.

## Current Gate

- Do not publish while `package.json` has `"private": true`.
- Do not remove `"private": true` except in a dedicated npm publication task.
- Do not publish from the private `mattbaconz/kernel-skills` repository.
- Use the public `mattbaconz/kernel` repository as the trusted publishing source.

## Package Metadata

- Package name: `@mattbaconz/kernel`
- License: Apache-2.0
- Copyright: `Copyright 2026 mattbaconz`
- Package files: `dist/` and `schemas/`
- Public package access: configured with `publishConfig.access: public`
- npm package remains unpublished until a separate explicit release task.

## Required Verification

Run these checks before any release tag or npm publication:

```bash
pnpm install --frozen-lockfile
pnpm test
pnpm typecheck
pnpm lint
pnpm build
pnpm verify:packed
npm pack --dry-run --json
```

Review the dry-run package contents and confirm they include only expected files:

- `LICENSE`
- `README.md`
- `package.json`
- `dist/`
- `schemas/`

## Trusted Publishing

Preferred release path is npm Trusted Publishing from GitHub Actions, not a long-lived npm token.

Before enabling publication:

1. Configure npm trusted publishing for `@mattbaconz/kernel`.
2. Set the trusted publisher to the public GitHub repository `mattbaconz/kernel`.
3. Set the workflow file to `.github/workflows/npm-release.yml`.
4. Confirm the workflow has `id-token: write`.
5. Confirm npm package ownership and 2FA settings for the `mattbaconz` account.

Trusted publishing uses OIDC. With trusted publishing, npm generates provenance attestations automatically. If trusted publishing is not available, do not fall back to a broad token without a separate release security review.

## Manual Workflow

The manual workflow `.github/workflows/npm-release.yml` is intentionally gated.

Default behavior:

- verifies the release artifact
- runs `npm pack --dry-run --json`
- refuses to publish because `enable_publish` defaults to `false`

Publication behavior:

- requires manual `workflow_dispatch`
- requires `enable_publish: true`
- refuses to publish while `package.json` has `"private": true`
- uses `npm publish --access public` only after the gates above pass

## Tag And Release

Before npm publication:

1. Confirm `CHANGELOG.md` has the intended version notes.
2. Confirm `package.json` version matches the release tag.
3. Create or verify the release tag, for example `v0.1.0`.
4. Confirm public CI is green on the tagged commit.
5. Create a GitHub Release after the tag is final.

## Rollback

npm packages cannot be treated like normal mutable deploys.

Use this rollback section for containment and follow-up when a release is wrong.

If a bad package is published:

1. Stop further publication.
2. Open a security or release issue if user impact is possible.
3. Deprecate the bad version on npm if appropriate.
4. Publish a fixed patch version instead of replacing the bad version.
5. Record evidence in the private source repository.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"dist",
"schemas"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit -p tsconfig.json",
Expand Down
45 changes: 45 additions & 0 deletions tests/release-readiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';

import type { Command } from 'commander';
import { parse } from 'yaml';
import { afterEach, describe, expect, test, vi } from 'vitest';

import { createKernelProgram } from '../src/cli/index.js';
Expand Down Expand Up @@ -159,6 +160,7 @@ describe('Kernel release-readiness workflow', () => {
private?: boolean;
bin?: Record<string, string>;
files?: string[];
publishConfig?: Record<string, string>;
scripts?: Record<string, string>;
};

Expand All @@ -169,6 +171,9 @@ describe('Kernel release-readiness workflow', () => {
kernel: './dist/cli/index.js'
});
expect(packageJson.files).toEqual(expect.arrayContaining(['dist', 'schemas']));
expect(packageJson.publishConfig).toEqual({
access: 'public'
});
expect(packageJson.scripts).toEqual(
expect.objectContaining({
build: 'tsc -p tsconfig.build.json',
Expand All @@ -183,6 +188,46 @@ describe('Kernel release-readiness workflow', () => {
);
await expect(readFile(join(repoRoot, 'LICENSE'), 'utf8')).resolves.toContain('Copyright 2026 mattbaconz');
});

test('documents npm release readiness without enabling publication', async () => {
const releaseChecklist = await readFile(join(repoRoot, 'RELEASE.md'), 'utf8');

expect(releaseChecklist).toContain('@mattbaconz/kernel');
expect(releaseChecklist).toContain('Do not publish while `package.json` has `"private": true`.');
expect(releaseChecklist).toContain('Trusted publishing');
expect(releaseChecklist).toContain('provenance');
expect(releaseChecklist).toContain('npm publish');
expect(releaseChecklist).toContain('rollback');
});

test('keeps npm release workflow manual and publish-gated', async () => {
const workflowText = await readFile(join(repoRoot, '.github', 'workflows', 'npm-release.yml'), 'utf8');
const workflow = parse(workflowText) as {
name?: string;
on?: Record<string, unknown>;
permissions?: Record<string, string>;
jobs?: Record<string, { steps?: Array<{ if?: string; run?: string }> }>;
};

expect(workflow.name).toBe('NPM Release');
expect(Object.keys(workflow.on ?? {})).toEqual(['workflow_dispatch']);
expect(workflow.permissions).toEqual({
contents: 'read',
'id-token': 'write'
});
expect(workflowText).toContain('enable_publish');
expect(workflowText).toContain('pnpm install --frozen-lockfile');
expect(workflowText).toContain('pnpm test');
expect(workflowText).toContain('pnpm typecheck');
expect(workflowText).toContain('pnpm lint');
expect(workflowText).toContain('pnpm build');
expect(workflowText).toContain('pnpm verify:packed');
expect(workflowText).toContain('npm pack --dry-run --json');
expect(workflowText).toContain("if: ${{ inputs.enable_publish != true }}");
expect(workflowText).toContain("if: ${{ inputs.enable_publish == true }}");
expect(workflowText).toContain('package.json private=true');
expect(workflowText).toContain('npm publish --access public');
});
});

async function createReleaseFixture(): Promise<string> {
Expand Down