diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index e290768..feb2071 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -20,10 +20,38 @@ jobs: runs-on: ubuntu-latest if: > startsWith(github.event.pull_request.head.ref, 'release-please--') || - (github.event.pull_request.user.login == 'dependabot[bot]' && - contains(github.event.pull_request.head.ref, '/non-major-')) + github.event.pull_request.user.login == 'dependabot[bot]' steps: + - id: classification + env: + IS_RELEASE_PLEASE: ${{ startsWith(github.event.pull_request.head.ref, 'release-please--') }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + python3 - <<'PY' + import os + import re + + should_merge = os.environ["IS_RELEASE_PLEASE"] == "true" + if not should_merge: + should_merge = "/non-major-" in os.environ["PR_HEAD_REF"] + if not should_merge: + versions = re.search( + r"\bfrom\s+[^0-9]*(\d+(?:\.\d+){0,2})\S*\s+to\s+[^0-9]*(\d+(?:\.\d+){0,2})", + os.environ["PR_TITLE"], + ) + if versions: + old = tuple(int(part) for part in versions.group(1).split(".")) + new = tuple(int(part) for part in versions.group(2).split(".")) + old += (0,) * (3 - len(old)) + new += (0,) * (3 - len(new)) + should_merge = new > old and new[0] == old[0] + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"should-merge={str(should_merge).lower()}\n") + PY - name: Enable auto-merge (merge commit) + if: steps.classification.outputs.should-merge == 'true' env: GH_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} PR_URL: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3d963a..5670ff9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,14 +9,52 @@ on: permissions: {} jobs: + classify-pr: + name: Classify pull request + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + skip-expensive-ci: ${{ steps.classification.outputs.skip-expensive-ci }} + steps: + - id: classification + env: + IS_RELEASE_PLEASE: ${{ startsWith(github.event.pull_request.head.ref, 'release-please--') }} + IS_DEPENDABOT: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + python3 - <<'PY' + import os + import re + + should_skip = os.environ["IS_RELEASE_PLEASE"] == "true" + if not should_skip and os.environ["IS_DEPENDABOT"] == "true": + # Dependabot documents group identifiers in its branch names. Its + # standard single-dependency titles provide a safe fallback while + # a group is first being adopted. + should_skip = "/non-major-" in os.environ["PR_HEAD_REF"] + if not should_skip: + versions = re.search( + r"\bfrom\s+[^0-9]*(\d+(?:\.\d+){0,2})\S*\s+to\s+[^0-9]*(\d+(?:\.\d+){0,2})", + os.environ["PR_TITLE"], + ) + if versions: + old = tuple(int(part) for part in versions.group(1).split(".")) + new = tuple(int(part) for part in versions.group(2).split(".")) + old += (0,) * (3 - len(old)) + new += (0,) * (3 - len(new)) + should_skip = new > old and new[0] == old[0] + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"skip-expensive-ci={str(should_skip).lower()}\n") + PY + test: name: Python ${{ matrix.python-version }} - if: > - github.event_name != 'pull_request' || !( - startsWith(github.event.pull_request.head.ref, 'release-please--') || - (github.event.pull_request.user.login == 'dependabot[bot]' && - contains(github.event.pull_request.head.ref, '/non-major-')) - ) + needs: classify-pr + if: always() && needs.classify-pr.outputs.skip-expensive-ci != 'true' runs-on: ubuntu-latest permissions: contents: read @@ -41,12 +79,8 @@ jobs: quality: name: CI / gate - if: > - github.event_name != 'pull_request' || !( - startsWith(github.event.pull_request.head.ref, 'release-please--') || - (github.event.pull_request.user.login == 'dependabot[bot]' && - contains(github.event.pull_request.head.ref, '/non-major-')) - ) + needs: classify-pr + if: always() && needs.classify-pr.outputs.skip-expensive-ci != 'true' runs-on: ubuntu-latest permissions: contents: read @@ -71,14 +105,11 @@ jobs: sonarqube: name: SonarQube Cloud + needs: classify-pr if: > + always() && needs.classify-pr.outputs.skip-expensive-ci != 'true' && (github.event_name != 'pull_request' || - github.event.pull_request.head.repo.full_name == github.repository) && - !(github.event_name == 'pull_request' && ( - startsWith(github.event.pull_request.head.ref, 'release-please--') || - (github.event.pull_request.user.login == 'dependabot[bot]' && - contains(github.event.pull_request.head.ref, '/non-major-')) - )) + github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest permissions: contents: read @@ -104,12 +135,10 @@ jobs: dependency-review: name: Dependency Review / gate + needs: classify-pr if: > - github.event_name == 'pull_request' && !( - startsWith(github.event.pull_request.head.ref, 'release-please--') || - (github.event.pull_request.user.login == 'dependabot[bot]' && - contains(github.event.pull_request.head.ref, '/non-major-')) - ) + always() && github.event_name == 'pull_request' && + needs.classify-pr.outputs.skip-expensive-ci != 'true' runs-on: ubuntu-latest permissions: contents: read diff --git a/packages/extended-data/tests/core/test_release_hygiene.py b/packages/extended-data/tests/core/test_release_hygiene.py index 9769855..a7adc9a 100644 --- a/packages/extended-data/tests/core/test_release_hygiene.py +++ b/packages/extended-data/tests/core/test_release_hygiene.py @@ -252,12 +252,17 @@ def test_automerge_workflow_uses_org_ci_token_without_checkout() -> None: automerge_workflow = (WORKFLOW_ROOT / "automerge.yml").read_text(encoding="utf-8") workflow = yaml.load(automerge_workflow, Loader=yaml.BaseLoader) automerge_steps = workflow["jobs"]["automerge"]["steps"] - merge_step = next(step for step in automerge_steps if step["name"] == "Enable auto-merge (merge commit)") + merge_step = next( + step for step in automerge_steps if step.get("name") == "Enable auto-merge (merge commit)" + ) assert "pull_request_target" in workflow["on"] assert workflow["permissions"] == {"contents": "write", "pull-requests": "write"} assert merge_step["env"]["GH_TOKEN"] == "${{ secrets.CI_GITHUB_TOKEN }}" assert "--auto --merge" in merge_step["run"] + classification_step = next(step for step in automerge_steps if step.get("id") == "classification") + assert "/non-major-" in classification_step["run"] + assert "PR_TITLE" in classification_step["env"] for step in automerge_steps: assert step.get("uses") != "actions/checkout" @@ -289,6 +294,7 @@ def test_lightweight_release_and_non_major_dependabot_prs_skip_expensive_ci() -> assert "release-please--" in ci_workflow assert "dependabot[bot]" in ci_workflow assert "/non-major-" in ci_workflow + assert "PR_TITLE" in ci_workflow assert "CI / gate" in ci_workflow assert "SonarQube Cloud" in ci_workflow