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
5 changes: 5 additions & 0 deletions .changeset/manual-release-5d3d8c98.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lino-arguments': patch
---

Adapt CI/CD pipeline for npm trusted publishing with provenance support. Replace shell script changeset validation with .mjs script. Add manual release workflow and helper scripts for changeset management.
53 changes: 4 additions & 49 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,53 +36,8 @@ jobs:
exit 0
fi

# Count changeset files (excluding README.md and config.json)
CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l)

echo "Found $CHANGESET_COUNT changeset file(s)"

# Ensure exactly one changeset file exists
if [ "$CHANGESET_COUNT" -eq 0 ]; then
echo "::error::No changeset found. Please add a changeset by running 'npm run changeset' and commit the result."
exit 1
elif [ "$CHANGESET_COUNT" -gt 1 ]; then
echo "::error::Multiple changesets found ($CHANGESET_COUNT). Each PR should have exactly ONE changeset."
echo "::error::Found changeset files:"
find .changeset -name "*.md" ! -name "README.md" -exec basename {} \;
exit 1
fi

# Get the changeset file
CHANGESET_FILE=$(find .changeset -name "*.md" ! -name "README.md" | head -1)
echo "Validating changeset: $CHANGESET_FILE"

# Check if changeset has a valid type (major, minor, or patch)
if ! grep -qE "^['\"]lino-arguments['\"]:\s+(major|minor|patch)" "$CHANGESET_FILE"; then
echo "::error::Changeset must specify a version type: major, minor, or patch"
echo "::error::Expected format in $CHANGESET_FILE:"
echo "::error::---"
echo "::error::'lino-arguments': patch"
echo "::error::---"
echo "::error::"
echo "::error::Your description here"
cat "$CHANGESET_FILE"
exit 1
fi

# Extract description (everything after the closing ---) and check it's not empty
DESCRIPTION=$(awk '/^---$/{count++; next} count==2' "$CHANGESET_FILE" | sed '/^[[:space:]]*$/d')

if [ -z "$DESCRIPTION" ]; then
echo "::error::Changeset must include a description of the changes"
echo "::error::The description should appear after the closing '---' in the changeset file"
echo "::error::Current content of $CHANGESET_FILE:"
cat "$CHANGESET_FILE"
exit 1
fi

echo "✅ Changeset validation passed"
echo " Type: $(grep -E "^['\"]lino-arguments['\"]:" "$CHANGESET_FILE" | sed "s/.*: //")"
echo " Description: $DESCRIPTION"
# Run validation script
node scripts/validate-changeset.mjs

# Linting and formatting - runs after changeset check on PRs, immediately on main
lint:
Expand Down Expand Up @@ -266,8 +221,8 @@ jobs:
# Pull the latest changes we just pushed
git pull origin main

# Publish to npm using OIDC trusted publishing
npm run changeset:publish
# Publish to npm using OIDC trusted publishing with provenance
npm run changeset:publish --provenance

echo "published=true" >> $GITHUB_OUTPUT

Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/manual-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Manual Release

on:
workflow_dispatch:
inputs:
bump_type:
description: 'Release type'
required: true
type: choice
options:
- patch
- minor
- major
description:
description: 'Release description (optional)'
required: false
type: string

permissions:
contents: write
pull-requests: write

jobs:
create-changeset:
name: Create Changeset
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'

- name: Install dependencies
run: npm install

- name: Create changeset
run: |
if [ -n "${{ inputs.description }}" ]; then
node scripts/create-manual-changeset.mjs "${{ inputs.bump_type }}" "${{ inputs.description }}"
else
node scripts/create-manual-changeset.mjs "${{ inputs.bump_type }}"
fi

- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
commit-message: 'chore: create changeset for ${{ inputs.bump_type }} release'
branch: changeset-release/manual-${{ github.run_id }}
title: 'chore: ${{ inputs.bump_type }} release'
body: |
## Manual Release Request

**Release Type:** `${{ inputs.bump_type }}`
**Description:** ${{ inputs.description || 'Manual release' }}
**Requested by:** @${{ github.actor }}

This PR contains a changeset for a manual ${{ inputs.bump_type }} release.

### Next Steps

