diff --git a/.github/workflows/readme-auto-merge.yml b/.github/workflows/readme-auto-merge.yml new file mode 100644 index 0000000..d20e670 --- /dev/null +++ b/.github/workflows/readme-auto-merge.yml @@ -0,0 +1,57 @@ +name: README auto-merge + +# Approves and merges the daily PR opened by update-readme.yml's App token. +# Closely modelled on robotiq.github.io's dependabot-auto-merge.yml. +# +# Why this isn't self-approval: prToMain's required-review rule blocks a +# PR's *author* from approving it, not a different actor. The App +# (robotiq-readme-bot[bot]) opens the PR; this workflow's own GITHUB_TOKEN +# (github-actions[bot]) submits the approval. Two distinct actors, so +# GitHub accepts it — the same arrangement that merges Dependabot's PRs on +# robotiq.github.io today. +# +# Why this is safe to merge without further human review: prToMain also +# requires the `test` status check (test.yml) to pass, and `gh pr merge +# --auto` queues the merge without completing it until that check reports +# success. A change that breaks scripts/update-readme.mjs therefore sits as +# an open, failing PR instead of reaching the org's public landing page. +# Nothing here bypasses a rule; it satisfies them. +# +# Why plain `pull_request` and not `pull_request_target`: the dependabot +# equivalent needs pull_request_target only because GitHub hands a +# read-only, secret-less token to `pull_request` runs on Dependabot PRs. +# That restriction doesn't apply here — this PR comes from a branch in this +# repo — so the safer trigger works. Note this job deliberately has NO +# checkout step; don't add one, since approving and merging code that the +# job has also checked out with a write-capable token is the footgun both +# triggers are guarding against. + +on: + pull_request: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + automerge: + # Both conditions matter: the actor check stops this from approving + # anyone else's PR, and the branch check keeps it to the generated one + # even if the App is ever used for something else. + if: >- + github.actor == 'robotiq-readme-bot[bot]' && + github.event.pull_request.head.ref == 'auto/update-readme' + runs-on: ubuntu-latest + steps: + - name: Approve + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: gh pr review --approve "$PR_URL" + + - name: Enable auto-merge + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: gh pr merge --auto --squash --delete-branch "$PR_URL" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b761b56..28f20ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,14 @@ name: Test -# Runs scripts/update-readme.test.mjs on every PR into main. Intended to be -# added as a required status check on the prToMain ruleset, so a PR that -# breaks scripts/update-readme.mjs (including one opened by -# update-readme.yml's own daily run) can't merge — see the "required check" -# discussion on PR #2. +# Runs scripts/update-readme.test.mjs on every PR into main, as the `test` +# required status check on the prToMain ruleset. +# +# This gates human PRs and the daily automated one alike. The latter only +# works because update-readme.yml opens its PR with a GitHub App token: +# GitHub doesn't trigger workflow runs from events created by GITHUB_TOKEN, +# so a PR opened with that token would never start this workflow and its +# required check would never report — see the note at the top of +# update-readme.yml. on: pull_request: diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index f24bb99..1262317 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -4,52 +4,57 @@ name: Update README # scripts/update-readme.mjs): the repository table (from the GitHub API) and # the software tools tables (imported from robotiq/robotiq.github.io's # docs/intro.mdx). Opens a PR only when the regenerated content actually -# differs, then auto-merges it — mirroring -# robotiq.github.io's .github/workflows/dependabot-auto-merge.yml. +# differs; readme-auto-merge.yml then approves and merges it. Fully +# automatic, no human review, no PAT, no bot account, and no ruleset bypass +# — every prToMain rule stays enforced against every actor. # -# Why this needs a PAT instead of just GITHUB_TOKEN: the prToMain ruleset's -# required-review rule blocks a PR's *author* from approving its own PR. In -# dependabot-auto-merge.yml, Dependabot (a distinct actor) opens the PR and -# GITHUB_TOKEN (github-actions[bot]) approves it — two different actors, so -# it's not self-approval. Here there's no Dependabot; this workflow itself -# generates the content. So the "open PR" step authenticates as -# README_BOT_PAT (a fine-grained PAT scoped to just this repo, Contents + -# Pull requests: write) instead of GITHUB_TOKEN, making its author a -# distinct actor from the github-actions[bot] identity that then approves -# and merges it in the next step. +# Why the PR is opened with a GitHub App token and not GITHUB_TOKEN: # -# Why this is safe to merge without further human review: the generated -# content is either the GitHub API's own repo descriptions or -# robotiq.github.io's own published docs/intro.mdx — both already public and -# already reviewed upstream. What actually gates the merge is the "Test" -# workflow (.github/workflows/test.yml) as a required status check on the -# prToMain ruleset: `gh pr merge --auto` waits on it, so a change that breaks -# update-readme.mjs sits as an open, failing PR instead of reaching the org's -# public landing page. Without that required check, `--auto` has nothing to -# wait on and errors out instead of merging — see the review on PR #2. +# 1. GitHub does not trigger workflow runs from events created by +# GITHUB_TOKEN. A PR opened with it would never start test.yml, so the +# required `test` check would sit "expected — waiting for status" +# forever and the PR would never become mergeable. +# 2. prToMain requires one approving review, and GitHub blocks a PR's own +# author from approving it. The approval in readme-auto-merge.yml comes +# from GITHUB_TOKEN (github-actions[bot]); that only works if some +# *other* actor opened the PR. # -# Also requires: `allow_auto_merge` enabled on this repo (Settings → General -# → Pull Requests), and the commit's author email resolving to a real GitHub -# account (below) so prToMain's require_extra_approval_for_unattributed_changes -# rule doesn't kick in and demand a second approval nothing here can produce. +# The `robotiq-readme-bot` App satisfies both: it's a distinct actor from +# github-actions[bot], and it isn't tied to anyone's personal account, so +# there's no rotation/offboarding problem. This is the same two-actor shape +# as robotiq.github.io's dependabot-auto-merge.yml, with the App standing in +# for Dependabot. on: schedule: - cron: '17 6 * * *' workflow_dispatch: {} +# The App token does all the writing; this job's own GITHUB_TOKEN only needs +# to read the checkout. permissions: - contents: write - pull-requests: write + contents: read + +concurrency: + group: update-readme + cancel-in-progress: false jobs: update-readme: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Mint App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: actions/checkout@v4 with: - token: ${{ secrets.README_BOT_PAT }} + # Persist the App token as the credential git push will use, so + # the push (and the PR it produces) is attributed to the App. + token: ${{ steps.app-token.outputs.token }} - uses: actions/setup-node@v4 with: @@ -57,55 +62,45 @@ jobs: - name: Regenerate README env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: node scripts/update-readme.mjs - name: Open PR if content changed - id: pr env: - GH_TOKEN: ${{ secrets.README_BOT_PAT }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | if git diff --quiet -- profile/README.md; then - echo "changed=false" >> "$GITHUB_OUTPUT" exit 0 fi # Fixed name rather than one timestamped per run: a run that lands - # before yesterday's PR merged (still waiting on the required - # check) force-pushes onto the same branch/PR instead of piling up - # a new branch and a new unmergeable PR each day. + # before yesterday's PR merged force-pushes onto the same + # branch/PR instead of piling up a new branch and a new PR each + # day. BRANCH="auto/update-readme" - git config user.name "robotiq-readme-bot" - # This must resolve to a real GitHub account (github-actions[bot]'s - # own noreply address) or the prToMain ruleset's - # require_extra_approval_for_unattributed_changes rule treats the - # commit as unattributed and demands a second approval this - # workflow has no way to produce. - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # The App's bot identity, so the commit is attributed to a real + # GitHub account — prToMain sets + # require_extra_approval_for_unattributed_changes, which would + # demand a second approval for commits it can't attribute. + git config user.name "robotiq-readme-bot[bot]" + git config user.email "332551394+robotiq-readme-bot[bot]@users.noreply.github.com" git checkout -b "$BRANCH" git add profile/README.md git commit -m "chore: update README repository table and software tools section" git push --force origin "$BRANCH" - EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json url --jq '.[0].url') + # `// empty` because --jq '.[0].url' prints the literal string + # "null" on an empty list, which [ -n ] would treat as a hit. + EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json url --jq '.[0].url // empty') if [ -n "$EXISTING_PR" ]; then - PR_URL="$EXISTING_PR" + # Already open: the force-push above fires a `synchronize` event, + # which re-runs readme-auto-merge.yml against it. + echo "Updated existing PR: $EXISTING_PR" else - PR_URL=$(gh pr create \ + gh pr create \ --base main \ --head "$BRANCH" \ --title "chore: update README repository table" \ - --body "Automated daily refresh of the repository table (GitHub API) and software tools section (robotiq.github.io docs/intro.mdx) — see \`scripts/update-readme.mjs\`.") + --body "Automated daily refresh of the repository table (GitHub API) and software tools section (robotiq.github.io docs/intro.mdx) — see \`scripts/update-readme.mjs\`." fi - - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "url=$PR_URL" >> "$GITHUB_OUTPUT" - - - name: Approve and auto-merge - if: steps.pr.outputs.changed == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_URL: ${{ steps.pr.outputs.url }} - run: | - gh pr review --approve "$PR_URL" - gh pr merge --auto --squash --delete-branch "$PR_URL"