diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index f4d2206..1e2c37c 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -1,6 +1,12 @@ name: Setup CI description: Setup CI +inputs: + skip-cache: + description: "Whether to skip the cache" + required: false + default: "false" + runs: using: composite steps: @@ -10,7 +16,8 @@ runs: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: ".node-version" - cache: pnpm + package-manager-cache: ${{ inputs.skip-cache != 'true' }} + cache: ${{ inputs.skip-cache != 'true' && 'pnpm' || '' }} - name: Install dependencies shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a3cce5..7495a7f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,5 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4a2c52..f4e0f66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,8 @@ name: CI on: pull_request: - push: - branches: - - main + # merge queue is required so all commits on target branches trigger this workflow + # despite lack of the push event trigger here merge_group: branches: - main @@ -12,7 +11,30 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: true + jobs: + lint-workflows: + name: Lint workflows + runs-on: ubuntu-latest + permissions: + actions: read # only required in private repos + security-events: write # allow writing security events + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + persona: pedantic + annotations: true + advanced-security: false + test: name: Test timeout-minutes: 20 @@ -49,8 +71,8 @@ jobs: name: CI OK runs-on: ubuntu-latest if: always() - needs: [test, typecheck] + needs: [lint-workflows, test, typecheck] steps: - name: Exit with error if some jobs are not successful run: exit 1 - if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }} + if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92b9145..e3384c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,7 +5,10 @@ on: branches: - main -concurrency: ${{ github.workflow }}-${{ github.ref }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + queue: max permissions: {} # each job should define its own permission explicitly @@ -27,6 +30,8 @@ jobs: persist-credentials: false - uses: ./.github/actions/ci-setup + with: + skip-cache: true # avoid cache poisoning attacks - name: Build run: pnpm build @@ -61,6 +66,8 @@ jobs: token: ${{ steps.app-token.outputs.token }} - uses: ./.github/actions/ci-setup + with: + skip-cache: true # avoid cache poisoning attacks - name: Build run: pnpm build diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 94f9e71..c77c6e5 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -4,25 +4,33 @@ on: issue_comment: types: [created] +concurrency: + group: ${{ github.workflow }}-${{ github.run_id }} + cancel-in-progress: true + permissions: {} jobs: release_check: + name: Validate release request if: github.repository == 'changesets/action' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/release-pr') timeout-minutes: 5 runs-on: ubuntu-latest permissions: - contents: read - issues: write - pull-requests: read + contents: read # to inspect the pull request commit state (changesets/action) + issues: write # to add and remove reactions on the triggering comment (changesets/action) + pull-requests: read # to inspect the pull request head state (changesets/action) steps: - id: report_in_progress run: | - echo "in_progress_reaction_id=$(gh api /repos/${{github.repository}}/issues/comments/${{github.event.comment.id}}/reactions -f content='eyes' --jq '.id')" >> "$GITHUB_OUTPUT" + in_progress_reaction_id=$(gh api "/repos/$GH_REPO/issues/comments/$COMMENT_ID/reactions" -f content='eyes' --jq '.id') + echo "in_progress_reaction_id=$in_progress_reaction_id" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} - id: parse_command run: | @@ -38,7 +46,7 @@ jobs: - id: check_authorization run: | - if [[ $AUTHOR_ASSOCIATION == 'MEMBER' || $AUTHOR_ASSOCIATION == 'OWNER' || $AUTHOR_ASSOCIATION == 'COLLABORATOR' ]] + if [[ "$AUTHOR_ASSOCIATION" == 'MEMBER' || "$AUTHOR_ASSOCIATION" == 'OWNER' || "$AUTHOR_ASSOCIATION" == 'COLLABORATOR' ]] then echo "User is authorized to release" else @@ -53,7 +61,7 @@ jobs: requested_sha=$(echo "$REQUESTED_SHA" | tr '[:upper:]' '[:lower:]') mapfile -t matches < <( - gh api /repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/commits --paginate --jq '.[].sha' | + gh api "/repos/$GH_REPO/pulls/$ISSUE_NUMBER/commits" --paginate --jq '.[].sha' | awk -v sha="$requested_sha" 'index(tolower($0), sha) == 1 { print $0 }' ) @@ -70,7 +78,7 @@ jobs: fi resolved_sha="${matches[0]}" - pr_head_sha=$(gh pr view ${{ github.event.issue.number }} --json headRefOid --jq '.headRefOid') + pr_head_sha=$(gh pr view "$ISSUE_NUMBER" --json headRefOid --jq '.headRefOid') if [[ "$resolved_sha" != "$pr_head_sha" ]] then @@ -82,13 +90,17 @@ jobs: echo "pr_head_sha=$pr_head_sha" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} REQUESTED_SHA: ${{ steps.parse_command.outputs.requested_sha }} - id: get_pr_head_repository run: | - echo "head_repository=$(gh pr view ${{ github.event.issue.number }} --json headRepositoryOwner,headRepository --jq '.headRepositoryOwner.login + "/" + .headRepository.name')" >> "$GITHUB_OUTPUT" + head_repository=$(gh pr view "$ISSUE_NUMBER" --json headRepositoryOwner,headRepository --jq '.headRepositoryOwner.login + "/" + .headRepository.name') + echo "head_repository=$head_repository" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} outputs: in_progress_reaction_id: ${{ steps.report_in_progress.outputs.in_progress_reaction_id }} @@ -98,15 +110,16 @@ jobs: head_repository: ${{ steps.get_pr_head_repository.outputs.head_repository }} release: + name: Release snapshot if: github.repository == 'changesets/action' timeout-minutes: 20 runs-on: ubuntu-latest needs: release_check permissions: - contents: write - issues: write - pull-requests: write + contents: write # to create the release commit and publish snapshot artifacts (changesets/action) + issues: write # to add and remove reactions on the triggering comment (changesets/action) + pull-requests: write # to comment on the pull request with the release result (changesets/action) steps: - name: Check out repo @@ -115,35 +128,40 @@ jobs: persist-credentials: false - uses: ./.github/actions/ci-setup + with: + skip-cache: true # avoid cache poisoning attacks - name: Fetch validated commit from pull request head repository run: | - git remote add pr-head https://github.com/$HEAD_REPOSITORY.git - git fetch --no-tags pr-head $RESOLVED_SHA + git remote add pr-head "https://github.com/$HEAD_REPOSITORY.git" + git fetch --no-tags pr-head "$RESOLVED_SHA" env: HEAD_REPOSITORY: ${{ needs.release_check.outputs.head_repository }} RESOLVED_SHA: ${{ needs.release_check.outputs.resolved_sha }} - name: Checkout validated commit - run: git checkout --detach $RESOLVED_SHA + run: git checkout --detach "$RESOLVED_SHA" env: RESOLVED_SHA: ${{ needs.release_check.outputs.resolved_sha }} - name: Check if Version Packages PR id: check_version_packages run: | - echo "version_packages=$(gh pr view ${{ github.event.issue.number }} --json headRefName --jq '.headRefName|startswith("changeset-release/")')" >> "$GITHUB_OUTPUT" + version_packages=$(gh pr view "$ISSUE_NUMBER" --json headRefName --jq '.headRefName|startswith("changeset-release/")') + echo "version_packages=$version_packages" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} - name: Reset Version Packages PR if: steps.check_version_packages.outputs.version_packages == 'true' run: git reset --hard HEAD~1 - name: Create snapshot version - run: pnpm changeset version --snapshot pr${{ github.event.issue.number }} + run: pnpm changeset version --snapshot "pr$ISSUE_NUMBER" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} - name: Build run: pnpm build @@ -157,45 +175,68 @@ jobs: run: pnpm release:pr - name: Add success reaction - run: gh api /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions -f content='rocket' + run: gh api "/repos/$GH_REPO/issues/comments/$COMMENT_ID/reactions" -f content='rocket' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} - name: Remove in-progress reaction - run: gh api -X DELETE /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions/${{ needs.release_check.outputs.in_progress_reaction_id }} + run: gh api -X DELETE "/repos/$GH_REPO/issues/comments/$COMMENT_ID/reactions/$IN_PROGRESS_REACTION_ID" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} + IN_PROGRESS_REACTION_ID: ${{ needs.release_check.outputs.in_progress_reaction_id }} - name: Comment success run: | - gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ needs.release_check.outputs.resolved_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [succeeded](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). Published commit: [${{ github.repository }}@$(git rev-parse HEAD)](https://github.com/${{ github.repository }}/commit/$(git rev-parse HEAD))." + published_commit="$(git rev-parse HEAD)" + body="The release for \`$RESOLVED_SHA\` triggered by [this comment]($COMMENT_URL) has [succeeded]($RUN_URL). Published commit: [$GH_REPO@$published_commit](https://github.com/$GH_REPO/commit/$published_commit)." + gh pr comment "$ISSUE_NUMBER" --body "$body" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENT_URL: ${{ github.event.comment.url }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + RESOLVED_SHA: ${{ needs.release_check.outputs.resolved_sha }} report-failure-if-needed: + name: Report failure needs: [release_check, release] timeout-minutes: 2 runs-on: ubuntu-latest if: failure() && github.repository == 'changesets/action' && (needs.release_check.result == 'failure' || needs.release.result == 'failure') permissions: - issues: write - pull-requests: write + issues: write # to add and remove reactions on the triggering comment (changesets/action) + pull-requests: write # to comment on the pull request with the failure result (changesets/action) steps: - name: Add failure reaction - run: gh api /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions -f content='-1' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Remove in-progress reaction - run: gh api -X DELETE /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions/${{ needs.release_check.outputs.in_progress_reaction_id }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Comment failure - run: | - gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ needs.release_check.outputs.requested_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [failed](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})." + run: gh api "/repos/$GH_REPO/issues/comments/$COMMENT_ID/reactions" -f content='-1' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} + + - name: Remove in-progress reaction + run: gh api -X DELETE "/repos/$GH_REPO/issues/comments/$COMMENT_ID/reactions/$IN_PROGRESS_REACTION_ID" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} + IN_PROGRESS_REACTION_ID: ${{ needs.release_check.outputs.in_progress_reaction_id }} + + - name: Comment failure + run: | + body="The release for \`$REQUESTED_SHA\` triggered by [this comment]($COMMENT_URL) has [failed]($RUN_URL)." + gh pr comment "$ISSUE_NUMBER" --body "$body" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENT_URL: ${{ github.event.comment.url }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REQUESTED_SHA: ${{ needs.release_check.outputs.requested_sha }}