From 1e7c230296a841b4616d0eaee256f7434111abdd Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 15 Sep 2026 07:33:08 +0000 Subject: [PATCH 1/2] Use a GitHub App for Dependabot merges --- .github/workflows/checks.yml | 46 ++++++++++++++++++++++++++++++++---- README.md | 22 +++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d01021b9..1931bd4d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -22,7 +22,6 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' permissions: - contents: write pull-requests: write steps: - name: Fetch dependabot metadata @@ -30,11 +29,48 @@ jobs: uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Approve and enable auto-merge - if: steps.metadata.outputs.update-type != 'version-update:semver-major' + - name: Check auto-merge policy + id: policy + env: + DEPENDENCIES: ${{ steps.metadata.outputs.updated-dependencies-json }} run: | - gh pr review --approve "$PR_URL" - gh pr merge --auto --squash "$PR_URL" + if jq --exit-status --slurp ' + def release: + type == "string" and test("^v?(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"); + length == 1 and (.[0] | + type == "array" and length > 0 and all(.[]; + (.prevVersion | release) and + (.newVersion | release) and + (.updateType == "version-update:semver-patch" or + (.updateType == "version-update:semver-minor" and + (.prevVersion | test("^v?[1-9][0-9]*\\.")))) + ) + ) + ' <<< "$DEPENDENCIES"; then + echo "eligible=true" >> "$GITHUB_OUTPUT" + else + echo "eligible=false" >> "$GITHUB_OUTPUT" + fi + - name: Approve pull request + if: steps.policy.outputs.eligible == 'true' + run: gh pr review --approve "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create merge token + if: steps.policy.outputs.eligible == 'true' + id: merge-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.ROBOSTACK_BOT_CLIENT_ID }} + private-key: ${{ secrets.ROBOSTACK_BOT_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + permission-workflows: write + - name: Enable auto-merge + if: steps.policy.outputs.eligible == 'true' + run: gh pr merge --auto --squash --match-head-commit "$PR_HEAD_SHA" "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + GH_TOKEN: ${{ steps.merge-token.outputs.token }} diff --git a/README.md b/README.md index 505eb016..58f95121 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,25 @@ pixi run serve ``` Open server on http://localhost:4321/ + +# Dependabot merges + +Dependabot patch updates are approved after checks pass. Minor updates are also +approved for dependencies already on `1.0.0` or later, but `0.x` minor updates and +all major updates need manual review. Every dependency in a PR must qualify; +missing version metadata, non-release versions, and unknown update types need +manual review. + +Eligible updates are merged with a GitHub App token so the merge triggers checks +and deployment on `main`. + +Install the App on this repository with these repository permissions: + +- Contents: read and write +- Pull requests: read and write +- Workflows: read and write, so dependency updates to workflow files can merge + +Set the repository Actions variable `ROBOSTACK_BOT_CLIENT_ID` to the App's +client ID. Store its private key as `ROBOSTACK_BOT_PRIVATE_KEY` in both +Actions secrets and Dependabot secrets. Dependabot-triggered workflows only +have access to Dependabot secrets. From 29fb616607ab38205194f3326c81637d48fd805f Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 15 Sep 2026 09:09:08 +0000 Subject: [PATCH 2/2] Move Dependabot merge policy into a Pixi task --- .github/workflows/checks.yml | 28 ++++++---------- README.md | 22 ------------- pixi.toml | 4 +++ scripts/check_dependabot_merge.py | 54 +++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 40 deletions(-) create mode 100644 scripts/check_dependabot_merge.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 1931bd4d..707cd0cc 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -22,8 +22,17 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' permissions: + contents: read pull-requests: write steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false + - uses: prefix-dev/setup-pixi@d3f436a425481402e6a95a1d1fc10331c708cd9e # v0.10.2 + with: + # Do not restore executable environments populated by PR checks. + cache: false - name: Fetch dependabot metadata id: metadata uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 @@ -33,24 +42,7 @@ jobs: id: policy env: DEPENDENCIES: ${{ steps.metadata.outputs.updated-dependencies-json }} - run: | - if jq --exit-status --slurp ' - def release: - type == "string" and test("^v?(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"); - length == 1 and (.[0] | - type == "array" and length > 0 and all(.[]; - (.prevVersion | release) and - (.newVersion | release) and - (.updateType == "version-update:semver-patch" or - (.updateType == "version-update:semver-minor" and - (.prevVersion | test("^v?[1-9][0-9]*\\.")))) - ) - ) - ' <<< "$DEPENDENCIES"; then - echo "eligible=true" >> "$GITHUB_OUTPUT" - else - echo "eligible=false" >> "$GITHUB_OUTPUT" - fi + run: pixi run check-dependabot-merge - name: Approve pull request if: steps.policy.outputs.eligible == 'true' run: gh pr review --approve "$PR_URL" diff --git a/README.md b/README.md index 58f95121..505eb016 100644 --- a/README.md +++ b/README.md @@ -26,25 +26,3 @@ pixi run serve ``` Open server on http://localhost:4321/ - -# Dependabot merges - -Dependabot patch updates are approved after checks pass. Minor updates are also -approved for dependencies already on `1.0.0` or later, but `0.x` minor updates and -all major updates need manual review. Every dependency in a PR must qualify; -missing version metadata, non-release versions, and unknown update types need -manual review. - -Eligible updates are merged with a GitHub App token so the merge triggers checks -and deployment on `main`. - -Install the App on this repository with these repository permissions: - -- Contents: read and write -- Pull requests: read and write -- Workflows: read and write, so dependency updates to workflow files can merge - -Set the repository Actions variable `ROBOSTACK_BOT_CLIENT_ID` to the App's -client ID. Store its private key as `ROBOSTACK_BOT_PRIVATE_KEY` in both -Actions secrets and Dependabot secrets. Dependabot-triggered workflows only -have access to Dependabot secrets. diff --git a/pixi.toml b/pixi.toml index 95f0cf9a..da7e6adb 100644 --- a/pixi.toml +++ b/pixi.toml @@ -87,6 +87,10 @@ py-rattler = ">=0.25,<0.26" niquests = ">=3.21.0,<4" [feature.scripts.tasks] +check-dependabot-merge = { + cmd = "python scripts/check_dependabot_merge.py", + description = "Check whether every Dependabot update qualifies for auto-merge", +} compare-completeness = { cmd = "python scripts/compare_pkg_completeness.py", description = "Add two arguments to give it the DISTRO and CHANNEL", diff --git a/scripts/check_dependabot_merge.py b/scripts/check_dependabot_merge.py new file mode 100644 index 00000000..22f04613 --- /dev/null +++ b/scripts/check_dependabot_merge.py @@ -0,0 +1,54 @@ +"""Allow patch updates and stable minor updates for every dependency in a PR.""" + +from __future__ import annotations + +import json +import os +import re +from pathlib import Path + +RELEASE = re.compile(r"v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)") + + +def eligible_dependency(dependency: object) -> bool: + if not isinstance(dependency, dict): + return False + + previous = dependency.get("prevVersion") + current = dependency.get("newVersion") + if not isinstance(previous, str) or not isinstance(current, str): + return False + + previous_release = RELEASE.fullmatch(previous) + if previous_release is None or RELEASE.fullmatch(current) is None: + return False + + update_type = dependency.get("updateType") + return update_type == "version-update:semver-patch" or ( + update_type == "version-update:semver-minor" and previous_release[1] != "0" + ) + + +def eligible_update(metadata: str) -> bool: + try: + dependencies = json.loads(metadata) + except json.JSONDecodeError: + return False + + return ( + isinstance(dependencies, list) + and bool(dependencies) + and all(eligible_dependency(dependency) for dependency in dependencies) + ) + + +def main() -> None: + eligible = eligible_update(os.environ.get("DEPENDENCIES", "")) + result = f"eligible={str(eligible).lower()}" + print(result) + with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: + output.write(f"{result}\n") + + +if __name__ == "__main__": + main()