From 9772be7aec0e93ee6caf13b009d617605b86b32e Mon Sep 17 00:00:00 2001 From: bcastets-robotiq Date: Mon, 21 Sep 2026 17:23:51 -0400 Subject: [PATCH 1/3] feat: fully automatic daily README update via ruleset bypass, no PAT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the PAT-based approve/merge with a direct merge, relying on a bypass actor for the GitHub Actions app on prToMain (exempting it from the 1-review requirement) plus a separate requireTests ruleset with no bypass actors, so the test check still gates the merge. Requires two settings changes before this actually merges on its own (see PR description) — until then it'll open PRs that wait for manual merge, same as before. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/update-readme.yml | 75 +++++++++-------------------- 1 file changed, 23 insertions(+), 52 deletions(-) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index f24bb99..587c67d 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -4,34 +4,25 @@ 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, then merges it — fully automatic, no human review, no bot PAT. # -# 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. +# How this merges without a second identity: `prToMain` has a bypass actor +# for the "GitHub Actions" app (mode: pull request), exempting +# github-actions[bot] — the identity GITHUB_TOKEN always acts as — from the +# 1-review requirement. A bot still can't literally approve its own PR (a +# GitHub-wide rule bypass doesn't touch), so this workflow doesn't try to; +# it just merges directly, since the rule requiring an approval doesn't +# apply to this actor in the first place. # -# 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. -# -# 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. +# What still gates the merge: a *separate* ruleset, `requireTests`, targets +# the same branch with only the "test" required status check and NO bypass +# actors — it applies to every actor, bypass or not. Bypassing prToMain's +# review rule doesn't touch it, because ruleset bypass is scoped to the +# whole ruleset it's granted on, not to individual rules within it (that's +# exactly why the check was moved into its own ruleset instead of living +# alongside the bypassed review rule in prToMain — see the discussion on +# PR #2). So a change that breaks update-readme.mjs still sits as an open, +# failing PR instead of reaching the org's public landing page. on: schedule: @@ -46,10 +37,7 @@ jobs: update-readme: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.README_BOT_PAT }} + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: @@ -60,27 +48,20 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node scripts/update-readme.mjs - - name: Open PR if content changed - id: pr + - name: Open and merge PR if content changed env: - GH_TOKEN: ${{ secrets.README_BOT_PAT }} + GH_TOKEN: ${{ secrets.GITHUB_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. + # check) force-pushes onto the same branch/PR instead of piling + # up a new branch and a new unmergeable 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.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout -b "$BRANCH" git add profile/README.md @@ -98,14 +79,4 @@ jobs: --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" From d040ae2f47168aa063f327d3184fbf865e6ffd30 Mon Sep 17 00:00:00 2001 From: Marc-Andre Begin Date: Tue, 22 Sep 2026 10:50:01 -0400 Subject: [PATCH 2/3] fix: run tests in-job instead of as a required check on the bot's PR test.yml can't gate the daily PR: GitHub doesn't trigger workflow runs from events created by GITHUB_TOKEN, so the required check would never report and --auto would never fire. Run the same tests as a step before the PR is opened, and drop the planned requireTests ruleset. Also fixes EXISTING_PR picking up the literal string "null", which would have skipped gh pr create on the first run. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test.yml | 12 +++++--- .github/workflows/update-readme.yml | 48 +++++++++++++++++------------ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b761b56..bf566a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,12 @@ 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 PRs opened by humans. It cannot gate the daily PR opened by +# update-readme.yml: GitHub doesn't trigger workflow runs from events +# created by GITHUB_TOKEN, so this workflow never starts for that PR. That +# run executes these same tests as a step in its own job instead. on: pull_request: diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 587c67d..9fa1efc 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -8,21 +8,21 @@ name: Update README # # How this merges without a second identity: `prToMain` has a bypass actor # for the "GitHub Actions" app (mode: pull request), exempting -# github-actions[bot] — the identity GITHUB_TOKEN always acts as — from the -# 1-review requirement. A bot still can't literally approve its own PR (a -# GitHub-wide rule bypass doesn't touch), so this workflow doesn't try to; -# it just merges directly, since the rule requiring an approval doesn't -# apply to this actor in the first place. +# github-actions[bot] — the identity GITHUB_TOKEN always acts as — from +# every rule in that ruleset, including the 1-review requirement and the +# `test` required check. A bot still can't literally approve its own PR (a +# GitHub-wide rule a bypass doesn't touch), so this workflow doesn't try +# to; it merges directly, since the rules that would block it don't apply +# to this actor. # -# What still gates the merge: a *separate* ruleset, `requireTests`, targets -# the same branch with only the "test" required status check and NO bypass -# actors — it applies to every actor, bypass or not. Bypassing prToMain's -# review rule doesn't touch it, because ruleset bypass is scoped to the -# whole ruleset it's granted on, not to individual rules within it (that's -# exactly why the check was moved into its own ruleset instead of living -# alongside the bypassed review rule in prToMain — see the discussion on -# PR #2). So a change that breaks update-readme.mjs still sits as an open, -# failing PR instead of reaching the org's public landing page. +# Why the `test` check can't be what gates this PR: GitHub does not trigger +# workflow runs from events created by GITHUB_TOKEN, so test.yml +# (on: pull_request) never starts for a PR this workflow opens — its check +# would sit "expected — waiting for status" forever and the merge would +# never happen. The test runs as a step in this job instead, before the PR +# is opened, so a change that breaks update-readme.mjs fails the run here +# and never reaches profile/README.md. test.yml still gates PRs opened by +# humans, where it does run. on: schedule: @@ -43,6 +43,11 @@ jobs: with: node-version: 20 + - name: Test update-readme.mjs + # This job's own gate — test.yml can't serve as one for the PR this + # job opens; see the note at the top of this file. + run: node --test scripts/*.test.mjs + - name: Regenerate README env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -57,9 +62,9 @@ jobs: 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 "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" @@ -68,7 +73,9 @@ jobs: 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" else @@ -79,4 +86,7 @@ jobs: --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 - gh pr merge --auto --squash --delete-branch "$PR_URL" + # Not --auto: with the prToMain bypass there's nothing left to + # wait on, and a direct merge fails loudly in this run instead of + # quietly queueing a PR that never lands. + gh pr merge --squash --delete-branch "$PR_URL" From 82de88a750d3402401da97f11828a797badbe979 Mon Sep 17 00:00:00 2001 From: Marc-Andre Begin Date: Tue, 22 Sep 2026 11:34:56 -0400 Subject: [PATCH 3/3] feat: open the daily PR with a GitHub App so the checks and review rules are satisfied MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the ruleset-bypass approach, which can't work: "GitHub Actions" isn't available as a bypass actor on this org, and a PR opened with GITHUB_TOKEN never triggers test.yml, so its required check would never report. Instead the robotiq-readme-bot App opens the PR (a distinct actor, tied to no personal account) and readme-auto-merge.yml approves it with GITHUB_TOKEN and enables auto-merge — the same two-actor arrangement that merges Dependabot's PRs on robotiq.github.io. prToMain needs no changes at all; every rule stays enforced against every actor. Also adds a concurrency group and commits under the App's bot identity so changes are attributed. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/readme-auto-merge.yml | 57 ++++++++++++++++ .github/workflows/test.yml | 10 +-- .github/workflows/update-readme.yml | 88 ++++++++++++++----------- 3 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/readme-auto-merge.yml 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 bf566a3..28f20ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,10 +3,12 @@ name: Test # Runs scripts/update-readme.test.mjs on every PR into main, as the `test` # required status check on the prToMain ruleset. # -# This gates PRs opened by humans. It cannot gate the daily PR opened by -# update-readme.yml: GitHub doesn't trigger workflow runs from events -# created by GITHUB_TOKEN, so this workflow never starts for that PR. That -# run executes these same tests as a step in its own job instead. +# 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 9fa1efc..1262317 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -4,58 +4,70 @@ 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 merges it — fully automatic, no human review, no bot PAT. +# 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. # -# How this merges without a second identity: `prToMain` has a bypass actor -# for the "GitHub Actions" app (mode: pull request), exempting -# github-actions[bot] — the identity GITHUB_TOKEN always acts as — from -# every rule in that ruleset, including the 1-review requirement and the -# `test` required check. A bot still can't literally approve its own PR (a -# GitHub-wide rule a bypass doesn't touch), so this workflow doesn't try -# to; it merges directly, since the rules that would block it don't apply -# to this actor. +# Why the PR is opened with a GitHub App token and not GITHUB_TOKEN: # -# Why the `test` check can't be what gates this PR: GitHub does not trigger -# workflow runs from events created by GITHUB_TOKEN, so test.yml -# (on: pull_request) never starts for a PR this workflow opens — its check -# would sit "expected — waiting for status" forever and the merge would -# never happen. The test runs as a step in this job instead, before the PR -# is opened, so a change that breaks update-readme.mjs fails the run here -# and never reaches profile/README.md. test.yml still gates PRs opened by -# humans, where it does run. +# 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. +# +# 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: 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: + # 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: node-version: 20 - - name: Test update-readme.mjs - # This job's own gate — test.yml can't serve as one for the PR this - # job opens; see the note at the top of this file. - run: node --test scripts/*.test.mjs - - name: Regenerate README env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: node scripts/update-readme.mjs - - name: Open and merge PR if content changed + - name: Open PR if content changed env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | if git diff --quiet -- profile/README.md; then exit 0 @@ -66,8 +78,13 @@ jobs: # branch/PR instead of piling up a new branch and a new PR each # day. BRANCH="auto/update-readme" - git config user.name "github-actions[bot]" - 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" @@ -77,16 +94,13 @@ jobs: # "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 - - # Not --auto: with the prToMain bypass there's nothing left to - # wait on, and a direct merge fails loudly in this run instead of - # quietly queueing a PR that never lands. - gh pr merge --squash --delete-branch "$PR_URL"