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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions changelog.d/+skip-opening-labels.feature.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
34 changes: 34 additions & 0 deletions tests/validate_news/test_news.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
20 changes: 20 additions & 0 deletions validate-news/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand All @@ -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 }}
Expand Down