diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d01021b9..707cd0cc 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -22,19 +22,47 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' permissions: - contents: write + 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 with: github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Approve and enable auto-merge - if: steps.metadata.outputs.update-type != 'version-update:semver-major' - run: | - gh pr review --approve "$PR_URL" - gh pr merge --auto --squash "$PR_URL" + - name: Check auto-merge policy + id: policy + env: + DEPENDENCIES: ${{ steps.metadata.outputs.updated-dependencies-json }} + run: pixi run check-dependabot-merge + - 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/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()