Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
75 changes: 52 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve workflow cancellation in downstream guards

When this workflow is manually canceled or superseded, always() remains true, so the newly guarded test job—and the quality, SonarQube, and dependency-review jobs using the same pattern—can continue or start despite cancellation. This can run the entire expensive CI suite after an operator attempted to stop it; use a cancellation-aware status check such as !cancelled() while retaining the fail-open behavior for classification failures.

Useful? React with 👍 / 👎.

runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion packages/extended-data/tests/core/test_release_hygiene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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

Expand Down