From f3ff4430c9a2e0747a1e17c57aef8a719f85a065 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 13:50:31 +0000 Subject: [PATCH 1/2] feat: adapt CI/CD for npm trusted publishing - Add npm trusted publishing with provenance support - Replace shell script changeset validation with .mjs script - Add manual release workflow for manual publishing - Add create-manual-changeset.mjs script - Add validate-changeset.mjs script - Update package.json with new changeset scripts Based on best practices from lino-env and test-anywhere repos. --- .github/workflows/main.yml | 53 ++------------- .github/workflows/manual-release.yml | 75 +++++++++++++++++++++ package.json | 4 +- scripts/create-manual-changeset.mjs | 54 +++++++++++++++ scripts/validate-changeset.mjs | 99 ++++++++++++++++++++++++++++ 5 files changed, 235 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/manual-release.yml create mode 100755 scripts/create-manual-changeset.mjs create mode 100755 scripts/validate-changeset.mjs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cbb2697..1cc82c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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: @@ -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 diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml new file mode 100644 index 0000000..609a82d --- /dev/null +++ b/.github/workflows/manual-release.yml @@ -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) diff --git a/package.json b/package.json index db1ef91..3d6395a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/create-manual-changeset.mjs b/scripts/create-manual-changeset.mjs new file mode 100755 index 0000000..428755e --- /dev/null +++ b/scripts/create-manual-changeset.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +/** + * Create a changeset file for manual releases + * Usage: node scripts/create-manual-changeset.mjs [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 [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); +} diff --git a/scripts/validate-changeset.mjs b/scripts/validate-changeset.mjs new file mode 100755 index 0000000..59ab7e6 --- /dev/null +++ b/scripts/validate-changeset.mjs @@ -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); +} From b08f95ccf3b0917acb806a05393d5c93f7511c70 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 15 Nov 2025 13:57:17 +0000 Subject: [PATCH 2/2] Add changeset for CI/CD pipeline adaptation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add changeset documenting the CI/CD pipeline improvements including npm trusted publishing with provenance support, replacement of shell script validation with .mjs script, and addition of manual release workflow. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/manual-release-5d3d8c98.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/manual-release-5d3d8c98.md diff --git a/.changeset/manual-release-5d3d8c98.md b/.changeset/manual-release-5d3d8c98.md new file mode 100644 index 0000000..3752e6b --- /dev/null +++ b/.changeset/manual-release-5d3d8c98.md @@ -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.