diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0980742 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# GitHub CODEOWNERS +# This file defines code ownership for automatic review requests + +# Critical workflow files must be reviewed by repository owner +/.github/workflows/release.yml @textlint-rule/admin +/.github/workflows/create-release-pr.yml @textlint-rule/admin + +# CODEOWNERS file itself requires review to prevent bypassing protections +/.github/CODEOWNERS @textlint-rule/admin diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml new file mode 100644 index 0000000..2268e25 --- /dev/null +++ b/.github/workflows/create-release-pr.yml @@ -0,0 +1,126 @@ +name: Create Release PR + +on: + workflow_dispatch: + inputs: + version: + description: 'Version type' + required: true + type: choice + options: + - patch + - minor + - major + +jobs: + create-release-pr: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Install pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + + - name: Setup Node.js + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: 'lts/*' + + # No need to install dependencies - npm version works without them + - name: Version bump + id: version + run: | + npm version "$VERSION_TYPE" --no-git-tag-version + VERSION=$(jq -r '.version' package.json) + echo "version=$VERSION" >> $GITHUB_OUTPUT + env: + VERSION_TYPE: ${{ github.event.inputs.version }} + + - name: Get release notes + id: release-notes + run: | + # Get the default branch + DEFAULT_BRANCH=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.default_branch') + + # Get the latest release tag using GitHub API + # Use the exit code to determine if a release exists + if LAST_TAG=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null); then + echo "Previous release found: $LAST_TAG" + else + LAST_TAG="" + echo "No previous releases found - this will be the first release" + fi + + # Generate release notes - only include previous_tag_name if we have a valid previous tag + echo "Generating release notes for tag: v$VERSION" + if [ -n "$LAST_TAG" ]; then + echo "Using previous tag: $LAST_TAG" + RELEASE_NOTES=$(gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + "/repos/$GITHUB_REPOSITORY/releases/generate-notes" \ + -f "tag_name=v$VERSION" \ + -f "target_commitish=$DEFAULT_BRANCH" \ + -f "previous_tag_name=$LAST_TAG" \ + --jq '.body') + else + echo "Generating notes from all commits" + RELEASE_NOTES=$(gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + "/repos/$GITHUB_REPOSITORY/releases/generate-notes" \ + -f "tag_name=v$VERSION" \ + -f "target_commitish=$DEFAULT_BRANCH" \ + --jq '.body') + fi + + # Set release notes as environment variable + echo "RELEASE_NOTES<> $GITHUB_ENV + echo "$RELEASE_NOTES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + GITHUB_REPOSITORY: ${{ github.repository }} + + - name: Commit version bump + run: | + git add . + git commit -m "chore: release v$VERSION" + env: + VERSION: ${{ steps.version.outputs.version }} + + - name: Push branch + run: | + git push -u "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:release/v$VERSION" + env: + VERSION: ${{ steps.version.outputs.version }} + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + + - name: Create Pull Request + run: | + echo "$RELEASE_NOTES" | gh pr create \ + --title "Release v$VERSION" \ + --body-file - \ + --base "$BASE_BRANCH" \ + --head "release/v$VERSION" \ + --label "Type: Release" \ + --assignee "$ACTOR" \ + --draft + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + BASE_BRANCH: ${{ github.ref_name }} + ACTOR: ${{ github.actor }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..308fe7c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,135 @@ +name: Release + +on: + pull_request: + branches: + - master + - main + types: + - closed + +jobs: + release: + if: | + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'Type: Release') + runs-on: ubuntu-latest + environment: + name: npm + permissions: + contents: write + id-token: write # OIDC + outputs: + released: ${{ steps.tag-check.outputs.exists == 'false' }} + version: ${{ steps.package.outputs.version }} + package-name: ${{ steps.package.outputs.name }} + release-url: ${{ steps.create-release.outputs.url }} + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Get package info + id: package + run: | + VERSION=$(jq -r '.version' package.json) + PACKAGE_NAME=$(jq -r '.name' package.json) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "name=$PACKAGE_NAME" >> $GITHUB_OUTPUT + + - name: Check if tag exists + id: tag-check + run: | + if git rev-parse "v$VERSION" >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + env: + VERSION: ${{ steps.package.outputs.version }} + + - name: Install pnpm + if: steps.tag-check.outputs.exists == 'false' + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + + - name: Setup Node.js + if: steps.tag-check.outputs.exists == 'false' + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: 'lts/*' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + if: steps.tag-check.outputs.exists == 'false' + run: pnpm install --frozen-lockfile + + - name: Build package + if: steps.tag-check.outputs.exists == 'false' + run: pnpm run build + + - name: Publish to npm with provenance + if: steps.tag-check.outputs.exists == 'false' + run: pnpm publish --access public + + - name: Create GitHub Release with tag + id: create-release + if: steps.tag-check.outputs.exists == 'false' + run: | + RELEASE_URL=$(gh release create "v$VERSION" \ + --title "v$VERSION" \ + --target "$SHA" \ + --notes "$PR_BODY") + echo "url=$RELEASE_URL" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.package.outputs.version }} + SHA: ${{ github.sha }} + PR_BODY: ${{ github.event.pull_request.body }} + + comment: + needs: release + if: | + always() && + github.event_name == 'pull_request' && + needs.release.outputs.released == 'true' + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Comment on PR - Success + if: needs.release.result == 'success' + run: | + gh pr comment "$PR_NUMBER" \ + --repo "$REPOSITORY" \ + --body "✅ **Release v$VERSION completed successfully!** + + - 📦 npm package: https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION + - 🏷️ GitHub Release: $RELEASE_URL + - 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID" + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + VERSION: ${{ needs.release.outputs.version }} + PACKAGE_NAME: ${{ needs.release.outputs.package-name }} + RELEASE_URL: ${{ needs.release.outputs.release-url }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + + - name: Comment on PR - Failure + if: needs.release.result == 'failure' + run: | + gh pr comment "$PR_NUMBER" \ + --repo "$REPOSITORY" \ + --body "❌ **Release v$VERSION failed** + + Please check the workflow logs for details. + 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID" + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + VERSION: ${{ needs.release.outputs.version }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + RUN_ID: ${{ github.run_id }}