Skip to content

Merge pull request #34 from link-foundation/issue-33-e6a44d7886c4 #23

Merge pull request #34 from link-foundation/issue-33-e6a44d7886c4

Merge pull request #34 from link-foundation/issue-33-e6a44d7886c4 #23

Workflow file for this run

# IMPORTANT: npm trusted publishing (OIDC) requires this workflow filename to match
# the "Workflow" field configured in npm package settings at npmjs.com.
# Currently configured as: js.yml (for link-foundation/lino-arguments)
# If you rename this file, update the npm trusted publisher config accordingly.
# See: docs/case-studies/issue-31/case-study.md
name: JavaScript CI/CD
on:
push:
branches:
- main
paths:
- 'js/**'
- '.github/workflows/js.yml'
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'js/**'
- '.github/workflows/js.yml'
workflow_dispatch:
inputs:
release_mode:
description: 'Manual release mode'
required: true
type: choice
default: 'instant'
options:
- instant
- changeset-pr
bump_type:
description: 'Manual release type'
required: true
type: choice
options:
- patch
- minor
- major
description:
description: 'Manual release description (optional)'
required: false
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
working-directory: js
jobs:
# === DETECT CHANGES - determines which jobs should run ===
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
if: github.event_name != 'workflow_dispatch'
outputs:
js-changed: ${{ steps.changes.outputs.js-changed }}
package-changed: ${{ steps.changes.outputs.package-changed }}
mjs-changed: ${{ steps.changes.outputs.mjs-changed }}
docs-changed: ${{ steps.changes.outputs.docs-changed }}
js-workflow-changed: ${{ steps.changes.outputs.js-workflow-changed }}
js-code-changed: ${{ steps.changes.outputs.js-code-changed }}
js-package-changed: ${{ steps.changes.outputs.js-package-changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Detect changes
id: changes
working-directory: .
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node scripts/detect-code-changes.mjs
# === CHANGESET CHECK - only runs on PRs with JS package code changes ===
# Docs-only PRs don't require changesets
# Workflow-only changes don't require changesets (they don't affect the package)
changeset-check:
name: Check for Changesets
runs-on: ubuntu-latest
needs: [detect-changes]
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.js-package-changed == 'true'
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: Check for changesets
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Skip changeset check for automated version PRs
if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then
echo "Skipping changeset check for automated release PR"
exit 0
fi
# Run changeset validation script
node ../scripts/validate-changeset.mjs
# === VERSION CHECK - prevents manual version modification in PRs ===
version-check:
name: Version Modification Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Check for manual version changes
working-directory: js
env:
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
# Skip version check for automated release PRs
if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then
echo "Skipping version check for automated release PR"
exit 0
fi
# Check if package.json version was manually changed
VERSION_DIFF=$(git diff "origin/${{ github.base_ref }}"...HEAD -- package.json | grep '"version"' || true)
if [ -n "$VERSION_DIFF" ]; then
echo "::error::Manual version changes in package.json are not allowed. Version bumps should be handled by the CI/CD pipeline."
exit 1
fi
echo "No manual version changes detected."
# === LINT AND FORMAT CHECK ===
lint:
name: Lint and Format Check
runs-on: ubuntu-latest
needs: [detect-changes]
# Note: always() is required because detect-changes is skipped on workflow_dispatch
if: |
always() && !cancelled() && (
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.js-changed == 'true' ||
needs.detect-changes.outputs.package-changed == 'true' ||
needs.detect-changes.outputs.js-workflow-changed == 'true'
)
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install dependencies
run: npm install
- name: Run ESLint
run: npm run lint
- name: Check formatting
run: npm run format:check
- name: Check file size limit
run: npm run check:file-size
# Test matrix: 3 runtimes (Node.js, Bun, Deno) x 3 OS (Ubuntu, macOS, Windows)
test:
name: Test (${{ matrix.runtime }} on ${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: [detect-changes, changeset-check]
# Run if: push event, OR changeset-check succeeded, OR changeset-check was skipped (docs-only PR or no JS changes)
if: always() && !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped')
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runtime: [node, bun, deno]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
if: matrix.runtime == 'node'
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install dependencies (Node.js)
if: matrix.runtime == 'node'
run: npm install
- name: Run tests (Node.js)
if: matrix.runtime == 'node'
run: npm test
- name: Setup Node.js (for npm install)
if: matrix.runtime == 'bun'
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies (Bun)
if: matrix.runtime == 'bun'
run: npm install
- name: Run tests (Bun)
if: matrix.runtime == 'bun'
run: bun test
- name: Setup Node.js (for npm install)
if: matrix.runtime == 'deno'
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Setup Deno
if: matrix.runtime == 'deno'
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install dependencies (Deno)
if: matrix.runtime == 'deno'
run: npm install
- name: Run tests (Deno)
if: matrix.runtime == 'deno'
run: deno test --allow-read --allow-env --allow-write
# Release - only runs on main after tests pass (for push events)
release:
name: Release
needs: [lint, test]
# Use always() to ensure this job runs even if changeset-check was skipped
if: always() && !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm install
- name: Update npm for OIDC trusted publishing
run: node ../scripts/setup-npm.mjs
- name: Configure git identity
working-directory: .
run: node scripts/git-config.mjs
- name: Check for changesets
id: check_changesets
run: |
# 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)"
echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
- name: Version packages and commit to main
if: steps.check_changesets.outputs.has_changesets == 'true'
id: version
run: node ../scripts/version-and-commit.mjs --mode changeset --tag-prefix "js_" --release-label "JavaScript"
- name: Check if current version needs publishing
id: check_publish
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "Current version: ${PACKAGE_NAME}@${CURRENT_VERSION}"
# Check if this version is already on npm
if npm view "${PACKAGE_NAME}@${CURRENT_VERSION}" version 2>/dev/null; then
echo "Version ${CURRENT_VERSION} is already published on npm"
echo "needs_publish=false" >> $GITHUB_OUTPUT
else
echo "Version ${CURRENT_VERSION} is NOT published on npm"
echo "needs_publish=true" >> $GITHUB_OUTPUT
fi
echo "current_version=${CURRENT_VERSION}" >> $GITHUB_OUTPUT
- name: Publish to npm
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' || steps.check_publish.outputs.needs_publish == 'true'
id: publish
run: node ../scripts/publish-to-npm.mjs --should-pull
- name: Ensure release tag exists
if: steps.publish.outputs.published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.publish.outputs.published_version }}"
TAG="js_${VERSION}"
# Check if the js_ prefixed tag already exists
if git rev-parse "${TAG}" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists"
else
echo "Tag ${TAG} does not exist, creating it..."
# Try to use the v-prefixed tag commit if it exists (migration from old tag scheme)
if git rev-parse "v${VERSION}" >/dev/null 2>&1; then
COMMIT=$(git rev-parse "v${VERSION}")
echo "Using commit from v${VERSION} tag: ${COMMIT}"
else
COMMIT=$(git rev-parse HEAD)
echo "Using HEAD commit: ${COMMIT}"
fi
git tag -a "${TAG}" "${COMMIT}" -m "Release [JavaScript] ${VERSION}"
git push origin "${TAG}"
echo "Created and pushed tag ${TAG}"
fi
- name: Create GitHub Release
if: steps.publish.outputs.published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node ../scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix "js_" --release-label "JavaScript"
- name: Format GitHub release notes
if: steps.publish.outputs.published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node ../scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix "js_"
# Manual Instant Release
instant-release:
name: Instant Release
if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm install
- name: Update npm for OIDC trusted publishing
run: node ../scripts/setup-npm.mjs
- name: Configure git identity
working-directory: .
run: node scripts/git-config.mjs
- name: Version packages and commit to main
id: version
run: node ../scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" --tag-prefix "js_" --release-label "JavaScript"
- name: Publish to npm
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
id: publish
run: node ../scripts/publish-to-npm.mjs
- name: Create GitHub Release
if: steps.publish.outputs.published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node ../scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix "js_" --release-label "JavaScript"
- name: Format GitHub release notes
if: steps.publish.outputs.published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node ../scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix "js_"
# Manual Changeset PR
changeset-pr:
name: Create Changeset PR
if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
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 file
run: node ../scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}"
- name: Format changeset with Prettier
run: npx prettier --write ".changeset/*.md" || true
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore(js): add changeset for manual ${{ github.event.inputs.bump_type }} release'
branch: changeset-manual-release-js-${{ github.run_id }}
delete-branch: true
title: 'chore(js): manual ${{ github.event.inputs.bump_type }} release'
body: |
## Manual Release Request (JavaScript)
This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release.
### Release Details
- **Type:** ${{ github.event.inputs.bump_type }}
- **Description:** ${{ github.event.inputs.description || 'Manual release' }}
- **Triggered by:** @${{ github.actor }}