Initial commit #1
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: Checks and release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| # Manual release support - consolidated here to work with npm trusted publishing | |
| # npm only allows ONE workflow file as trusted publisher, so all publishing | |
| # must go through this workflow (release.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: ${{ github.workflow }}-${{ github.ref }} | |
| 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 | |
| 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 | |
| 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: '20.x' | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Check for changesets | |
| env: | |
| # Pass PR context to the validation script | |
| 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 | |
| # This validates that exactly ONE changeset was ADDED by this PR | |
| # Pre-existing changesets from other merged PRs are ignored | |
| node scripts/validate-changeset.mjs | |
| # === LINT AND FORMAT CHECK === | |
| # Lint runs independently of changeset-check - it's a fast check that should always run | |
| # See: https://github.com/link-assistant/hive-mind/pull/1024 for why this dependency was removed | |
| # IMPORTANT: ESLint includes max-lines rule (1500 lines) to ensure files stay maintainable | |
| # See docs/case-studies/issue-23 for why fresh merge simulation is critical | |
| lint: | |
| name: Lint and Format Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: | | |
| github.event_name == 'push' || | |
| 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 | |
| with: | |
| # For PRs, fetch enough history to merge with base branch | |
| fetch-depth: 0 | |
| - name: Simulate fresh merge with base branch (PR only) | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| echo "=== Synchronizing PR with latest $BASE_REF ===" | |
| echo "This prevents stale merge preview issues (see docs/case-studies/issue-23)" | |
| echo "" | |
| # Configure git for merge | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git config user.name "github-actions[bot]" | |
| # Fetch the latest base branch | |
| echo "Fetching latest $BASE_REF..." | |
| git fetch origin "$BASE_REF" | |
| # Get current and base branch info | |
| CURRENT_SHA=$(git rev-parse HEAD) | |
| BASE_SHA=$(git rev-parse "origin/$BASE_REF") | |
| echo "Current checkout (merge preview): $CURRENT_SHA" | |
| echo "Latest base branch ($BASE_REF): $BASE_SHA" | |
| echo "" | |
| # Check if base branch has new commits not in the merge preview | |
| BEHIND_COUNT=$(git rev-list --count HEAD..origin/$BASE_REF) | |
| if [ "$BEHIND_COUNT" -eq 0 ]; then | |
| echo "Merge preview is up-to-date with $BASE_REF. No simulation needed." | |
| else | |
| echo "Base branch has $BEHIND_COUNT new commit(s) since PR was opened/synced." | |
| echo "Simulating fresh merge to validate actual merge result..." | |
| echo "" | |
| # Attempt to merge the latest base branch | |
| if git merge origin/$BASE_REF --no-edit; then | |
| echo "" | |
| echo "Fresh merge simulation successful!" | |
| echo "Checks will now run against the up-to-date merged state." | |
| else | |
| echo "" | |
| echo "::error::Merge conflict detected! PR needs to be rebased/updated before it can be merged." | |
| echo "The PR branch is out of sync with $BASE_REF and cannot be automatically merged." | |
| exit 1 | |
| fi | |
| fi | |
| echo "" | |
| - 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 code duplication | |
| run: npm run check:duplication | |
| # Test matrix: 3 runtimes (Node.js, Bun, Deno) x 3 OS (Ubuntu, macOS, Windows) | |
| # IMPORTANT: Tests must validate the ACTUAL merge result, not a stale merge preview. | |
| # See docs/case-studies/issue-23 for why this is critical. | |
| 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) | |
| if: always() && (github.event_name == 'push' || 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 | |
| with: | |
| # For PRs, fetch enough history to merge with base branch | |
| fetch-depth: 0 | |
| - name: Simulate fresh merge with base branch (PR only) | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| shell: bash | |
| run: | | |
| echo "=== Synchronizing PR with latest $BASE_REF ===" | |
| echo "This prevents stale merge preview issues (see docs/case-studies/issue-23)" | |
| echo "" | |
| # Configure git for merge | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git config user.name "github-actions[bot]" | |
| # Fetch the latest base branch | |
| echo "Fetching latest $BASE_REF..." | |
| git fetch origin "$BASE_REF" | |
| # Get current and base branch info | |
| CURRENT_SHA=$(git rev-parse HEAD) | |
| BASE_SHA=$(git rev-parse "origin/$BASE_REF") | |
| echo "Current checkout (merge preview): $CURRENT_SHA" | |
| echo "Latest base branch ($BASE_REF): $BASE_SHA" | |
| echo "" | |
| # Check if base branch has new commits not in the merge preview | |
| BEHIND_COUNT=$(git rev-list --count HEAD..origin/$BASE_REF) | |
| if [ "$BEHIND_COUNT" -eq 0 ]; then | |
| echo "Merge preview is up-to-date with $BASE_REF. No simulation needed." | |
| else | |
| echo "Base branch has $BEHIND_COUNT new commit(s) since PR was opened/synced." | |
| echo "Simulating fresh merge to validate actual merge result..." | |
| echo "" | |
| # Attempt to merge the latest base branch | |
| if git merge origin/$BASE_REF --no-edit; then | |
| echo "" | |
| echo "Fresh merge simulation successful!" | |
| echo "Checks will now run against the up-to-date merged state." | |
| else | |
| echo "" | |
| echo "::error::Merge conflict detected! PR needs to be rebased/updated before it can be merged." | |
| echo "The PR branch is out of sync with $BASE_REF and cannot be automatically merged." | |
| exit 1 | |
| fi | |
| fi | |
| echo "" | |
| - 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 Bun | |
| if: matrix.runtime == 'bun' | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies (Bun) | |
| if: matrix.runtime == 'bun' | |
| run: bun install | |
| - name: Run tests (Bun) | |
| if: matrix.runtime == 'bun' | |
| run: bun test | |
| - name: Setup Deno | |
| if: matrix.runtime == 'deno' | |
| uses: denoland/setup-deno@v2 | |
| with: | |
| deno-version: v2.x | |
| - name: Run tests (Deno) | |
| if: matrix.runtime == 'deno' | |
| run: deno test --allow-read | |
| # 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() && 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: '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: Check for changesets | |
| id: check_changesets | |
| 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 | |
| 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 | |
| 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 | |
| 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 }} | |
| 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 }} | |
| 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 | |
| # This job is in release.yml because npm trusted publishing | |
| # only allows one workflow file to be registered as a trusted publisher | |
| instant-release: | |
| name: Instant Release | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant' | |
| 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: '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: 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 }}" | |
| - 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 | |
| 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 }}" | |
| - 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 }}" | |
| # 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: '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: | | |
| # 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' | |
| body: | | |
| ## Manual Release Request | |
| 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 }} | |
| ### 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 |