Skip to content

docs(contributing): correct releasing.rst's cascade guard (#958) - #959

Merged
JarryShaw merged 1 commit into
mainfrom
docs/958-releasing-cascade-guard
Oct 1, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
docs/958-releasing-cascade-guard

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Please follow the guide below

  • Searched for similar pull requests
  • Followed the coding style (make pylint, make mypy, make isort)
  • make test passes, and a test case covers the change
  • Added a changelog entry under docs/source/changelog/ and regenerated CHANGELOG.md, if the change is user-visible — N/A — documentation prose only, no library behaviour change

What is the purpose of your pull request?

  • fix — corrects a defect
  • feat — adds a feature
  • perf — changes performance, not behaviour
  • refactor — changes neither behaviour nor performance
  • test — tests only
  • docs — documentation only
  • ci — workflows or build tooling
  • chore — anything else

Description of your pull request and other information

Closes #958. The companion to #957. On main today this is the only page that states the cascade guard at all, and it states it wrongly — pep.rst does not mention it, and the corrected version of that page lives only in unmerged #957.

What was wrong. releasing.rst generalised the sibling workflows' guard to the three publishing jobs:

The three jobs here do the same for each of their own direct dependencies: !cancelled() && needs.<job>.result != 'failure'.

No job in create-release.yml uses that spelling. The only two occurrences in the file are comments explaining why it is not used (:119, :124-128). What tag, pypi and conda actually use, for github and — for conda — tag:

!cancelled() && (needs.<job>.result == 'success' || needs.<job>.result == 'skipped')

Why it matters rather than being a typo. The two spellings differ only when a predecessor's own result is cancelled, and that is the case the real spelling exists for. With tag cancelled and the run itself not cancelled, != 'failure' evaluates true and conda runs, into actions/checkout with ref: conda-<version>+0 for a tag tag never pushed — trading a clean skip for a checkout failure. So the documented guard would have reintroduced exactly what the real one prevents.

A second over-generalisation, which the original sentence also carried. "each of their own direct dependencies" is false for version_check, which is a direct dependency of all three and is required to have strictly succeeded, with no || 'skipped' (:445, :523, :622). It now has its own sentence, with the reason: it produces the evidence the gates read, so a skipped or cancelled version_check leaves them nothing to decide on.

Untouched and correct: the first half of the paragraph. cron-vendor.yml, deploy-pages.yml and cron-conda.yml genuinely do use != 'failure' for needs.unit-tests.result. Only the sentence generalising that to the publishing jobs was wrong.

Verification. tests/project/test_release_gates.py:624 already asserts != 'failure' must not appear in these conditions — so the behaviour was pinned and only the prose disagreed. 34 tests, 23 subtests, re-derived as 34 tests, OK under plain unittest. grep -rln "releasing.rst" tests/ is empty, so no test pins this page's prose. The edited block parses under docutils.

A grep trap for anyone re-checking this: grep "!= 'failure'" docs/source/contributing/releasing.rst on main returns only :171, which is the correct occurrence. The wrong one wrapped across a line break, so a single-line grep missed it entirely.

Not verified: no sphinx-build was run. No workflow file was changed.

The page generalised the sibling workflows' guard to the three
publishing jobs: "The three jobs here do the same for each of their own
direct dependencies: `!cancelled() && needs.<job>.result !=
'failure'`". No job in create-release.yml uses that spelling. The only
two occurrences in the file are comments explaining why it is not used
(:119, :124-128).

What the three actually use, for `github` and -- for `conda` -- `tag`:

    !cancelled() && (needs.<job>.result == 'success' ||
                     needs.<job>.result == 'skipped')

The two differ only when a predecessor's own result is `cancelled`:
`!= 'failure'` admits it, the equality pair does not. With `tag`
cancelled and the run itself not, `!= 'failure'` would send `conda`
into `actions/checkout` with `ref: conda-<version>+0` for a tag never
pushed, trading a clean skip for a checkout failure. So the documented
guard would have reintroduced what the real one prevents.

`version_check` is now named as the exception rather than folded into
"each of their own direct dependencies", which it is: all three require
it to have strictly succeeded, with no `|| 'skipped'`, because it
produces the evidence the gates read.

The first half of the paragraph is untouched and correct -- the three
sibling workflows do use `!= 'failure'` for `needs.unit-tests.result`.

tests/project/test_release_gates.py:624 already asserts `!= 'failure'`
must not appear in these conditions, so the behaviour was pinned and
only the prose disagreed. 34 tests, 23 subtests, re-derived as 34 under
plain unittest. No test pins this page's prose.
@JarryShaw JarryShaw added docs Pull requests that change documentation only (docs: subject prefix) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 1, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO at fc5b5c53c — fable cross-review, first round clean. Different model from the author and from #957's reviewer.

