Align CI/CD pipelines with template best practices #21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| jobs: | |
| # === DETECT CHANGES - determines which jobs should run === | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'workflow_dispatch' | |
| outputs: | |
| mjs-changed: ${{ steps.changes.outputs.mjs-changed }} | |
| js-changed: ${{ steps.changes.outputs.js-changed }} | |
| package-changed: ${{ steps.changes.outputs.package-changed }} | |
| docs-changed: ${{ steps.changes.outputs.docs-changed }} | |
| workflow-changed: ${{ steps.changes.outputs.workflow-changed }} | |
| any-code-changed: ${{ steps.changes.outputs.any-code-changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect changes | |
| id: changes | |
| working-directory: ./js | |
| 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 | |
| # === VERSION CHANGE CHECK === | |
| # Prohibit manual version changes in package.json - versions should only be changed by CI/CD | |
| version-check: | |
| name: Check for Manual Version Changes | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for version changes in package.json | |
| working-directory: ./js | |
| env: | |
| GITHUB_HEAD_REF: ${{ github.head_ref }} | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| run: node scripts/check-version.mjs | |
| # === CHANGESET CHECK - only runs on PRs with code changes === | |
| # Docs-only PRs (./docs folder, markdown files) don't require changesets | |
| changeset-check: | |
| name: Check for Changesets | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| working-directory: ./js | |
| run: npm install | |
| - name: Check for changesets | |
| working-directory: ./js | |
| 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 | |
| # === LINT AND FORMAT CHECK === | |
| # Lint runs independently of changeset-check - it's a fast check that should always run | |
| lint: | |
| name: Lint and Format Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: | | |
| always() && !cancelled() && ( | |
| github.event_name == 'push' || | |
| github.event_name == 'workflow_dispatch' || | |
| needs.detect-changes.outputs.mjs-changed == 'true' || | |
| needs.detect-changes.outputs.js-changed == 'true' || | |
| needs.detect-changes.outputs.docs-changed == 'true' || | |
| needs.detect-changes.outputs.package-changed == 'true' || | |
| needs.detect-changes.outputs.workflow-changed == 'true' | |
| ) | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| working-directory: ./js | |
| run: npm install | |
| - name: Run ESLint | |
| working-directory: ./js | |
| run: npm run lint | |
| - name: Check formatting | |
| working-directory: ./js | |
| run: npm run format:check | |
| - name: Check code duplication | |
| working-directory: ./js | |
| run: npm run check:duplication | |
| # Test matrix: Node.js on multiple OS | |
| test: | |
| name: Test (Node.js 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) | |
| 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] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| working-directory: ./js | |
| run: npm install | |
| - name: Run tests | |
| working-directory: ./js | |
| run: npm test | |
| - name: Run example | |
| working-directory: ./js | |
| run: npm run example | |
| # 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 | |
| # This is needed because lint/test jobs have a transitive dependency on changeset-check | |
| 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 required for npm OIDC trusted publishing | |
| 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: '22' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| working-directory: ./js | |
| run: npm install | |
| - name: Update npm for OIDC trusted publishing | |
| working-directory: ./js | |
| run: node scripts/setup-npm.mjs | |
| - name: Check for changesets | |
| id: check_changesets | |
| working-directory: ./js | |
| run: node scripts/check-changesets.mjs | |
| - name: Merge multiple changesets | |
| if: steps.check_changesets.outputs.has_changesets == 'true' && steps.check_changesets.outputs.changeset_count > 1 | |
| working-directory: ./js | |
| run: | | |
| echo "Multiple changesets detected, merging..." | |
| node scripts/merge-changesets.mjs | |
| - name: Version packages and commit to main | |
| if: steps.check_changesets.outputs.has_changesets == 'true' | |
| id: version | |
| working-directory: ./js | |
| run: node scripts/version-and-commit.mjs --mode changeset | |
| - name: Publish to npm | |
| # Run if version was committed OR if a previous attempt already committed (for re-runs) | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| id: publish | |
| working-directory: ./js | |
| run: node scripts/publish-to-npm.mjs --should-pull | |
| - name: Create GitHub Release | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| working-directory: ./js | |
| run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" | |
| - name: Format GitHub release notes | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| working-directory: ./js | |
| run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" | |
| # Manual Instant Release - triggered via workflow_dispatch with instant mode | |
| instant-release: | |
| name: Instant Release | |
| needs: [lint, test] | |
| # Note: always() is required to evaluate the condition when dependencies use always() | |
| if: | | |
| always() && !cancelled() && | |
| github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.release_mode == 'instant' && | |
| needs.lint.result == 'success' && | |
| needs.test.result == 'success' | |
| runs-on: ubuntu-latest | |
| # Permissions required for npm OIDC trusted publishing | |
| 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: '22' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| working-directory: ./js | |
| run: npm install | |
| - name: Update npm for OIDC trusted publishing | |
| working-directory: ./js | |
| run: node scripts/setup-npm.mjs | |
| - name: Version packages and commit to main | |
| id: version | |
| working-directory: ./js | |
| run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" | |
| - name: Publish to npm | |
| # Run if version was committed OR if a previous attempt already committed (for re-runs) | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| id: publish | |
| working-directory: ./js | |
| run: node scripts/publish-to-npm.mjs | |
| - name: Create GitHub Release | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| working-directory: ./js | |
| run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" | |
| - name: Format GitHub release notes | |
| if: steps.publish.outputs.published == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| working-directory: ./js | |
| run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" | |
| # Manual Changeset PR - creates a pull request with the changeset for review | |
| 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: '22' | |
| - name: Install dependencies | |
| working-directory: ./js | |
| run: npm install | |
| - name: Create changeset file | |
| working-directory: ./js | |
| run: node scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" | |
| - name: Format changeset with Prettier | |
| working-directory: ./js | |
| run: | | |
| # Run Prettier on the changeset file to ensure it matches project style | |
| npx prettier --write ".changeset/*.md" || true | |
| echo "Formatted changeset files" | |
| - name: Create Pull Request | |
| uses: peter-evans/create-pull-request@v7 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| commit-message: 'chore: add changeset for manual ${{ github.event.inputs.bump_type }} release' | |
| branch: changeset-manual-release-${{ github.run_id }} | |
| delete-branch: true | |
| title: 'chore: manual ${{ github.event.inputs.bump_type }} release (JS)' | |
| body: | | |
| ## Manual Release Request (JavaScript) | |
| This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release for the JavaScript package. | |
| ### Release Details | |
| - **Type:** ${{ github.event.inputs.bump_type }} | |
| - **Description:** ${{ github.event.inputs.description || 'Manual release' }} | |
| - **Triggered by:** @${{ github.actor }} | |
| ### Next Steps | |
| 1. Review the changeset in this PR | |
| 2. Merge this PR to main | |
| 3. The automated release workflow will create a version PR | |
| 4. Merge the version PR to publish to npm and create a GitHub release |