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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.head_ref }}
cancel-in-progress: true

jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- run: npm test
166 changes: 166 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: Publish

on:
push:
branches: [main]
paths:
- "base.json"
- "nodejs.json"
- "node-library.json"
- "react.json"
- "nextjs.json"
- "package.json"
- ".github/workflows/publish.yml"
workflow_dispatch:
inputs:
bump:
description: "Version bump type"
required: true
type: choice
options:
- patch
- minor
- major

concurrency:
group: publish-${{ github.event_name }}
cancel-in-progress: true

permissions:
contents: write
id-token: write

jobs:
quality:
name: Quality Gates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- run: npm test

canary:
name: Publish Canary
needs: quality
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org

- name: Upgrade npm for OIDC support
run: npm install -g npm@latest

- name: Publish canary
run: |
sed -i '/_authToken/d' "$NPM_CONFIG_USERCONFIG"
unset NODE_AUTH_TOKEN

BASE_VERSION=$(node -p "require('./package.json').version")
SHORT_SHA=$(echo "$GITHUB_SHA" | cut -c1-7)
CANARY_VERSION="${BASE_VERSION}-canary.${SHORT_SHA}"
npm version "$CANARY_VERSION" --no-git-tag-version
npm publish --tag canary --provenance --access public

release:
name: Publish Release
needs: quality
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org

- name: Bump version
id: version
run: |
npm version ${{ inputs.bump }} --no-git-tag-version
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Generate changelog
id: changelog
run: |
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
RANGE="HEAD"
else
RANGE="${LAST_TAG}..HEAD"
fi

{
echo "body<<CHANGELOG_EOF"

FEATS=$(git log "$RANGE" --pretty=format:"%s" --grep="^feat" 2>/dev/null || true)
if [ -n "$FEATS" ]; then
echo "### Features"
echo "$FEATS" | sed 's/^/- /'
echo ""
fi

FIXES=$(git log "$RANGE" --pretty=format:"%s" --grep="^fix" 2>/dev/null || true)
if [ -n "$FIXES" ]; then
echo "### Bug Fixes"
echo "$FIXES" | sed 's/^/- /'
echo ""
fi

OTHERS=$(git log "$RANGE" --pretty=format:"%s" --invert-grep --grep="^feat" --grep="^fix" 2>/dev/null || true)
if [ -n "$OTHERS" ]; then
echo "### Other Changes"
echo "$OTHERS" | sed 's/^/- /'
echo ""
fi

echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"

- name: Commit and tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore(release): v${{ steps.version.outputs.version }}"
git tag -a "v${{ steps.version.outputs.version }}" -m "v${{ steps.version.outputs.version }}"
git push origin main --follow-tags

- name: Upgrade npm for OIDC support
run: npm install -g npm@latest

- name: Publish to npm
run: |
sed -i '/_authToken/d' "$NPM_CONFIG_USERCONFIG"
unset NODE_AUTH_TOKEN
npm publish --tag latest --provenance --access public

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cat <<'NOTES_EOF' > /tmp/release-notes.md
${{ steps.changelog.outputs.body }}
NOTES_EOF
gh release create "v${{ steps.version.outputs.version }}" \
--title "v${{ steps.version.outputs.version }}" \
--notes-file /tmp/release-notes.md
12 changes: 5 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,12 @@ git push && git push --tags

## Publishing (CI)

Push a version tag — CI handles the rest:
Two modes via `publish.yml`:

```sh
npm version patch
git push && git push --tags
# release.yml: npm publish --provenance → GitHub Release
```
- **Canary**: auto-publishes on push to main (when config files change) as `x.y.z-canary.<sha>` on `canary` tag
- **Release**: manual trigger via Actions → Publish → workflow_dispatch, select bump type (patch/minor/major)

Requires `NPM_TOKEN` secret in GitHub repo settings.
Uses npm OIDC provenance — no `NPM_TOKEN` secret needed. Requires npm trusted publishers configured on npmjs.org.

## Project structure

Expand All @@ -46,5 +43,6 @@ nextjs.json ← Next.js apps (Bundler, ES2022, noEmit, next plugin)

| Command | What |
|---------|------|
| `npm test` | Run smoke tests (JSON validation) |
| `npm pack --dry-run` | Preview tarball contents |
| `npm view @vllnt/typescript` | Check published version |
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"node-library.json",
"react.json"
],
"scripts": {
"test": "node --test tests/smoke.test.js",
"prepublishOnly": "node --test tests/smoke.test.js"
},
"publishConfig": {
"access": "public"
}
Expand Down
35 changes: 35 additions & 0 deletions tests/smoke.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { test } = require('node:test')
const assert = require('node:assert/strict')
const { readFileSync, readdirSync } = require('node:fs')
const { join } = require('node:path')

const root = join(__dirname, '..')

const configs = readdirSync(root).filter(
(f) => f.endsWith('.json') && f !== 'package.json',
)

test('all config files are valid JSON with compilerOptions', () => {
for (const config of configs) {
const raw = readFileSync(join(root, config), 'utf-8')
const parsed = JSON.parse(raw)
assert.ok(parsed.compilerOptions, `${config} must have compilerOptions`)
assert.equal(typeof parsed.compilerOptions, 'object')
}
})

test('derived configs extend base.json', () => {
const derived = configs.filter((c) => c !== 'base.json')
for (const config of derived) {
const raw = readFileSync(join(root, config), 'utf-8')
const parsed = JSON.parse(raw)
assert.equal(parsed.extends, './base.json', `${config} must extend base.json`)
}
})

test('package.json files array includes all configs', () => {
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'))
for (const config of configs) {
assert.ok(pkg.files.includes(config), `${config} must be in package.json files`)
}
})