From 39e5fae301db0eccfd61ddbaa7c7b26d5feeb55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 11:04:27 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(validate-news):=20skip=20label?= =?UTF-8?q?s=20set=20at=20opening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gh pr create --label` labels a pull request right after opening it, so callers triggered on `labeled` ran the check twice, and those that cancel runs in progress showed the cancelled run as a failed check. A label event within a minute of opening now passes at once: the run for `opened` reads the labels from the API when it checks, so it already covers that label. --- README.md | 4 +++ changelog.d/+skip-opening-labels.feature.md | 2 ++ tests/conftest.py | 6 ++++ tests/validate_news/test_news.py | 34 +++++++++++++++++++++ validate-news/action.yml | 20 ++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 changelog.d/+skip-opening-labels.feature.md diff --git a/README.md b/README.md index f464b1b..b614d7b 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,10 @@ The Action expects a pull request base ref and a Markdown or reStructuredText ch PR's labels from the API rather than from the event, so re-running an older run sees the labels the PR has now; the job needs `pull-requests: read` for that. +Opening a PR with labels, as `gh pr create --label` does, fires `labeled` a moment after `opened`. A label event within +a minute of opening passes at once, since the run for `opened` reads the labels when it checks. Do not cancel runs in +progress with a `concurrency` group: the cancelled run would show as a failed check while the other one passes. + ## Refresh release tools Call `upgrade-release-tools.yaml` from a scheduled or manual workflow. Its read-only job refreshes the universal diff --git a/changelog.d/+skip-opening-labels.feature.md b/changelog.d/+skip-opening-labels.feature.md new file mode 100644 index 0000000..a49441e --- /dev/null +++ b/changelog.d/+skip-opening-labels.feature.md @@ -0,0 +1,2 @@ +`validate-news` passes at once for a label added or removed within a minute of a pull request opening, as +`gh pr create --label` does, since the run for the opening already checks the current labels. diff --git a/tests/conftest.py b/tests/conftest.py index 4290b36..912b684 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -473,6 +473,12 @@ def _expression( return "Bot" if environment.get("BOT_AUTHOR") == "true" else "User" if expression == "github.event.pull_request.number": return environment.get("PULL_NUMBER", "7") + if expression == "github.event.action": + return environment.get("EVENT_ACTION", "opened") + if expression == "github.event.pull_request.created_at": + return environment.get("CREATED_AT", "2026-01-01T00:00:00Z") + if expression == "github.event.pull_request.updated_at": + return environment.get("UPDATED_AT", "2026-01-01T00:00:00Z") raise ValueError(f"unsupported GitHub step environment: {expression}") # pragma: no cover - new syntax only diff --git a/tests/validate_news/test_news.py b/tests/validate_news/test_news.py index 41c7e23..46e9d49 100644 --- a/tests/validate_news/test_news.py +++ b/tests/validate_news/test_news.py @@ -461,3 +461,37 @@ def test_validate_news_explains_choices( ) assert explanation in result.stdout + + +@pytest.mark.parametrize( + ("event", "updated", "checked"), + [ + pytest.param("labeled", "2026-01-01T00:00:02Z", False, id="labeled-at-opening"), + pytest.param("unlabeled", "2026-01-01T00:00:59Z", False, id="unlabeled-at-opening"), + pytest.param("labeled", "2026-01-01T00:01:00Z", True, id="labeled-later"), + pytest.param("synchronize", "2026-01-01T00:00:02Z", True, id="pushed-at-opening"), + ], +) +def test_validate_news_skips_labels_set_while_opening( + repository: Path, + tmp_path: Path, + action_scripts: Path, + event: str, + updated: str, + checked: bool, + github_api: tuple[HTTPServer, dict[str, str]], +) -> None: + result = action( + repository, + tmp_path, + action_scripts, + "validate-news", + version="0.2.0", + BASE_REF="main", + BOT_AUTHOR="false", + EVENT_ACTION=event, + CREATED_AT="2026-01-01T00:00:00Z", + UPDATED_AT=updated, + **_current_labels(github_api, "false"), + ) + assert (result.returncode, "add a changelog fragment" in result.stderr) == ((1, True) if checked else (0, False)) diff --git a/validate-news/action.yml b/validate-news/action.yml index fe6de80..81a049d 100644 --- a/validate-news/action.yml +++ b/validate-news/action.yml @@ -28,8 +28,25 @@ inputs: runs: using: composite steps: + - id: opening + name: 🧭 Skip labels set while opening + if: github.event.action == 'labeled' || github.event.action == 'unlabeled' + shell: bash + env: + CREATED: ${{ github.event.pull_request.created_at }} + UPDATED: ${{ github.event.pull_request.updated_at }} + run: | + set -euo pipefail + # `gh pr create --label` labels a pull request right after opening it, so `labeled` fires beside `opened`. The + # run for `opened` reads the current labels when it checks, which leaves this run nothing to add. + seconds=$(($(date -d "$UPDATED" +%s) - $(date -d "$CREATED" +%s))) + if ((seconds < 60)); then + echo "News check skipped: labels changed ${seconds}s after opening, which the run for opening covers" + echo 'skip=true' >>"$GITHUB_OUTPUT" + fi - id: news-change name: 🧭 Check whether news inputs changed + if: steps.opening.outputs.skip != 'true' shell: bash env: BASE_REF: ${{ github.base_ref }} @@ -91,6 +108,7 @@ runs: echo "Changed fragments: $(plural "$(<"$RUNNER_TEMP/news-fragment-count")" fragment) valid against origin/$BASE_REF" - id: labels name: 🏷️ Read the current labels + if: steps.opening.outputs.skip != 'true' shell: bash env: GH_TOKEN: ${{ github.token }} @@ -103,6 +121,7 @@ runs: echo "Current labels of #$PULL: skip news $([[ "$skip" == true ]] && echo present || echo absent)" echo "skip=$skip" >>"$GITHUB_OUTPUT" - name: 🔍 Require news for human changes + if: steps.opening.outputs.skip != 'true' shell: bash env: SKIP_NEWS: ${{ steps.labels.outputs.skip }} @@ -124,6 +143,7 @@ runs: fi - id: lock-change name: 🧭 Check whether release inputs changed + if: steps.opening.outputs.skip != 'true' shell: bash env: BASE_REF: ${{ github.base_ref }}