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 }}