It did the two things I most wanted and neither was in the diff's own claims.

It enumerated rather than accepted "differ only when cancelled". needs.<job>.result takes exactly {success, failure, cancelled, skipped}; the two spellings agree on success T/T, failure F/F, skipped T/T, and diverge only on cancelled T/F. On a whole-run cancellation the shared !cancelled() prefix makes both false, so there is no second divergence. "Only" survives.

It checked the half of the paragraph this change does not touch, which nobody had: all three siblings do open with exactly !cancelled() && needs.unit-tests.result != 'failure' && — cron-vendor.yml:68, deploy-pages.yml:66, cron-conda.yml:64. So the untouched sentence is correct and the fix is confined to the right place.

It also derived the conda/tag direction independently of pep.rst — the clause that was inverted on the sibling page — and found the checkout real: ref: conda-${{ … PCAPKIT_VERSION }}+0 at :642, with tag being what pushes that tag. And it noted what the pinning test does not cover: there is no version_check.result assertion anywhere in test_release_gates.py, so the new version_check paragraph rests on the workflow file rather than on a test.

Three things it raised that I am acting on separately rather than here:

  • A correction to my own PR description, now fixed. I wrote that main ships one page stating this guard correctly and one wrongly. It does not: main's pep.rst does not state the cascade guard at all — I verified, the only skipped hit on that page is unrelated prose at :310. The correct version exists only in unmerged docs(contributing): correct pep.rst's release gating description (#956) #957, so my sentence described the post-docs(contributing): correct pep.rst's release gating description (#956) #957 state.
  • The workflow's own comment carries the very over-generalisation this change removes. create-release.yml:121-124 says the three jobs "do the same for each of their own direct dependencies, but spelled as" the equality pair — which is false for version_check, exactly as the prose was. After this merges, the docs describe the workflow more accurately than the workflow describes itself. Filing it.
  • "unit-tests's other callers" in the untouched sentence is loose, since the three depend on unit-tests only transitively. Pre-existing and outside this diff; leaving it.

One more non-blocking point worth keeping: the version_check strictness has a sharper consequence than the paragraph gives. On a v* tag push the startsWith clause short-circuits the evidence check, so version_check.result == 'success' becomes the only thing transitively binding the unit-tests gate. True as written, just not the strongest statement of it.

Unverified by anyone: no sphinx-build, on this page or pep.rst.

Labelled review: good-to-go. CI still running, so not ready to merge — and the merge is yours.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 1, 2026
JarryShaw added a commit that referenced this pull request Oct 1, 2026
… predecessors (#960)

The comment above `jobs:` said `tag`, `pypi` and `conda` spell the
guard as the success-or-skipped equality pair "for each of their own
direct dependencies". That is false for `version_check`, a direct
dependency of all three (:443, :521, :620) which each require to have
strictly succeeded, with no `|| 'skipped'` (:445, :523, :622). The
equality pair applies only to `github`, and for `conda` also `tag`.

The comment is where the documentation got the claim from -- #958 was
the same error in releasing.rst, corrected in #959 -- so leaving it
would have had the docs describe this file more accurately than the
file describes itself.

Comment text only; no executable YAML changed. Verified by parsing both
revisions with `yaml.safe_load` and comparing the loaded structures,
which is stronger than the `grep -vE '^[+-]#'` it replaces -- that
pattern only matches `#` at column zero and says nothing about indented
YAML.

Also adds test_version_check_must_strictly_succeed, which nothing in
the module covered. It asserts the set of comparisons against
`version_check.result` is exactly `{('==', 'success')}` rather than
checking substrings, because the substring form cannot catch
`(!= 'failure' || == 'success')` -- logically just `!= 'failure'`, so it
admits `skipped` and `cancelled` while keeping the `== 'success'`
literal -- nor a bare `|| == 'cancelled'`.

Falsifiability confirmed against mutated copies of the workflow: head
OK, and failures on `|| == 'skipped'`, `|| == 'cancelled'`,
`(!= 'failure' || == 'success')` and the clause deleted outright.

tests/project/test_release_gates.py: 35 passed, 26 subtests,
re-derived as 35 under plain unittest.
JarryShaw added a commit that referenced this pull request Oct 1, 2026
… predecessors (#960)

The comment above `jobs:` said `tag`, `pypi` and `conda` spell the
guard as the success-or-skipped equality pair "for each of their own
direct dependencies". That is false for `version_check`, a direct
dependency of all three (:443, :521, :620) which each require to have
strictly succeeded, with no `|| 'skipped'` (:445, :523, :622). The
equality pair applies only to `github`, and for `conda` also `tag`.

The comment is where the documentation got the claim from -- #958 was
the same error in releasing.rst, corrected in #959 -- so leaving it
would have had the docs describe this file more accurately than the
file describes itself.

Comment text only; no executable YAML changed. Verified by parsing both
revisions with `yaml.safe_load` and comparing the loaded structures,
which is stronger than a `grep -vE '^[+-]#'` -- that pattern only
matches `#` at column zero and says nothing about indented YAML.

Also adds test_version_check_must_strictly_succeed, which nothing in
the module covered. Substring assertions cannot pin this clause: a
weakening can keep the `== 'success'` literal while meaning the
opposite, since `(!= 'failure' || == 'success')` is just
`!= 'failure'`. So the test strips whitespace, then asserts the set of
comparisons against `version_check.result` is exactly
`{('==', 'success')}` *and* that the property is referenced exactly
once. The count is the exhaustiveness half: anything added alongside
the real clause raises it however spelled, including the mirror-image
`'skipped' == needs.version_check.result` and forms using no `==` at
all, such as `contains(fromJSON('["skipped"]'), ...)`.

Falsifiability confirmed against mutated copies of the workflow, each
checked to be valid YAML first. Head passes; all seven of
`|| == 'skipped'`, `|| == 'cancelled'`, `(!= 'failure' || == 'success')`,
reversed operands, `contains(fromJSON(...))`, whitespace-padded property
dots, and the clause deleted outright fail.

tests/project/test_release_gates.py: 35 passed, 26 subtests,
re-derived as 35 under plain unittest.
@JarryShaw
JarryShaw merged commit a1a13e6 into main Oct 1, 2026
63 checks passed
@JarryShaw
JarryShaw deleted the docs/958-releasing-cascade-guard branch October 1, 2026 05:16
JarryShaw added a commit that referenced this pull request Oct 1, 2026
… predecessors (#960)

The comment above `jobs:` said `tag`, `pypi` and `conda` spell the
guard as the success-or-skipped equality pair "for each of their own
direct dependencies". That is false for `version_check`, a direct
dependency of all three (:443, :521, :620) which each require to have
strictly succeeded, with no `|| 'skipped'` (:445, :523, :622). The
equality pair applies only to `github`, and for `conda` also `tag`.

The comment is where the documentation got the claim from -- #958 was
the same error in releasing.rst, corrected in #959 -- so leaving it
would have had the docs describe this file more accurately than the
file describes itself.

Comment text only; no executable YAML changed. Verified by parsing both
revisions with `yaml.safe_load` and comparing the loaded structures,
which is stronger than a `grep -vE '^[+-]#'` -- that pattern only
matches `#` at column zero and says nothing about indented YAML.

Also adds test_version_check_must_strictly_succeed, which nothing in
the module covered. Substring assertions cannot pin this clause on
their own, so it makes three, over a whitespace-stripped condition:

- the clause appears as a top-level `&&` conjunct, because vocabulary
  is not structure -- a one-character `&&`-to-`||` slip, or a well-meant
  `|| github.event_name == 'push'`, keeps every token while nullifying
  the gate;
- the set of comparisons against `version_check.result` is exactly
  `{('==', 'success')}`, since `(!= 'failure' || == 'success')` keeps the
  literal while meaning the opposite;
- `version_check.result` is referenced exactly once, which is the
  exhaustiveness half: anything added raises the count however spelled,
  including `'skipped' == needs.version_check.result` and forms using no
  `==` at all.

Falsifiability confirmed against mutated copies, each valid YAML first.
Head passes; `|| == 'skipped'`, `|| == 'cancelled'`,
`(!= 'failure' || == 'success')`, reversed operands,
`contains(fromJSON(...))`, whitespace-padded dots, `|| event_name`,
a negation, `&&`-to-`||`, `|| outputs != ''`, and the clause deleted all
fail.

tests/project/test_release_gates.py: 35 passed, 26 subtests,
re-derived as 35 under plain unittest.
JarryShaw added a commit that referenced this pull request Oct 1, 2026
… predecessors (#960)

The comment above `jobs:` said `tag`, `pypi` and `conda` spell the
guard as the success-or-skipped equality pair "for each of their own
direct dependencies". That is false for `version_check`, a direct
dependency of all three (:443, :521, :620) which each require to have
strictly succeeded, with no `|| 'skipped'` (:445, :523, :622). The
equality pair applies only to `github`, and for `conda` also `tag`.

The comment is where the documentation got the claim from -- #958 was
the same error in releasing.rst, corrected in #959 -- so leaving it
would have had the docs describe this file more accurately than the
file describes itself.

Comment text only; no executable YAML changed. Verified by parsing both
revisions with `yaml.safe_load` and comparing the loaded structures,
which is stronger than a `grep -vE '^[+-]#'` -- that pattern only
matches `#` at column zero and says nothing about indented YAML.

Also adds test_version_check_must_strictly_succeed, which nothing in
the module covered. An `assertNotIn` over known-bad spellings cannot
pin this clause, since `(!= 'failure' || == 'success')` keeps the
literal while meaning the opposite. Two assertions over a
whitespace-stripped condition instead:

- the clause appears as a top-level `&&` conjunct, because vocabulary
  is not structure -- a one-character `&&`-to-`||` slip keeps every
  token while nullifying the gate;
- `version_check.result` is referenced exactly once, which closes the
  rest: anything added alongside the real clause raises the count
  however spelled.

A comparison-set assertion was carried for two rounds and is dropped as
provably redundant: the conjunct substring guarantees one occurrence
followed by `== 'success'`, the count guarantees it is the only one, so
the set is necessarily `{('==', 'success')}`. Brute-forced over 173,088
synthetic conditions -- zero cases where it fires alone.

Knowingly not closed, and now recorded in the docstring: a top-level
`||` outside the conjunction chain. Conjunction is monotone, so adding
`&&` conjuncts can only strengthen the gate; disjunction is the only
remaining weakening and catching it needs the expression evaluated
rather than matched. Tracked as #962.

Falsifiability confirmed against mutated copies, each valid YAML first.
Head passes, a legitimate extra `&&` conjunct still passes, and
`|| == 'skipped'`, `(!= 'failure' || == 'success')`, reversed operands,
`contains(fromJSON(...))`, `|| event_name`, a negation, `&&`-to-`||`
and the clause deleted all fail.

tests/project/test_release_gates.py: 35 passed, 26 subtests,
re-derived as 35 under plain unittest.
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Oct 1, 2026
JarryShaw added a commit that referenced this pull request Oct 1, 2026
… predecessors (#960)

The comment above `jobs:` said `tag`, `pypi` and `conda` spell the
guard as the success-or-skipped equality pair "for each of their own
direct dependencies". That is false for `version_check`, a direct
dependency of all three (:443, :521, :620) which each require to have
strictly succeeded, with no `|| 'skipped'` (:445, :523, :622). The
equality pair applies only to `github`, and for `conda` also `tag`.

The comment is where the documentation got the claim from -- #958 was
the same error in releasing.rst, corrected in #959 -- so leaving it
would have had the docs describe this file more accurately than the
file describes itself.

Comment text only; no executable YAML changed. Verified by parsing both
revisions with `yaml.safe_load` and comparing the loaded structures,
which is stronger than a `grep -vE '^[+-]#'` -- that pattern only
matches `#` at column zero and says nothing about indented YAML.

Also adds test_version_check_must_strictly_succeed, which nothing in
the module covered. An `assertNotIn` over known-bad spellings cannot
pin this clause, since `(!= 'failure' || == 'success')` keeps the
literal while meaning the opposite. Two assertions over a
whitespace-stripped condition instead:

- the clause appears as a top-level `&&` conjunct, because vocabulary
  is not structure -- a one-character `&&`-to-`||` slip keeps every
  token while nullifying the gate;
- `version_check.result` is referenced exactly once, which closes the
  rest: anything added alongside the real clause raises the count
  however spelled.

A comparison-set assertion was carried for two rounds and is dropped as
provably redundant: the conjunct substring guarantees one occurrence
followed by `== 'success'`, the count guarantees it is the only one, so
the set is necessarily `{('==', 'success')}`. Brute-forced over 173,088
synthetic conditions -- zero cases where it fires alone.

Knowingly not closed, and now recorded in the docstring: a top-level
`||` outside the conjunction chain. Conjunction is monotone, so adding
`&&` conjuncts can only strengthen the gate; disjunction is the only
remaining weakening and catching it needs the expression evaluated
rather than matched. Tracked as #962.

Falsifiability confirmed against mutated copies, each valid YAML first.
Head passes, a legitimate extra `&&` conjunct still passes, and
`|| == 'skipped'`, `(!= 'failure' || == 'success')`, reversed operands,
`contains(fromJSON(...))`, `|| event_name`, a negation, `&&`-to-`||`
and the clause deleted all fail.

tests/project/test_release_gates.py: 35 passed, 26 subtests,
re-derived as 35 under plain unittest.
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
@JarryShaw JarryShaw moved this to Done in PyPCAPKit Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Pull requests that change documentation only (docs: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

docs(contributing): releasing.rst documents the cascade guard with the rejected spelling

1 participant