1. Review the changeset file
2. Merge this PR to main
3. The automated release workflow will:
- Bump the version
- Update the CHANGELOG
- Publish to npm
- Create a GitHub release

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"changeset": "changeset",
"changeset:version": "node scripts/changeset-version.mjs",
"changeset:publish": "changeset publish",
"changeset:status": "changeset status --since=origin/main"
"changeset:status": "changeset status --since=origin/main",
"changeset:validate": "node scripts/validate-changeset.mjs",
"changeset:create": "node scripts/create-manual-changeset.mjs"
},
"keywords": [
"lino",
Expand Down
54 changes: 54 additions & 0 deletions scripts/create-manual-changeset.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node

/**
* Create a changeset file for manual releases
* Usage: node scripts/create-manual-changeset.mjs <bump_type> [description]
*/

import { writeFileSync } from 'fs';
import { randomBytes } from 'crypto';
import { execSync } from 'child_process';

try {
// Get bump type from command line arguments
const bumpType = process.argv[2];
const description =
process.argv.slice(3).join(' ') || `Manual ${bumpType} release`;

if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) {
console.error(
'Usage: node scripts/create-manual-changeset.mjs <major|minor|patch> [description]'
);
process.exit(1);
}

// Generate a random changeset ID
const changesetId = randomBytes(4).toString('hex');
const changesetFile = `.changeset/manual-release-${changesetId}.md`;

// Create the changeset file with single quotes to match Prettier config
const content = `---
'lino-arguments': ${bumpType}
---

${description}
`;

writeFileSync(changesetFile, content, 'utf-8');

console.log(`Created changeset: ${changesetFile}`);
console.log('Content:');
console.log(content);

// Format with Prettier
console.log('\nFormatting with Prettier...');
execSync(`npx prettier --write "${changesetFile}"`, { stdio: 'inherit' });

console.log('\n✅ Changeset created and formatted successfully');
} catch (error) {
console.error('Error creating changeset:', error.message);
if (process.env.DEBUG) {
console.error('Stack trace:', error.stack);
}
process.exit(1);
}
99 changes: 99 additions & 0 deletions scripts/validate-changeset.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env node

/**
* Validate changeset for CI - ensures exactly one valid changeset exists
*/

import { readdirSync, readFileSync } from 'fs';
import { join } from 'path';

try {
// Count changeset files (excluding README.md and config.json)
const changesetDir = '.changeset';
const changesetFiles = readdirSync(changesetDir).filter(
(file) => file.endsWith('.md') && file !== 'README.md'
);

const changesetCount = changesetFiles.length;
console.log(`Found ${changesetCount} changeset file(s)`);

// Ensure exactly one changeset file exists
if (changesetCount === 0) {
console.error(
"::error::No changeset found. Please add a changeset by running 'npm run changeset' and commit the result."
);
process.exit(1);
} else if (changesetCount > 1) {
console.error(
`::error::Multiple changesets found (${changesetCount}). Each PR should have exactly ONE changeset.`
);
console.error('::error::Found changeset files:');
changesetFiles.forEach((file) => console.error(` ${file}`));
process.exit(1);
}

// Get the changeset file
const changesetFile = join(changesetDir, changesetFiles[0]);
console.log(`Validating changeset: ${changesetFile}`);

// Read the changeset file
const content = readFileSync(changesetFile, 'utf-8');

// Check if changeset has a valid type (major, minor, or patch)
const versionTypeRegex = /^['"]lino-arguments['"]:\s+(major|minor|patch)/m;
if (!versionTypeRegex.test(content)) {
console.error(
'::error::Changeset must specify a version type: major, minor, or patch'
);
console.error(`::error::Expected format in ${changesetFile}:`);
console.error('::error::---');
console.error("::error::'lino-arguments': patch");
console.error('::error::---');
console.error('::error::');
console.error('::error::Your description here');
console.error('\nFile content:');
console.error(content);
process.exit(1);
}

// Extract description (everything after the closing ---) and check it's not empty
const parts = content.split('---');
if (parts.length < 3) {
console.error(
'::error::Changeset must include a description of the changes'
);
console.error(
"::error::The description should appear after the closing '---' in the changeset file"
);
console.error(`::error::Current content of ${changesetFile}:`);
console.error(content);
process.exit(1);
}

const description = parts.slice(2).join('---').trim();
if (!description) {
console.error(
'::error::Changeset must include a description of the changes'
);
console.error(
"::error::The description should appear after the closing '---' in the changeset file"
);
console.error(`::error::Current content of ${changesetFile}:`);
console.error(content);
process.exit(1);
}

// Extract version type
const versionTypeMatch = content.match(versionTypeRegex);
const versionType = versionTypeMatch ? versionTypeMatch[1] : 'unknown';

console.log('✅ Changeset validation passed');
console.log(` Type: ${versionType}`);
console.log(` Description: ${description}`);
} catch (error) {
console.error('Error during changeset validation:', error.message);
if (process.env.DEBUG) {
console.error('Stack trace:', error.stack);
}
process.exit(1);
}