From 80e6291c74e02b76860615e3367e51bdfa365de9 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Tue, 13 Oct 2026 13:43:56 -0400 Subject: [PATCH 1/3] fix(ci): Conda Update keeps the release's name and notes when it uploads (#1657) conda-dist attaches each bump's packages with ncipollo/release-action@v1.21.0 and allowUpdates: true, which calls Update a release first. That call sends name, body, prerelease and draft whether or not the step sets them, and unset they are the tag, "" and false, so every bump wiped the release's name and notes. - The step sets omitBodyDuringUpdate, omitDraftDuringUpdate, omitNameDuringUpdate and omitPrereleaseDuringUpdate, which leave those fields out of the update. - releasing.rst says so, and that a draft would be retargeted at commit rather than published. - tests/project/test_conda_dist_keeps_release_notes_1657.py pins the four inputs and the step's whole input set; it fails on origin/main's cron-conda.yml with failures=6. --- .github/workflows/cron-conda.yml | 12 +++ docs/source/contributing/releasing.rst | 11 ++- ...est_conda_dist_keeps_release_notes_1657.py | 80 +++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 tests/project/test_conda_dist_keeps_release_notes_1657.py diff --git a/.github/workflows/cron-conda.yml b/.github/workflows/cron-conda.yml index 08030b80a..d49d8c255 100644 --- a/.github/workflows/cron-conda.yml +++ b/.github/workflows/cron-conda.yml @@ -596,6 +596,14 @@ jobs: # GitHub's workflow-file rule checks main, not the commit Create Release # made the release at. The tag stays: the target is "Unused if the Git # tag already exists" (REST docs, Update a release). + # + # #1657: that update also sends ``name``, ``body``, ``prerelease`` and + # ``draft``, which unset inputs make the tag, "" and false + # (src/Inputs.ts:70-92,128-135,210-240), so each bump wiped the + # release's name and notes. Each ``omit*DuringUpdate`` makes its field + # undefined (:210-252), which Octokit drops from the request + # (dist/index.js:38369-38388), so the release keeps what Create Release + # wrote. - name: Upload package to GitHub Release if: ${{ matrix.os == 'ubuntu-latest' && steps.release.outputs.published == 'true' }} uses: ncipollo/release-action@v1.21.0 @@ -603,5 +611,9 @@ jobs: allowUpdates: true artifacts: ${{ steps.conda_build.outputs.packages_csv }} commit: ${{ github.ref_name }} + omitBodyDuringUpdate: true + omitDraftDuringUpdate: true + omitNameDuringUpdate: true + omitPrereleaseDuringUpdate: true tag: "v${{ needs.conda-update.outputs.PCAPKIT_VERSION }}" token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/docs/source/contributing/releasing.rst b/docs/source/contributing/releasing.rst index 4de9bb7f0..0fbe75b3e 100644 --- a/docs/source/contributing/releasing.rst +++ b/docs/source/contributing/releasing.rst @@ -203,13 +203,18 @@ names the default branch -- ``github.ref_name`` on both its triggers -- as stays put, as Update a release calls the target "Unused if the Git tag already exists". It also uploads only to a published release, read first with ``gh release view``. With none, the action would create one, and its tag, at -``commit`` (``src/Action.ts:72-92``), and it would publish a draft, since an -unset ``draft`` is ``false`` on update (``src/Inputs.ts:210-213``). Conda +``commit`` (``src/Action.ts:72-92``), and it would give a draft ``commit`` as +its target (``src/Action.ts:85-92,107-120``). Conda Update bumped 1.5.0b9's build three times while that run waited on approval; once ``conda-dist`` runs on every bump (`#1632 `__), either path would tag the pending version at ``main``'s tip and fail Create Release's tag -check. That build then goes to anaconda.org only, with a ``::notice``. +check. That build then goes to anaconda.org only, with a ``::notice``. The +update also sends the release's name, notes and ``prerelease`` and ``draft`` +flags, which unset inputs make the tag, ``""`` and ``false`` +(``src/Inputs.ts:70-92,128-135,210-240``), so the step sets the four +``omit*DuringUpdate`` inputs that leave them out (`#1657 +`__). The Release App --------------- diff --git a/tests/project/test_conda_dist_keeps_release_notes_1657.py b/tests/project/test_conda_dist_keeps_release_notes_1657.py new file mode 100644 index 000000000..296023502 --- /dev/null +++ b/tests/project/test_conda_dist_keeps_release_notes_1657.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""Conda Update's GitHub upload keeps the release's name and notes (#1657). + +``conda-dist`` in :file:`.github/workflows/cron-conda.yml` attaches each build +bump's packages with ``ncipollo/release-action@v1.21.0`` and ``allowUpdates: +true``, which calls Update a release before uploading (``src/Action.ts:107-120``). +That request sends ``name``, ``body``, ``prerelease`` and ``draft`` whether or not +the step sets them, and unset they are the tag, ``""`` and ``false`` +(``src/Inputs.ts:70-92,128-135,210-240``). ``v1.3.5.post42``, ``v1.4.1``, +``v1.4.1.post2`` and ``v1.5.0a1`` read name-is-tag and a 0-character body that +way. Each ``omit*DuringUpdate`` input makes its field ``undefined`` +(``:210-252``), which Octokit drops from the request body +(``dist/index.js:38369-38388``), so the release keeps what Create Release wrote. + +The step is read with the scanner of ``test_release_tags_the_run_sha_1634.py``, +which runs without PyYAML; :meth:`TestTheUploadKeepsTheRelease.test_yaml_reads_the_same` +checks it against :func:`yaml.safe_load` where PyYAML is installed. + +""" + +from __future__ import annotations + +import unittest + +from tests.project.test_release_tags_the_run_sha_1634 import (CONDA_WORKFLOW, UPLOAD_STEP, + CondaDistMixin) + +#: The pinned action. The input names and the citations above are for this tag. +ACTION = 'ncipollo/release-action@v1.21.0' + +#: The inputs that leave a field out of Update a release, and the field each keeps. +OMITTED = { + 'omitBodyDuringUpdate': 'body', + 'omitDraftDuringUpdate': 'draft', + 'omitNameDuringUpdate': 'name', + 'omitPrereleaseDuringUpdate': 'prerelease', +} + +#: Every input the step sets. Another one changes what the update sends -- a +#: ``makeLatest``, say, whose ``legacy`` default is already sent as +#: ``make_latest`` (``action.yml:58-61``, ``src/Releases.ts:168``) -- so adding +#: one means reading the action again before adding it here. +INPUTS = {'allowUpdates', 'artifacts', 'commit', 'tag', 'token', *OMITTED} + + +class TestTheUploadKeepsTheRelease(CondaDistMixin, unittest.TestCase): + """Update a release leaves the name, body and flags as Create Release wrote them.""" + + def test_the_action_is_the_one_read(self) -> None: + self.assertEqual(self.upload().get('uses'), ACTION) + self.assertEqual(self.upload()['with'].get('allowUpdates'), 'true') + + def test_each_field_the_update_would_reset_is_omitted(self) -> None: + inputs = self.upload()['with'] + for flag, field in OMITTED.items(): + with self.subTest(flag=flag): + self.assertEqual(inputs.get(flag), 'true', + f'without {flag}, every bump resets the release\'s {field}') + + def test_every_input_is_accounted_for(self) -> None: + self.assertEqual(sorted(self.upload()['with']), sorted(INPUTS)) + + def test_yaml_reads_the_same(self) -> None: + """A key the scanner reads but YAML places elsewhere would pass vacuously.""" + try: + import yaml # pylint: disable=import-outside-toplevel + except ImportError: + self.skipTest('PyYAML is not installed (the `test` extra)') + steps = yaml.safe_load(CONDA_WORKFLOW.read_text(encoding='utf-8'))['jobs']['conda-dist']['steps'] + found = [step for step in steps if step.get('name') == UPLOAD_STEP] + self.assertEqual(len(found), 1) + inputs = found[0]['with'] + self.assertEqual(sorted(inputs), sorted(INPUTS)) + for flag in OMITTED: + with self.subTest(flag=flag): + self.assertIs(inputs[flag], True) + + +if __name__ == '__main__': + unittest.main() From e895368ae985536175a816923bd16d53aa8ae347 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Tue, 13 Oct 2026 14:57:02 -0400 Subject: [PATCH 2/3] fix(ci): Conda Update's release check gives a draft's real reason to skip (#1657) With omitDraftDuringUpdate set, the upload no longer publishes a draft; it still finds it by tag and updates it with target_commitish: commit (ncipollo/release-action v1.21.0 src/Action.ts:85-92,107-120), so the draft stays a draft but is retargeted at main, which a re-run of Create Release would reuse. The skip is unchanged; only its stated reason was stale. - cron-conda.yml: the check's comment and its draft ::notice say the upload would retarget the draft at main, not publish it. "tagging main's tip" moves into the missing-release reason, whose notice is byte-identical. - releasing.rst says the same. - test_release_tags_the_run_sha_1634.py: the docstring, message and notice pins follow; the draft pin fails on the previous text, and the missing-release pin now covers its whole sentence. --- .github/workflows/cron-conda.yml | 17 ++++++++-------- docs/source/contributing/releasing.rst | 5 +++-- .../test_release_tags_the_run_sha_1634.py | 20 +++++++++++-------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cron-conda.yml b/.github/workflows/cron-conda.yml index d49d8c255..9622943e7 100644 --- a/.github/workflows/cron-conda.yml +++ b/.github/workflows/cron-conda.yml @@ -558,11 +558,12 @@ jobs: # #1634: the upload below never makes the release. ncipollo/release-action # v1.21.0 creates a missing one, and its tag, at ``commit`` - # (src/Action.ts:72-92), and publishes a draft, since an unset ``draft`` - # is false on update (src/Inputs.ts:210-213). A bump made while Create - # Release waits on approval would then tag the pending version at main's - # tip, and Create Release's tag check would fail. Only the GitHub upload - # is skipped; the build is on anaconda.org regardless. + # (src/Action.ts:72-92), and retargets a draft at ``commit`` + # (:85-92,107-120): ``omitDraftDuringUpdate`` keeps it a draft, but a + # re-run of Create Release would reuse it, target and all. A bump made + # while Create Release waits on approval would then tag the pending + # version at main's tip, and Create Release's tag check would fail. Only + # the GitHub upload is skipped; the build is on anaconda.org regardless. - name: Check the GitHub release exists id: release if: ${{ matrix.os == 'ubuntu-latest' }} @@ -579,15 +580,15 @@ jobs: exit 0 fi if [ "$status" -eq 0 ] && [ "$draft" = true ]; then - why="is a draft, which the upload would publish" + why="is a draft, which the upload would retarget at main" elif [ "$(cat "$errors")" = 'release not found' ]; then - why="does not exist, and the upload would create it" + why="does not exist, and the upload would create it, tagging main's tip" else cat "$errors" >&2 echo "::error title=Could not read ${PCAPKIT_RELEASE_TAG}::gh release view exited ${status}." exit 1 fi - echo "::notice title=No GitHub upload for ${PCAPKIT_RELEASE_TAG}::${PCAPKIT_RELEASE_TAG} ${why}, tagging main's tip. Create Release makes it, at the commit its run was created for (#1634); this build is on anaconda.org only." + echo "::notice title=No GitHub upload for ${PCAPKIT_RELEASE_TAG}::${PCAPKIT_RELEASE_TAG} ${why}. Create Release makes it, at the commit its run was created for (#1634); this build is on anaconda.org only." echo "published=false" >> "$GITHUB_OUTPUT" # ``commit`` is sent to Update a release as ``target_commitish`` diff --git a/docs/source/contributing/releasing.rst b/docs/source/contributing/releasing.rst index 0fbe75b3e..c488a2931 100644 --- a/docs/source/contributing/releasing.rst +++ b/docs/source/contributing/releasing.rst @@ -203,8 +203,9 @@ names the default branch -- ``github.ref_name`` on both its triggers -- as stays put, as Update a release calls the target "Unused if the Git tag already exists". It also uploads only to a published release, read first with ``gh release view``. With none, the action would create one, and its tag, at -``commit`` (``src/Action.ts:72-92``), and it would give a draft ``commit`` as -its target (``src/Action.ts:85-92,107-120``). Conda +``commit`` (``src/Action.ts:72-92``), and it would retarget a draft at +``commit`` (``src/Action.ts:85-92,107-120``), which stays a draft but which a +re-run of Create Release would reuse, target and all. Conda Update bumped 1.5.0b9's build three times while that run waited on approval; once ``conda-dist`` runs on every bump (`#1632 `__), either path diff --git a/tests/project/test_release_tags_the_run_sha_1634.py b/tests/project/test_release_tags_the_run_sha_1634.py index 8e730f477..b81138f33 100644 --- a/tests/project/test_release_tags_the_run_sha_1634.py +++ b/tests/project/test_release_tags_the_run_sha_1634.py @@ -1234,11 +1234,13 @@ class TestCondaUpdateNeverMakesTheRelease(CondaDistMixin, unittest.TestCase): ``conda-dist`` attaches its packages with ``ncipollo/release-action@v1.21.0`` and ``allowUpdates: true``, kept so #1632's ``test_cron_conda_really_does_publish`` still finds it. That action's ``createOrUpdateRelease`` (``src/Action.ts:53-70``) - reads the release by tag; on a 404 it updates a draft of that tag or else - calls ``createRelease`` (``:72-92``) with ``target_commitish: commit`` - (``src/Releases.ts:76-99``), and its update sends ``draft: false`` unless - ``omitDraftDuringUpdate`` is set (``src/Inputs.ts:210-213``), publishing the - draft. Either way a bump made while Create Release waits on its approval -- + reads the release by tag, which GitHub's REST docs give as "a published + release"; on a 404 it updates a draft of that tag or else calls + ``createRelease`` (``:72-92``), both with ``target_commitish: commit`` + (``:107-120``, ``src/Releases.ts:76-99,150-174``). ``omitDraftDuringUpdate`` + keeps the draft a draft (``src/Inputs.ts:210-213``), but retargeted at + ``commit``, which a re-run of Create Release would reuse, target and all. + Either way a bump made while Create Release waits on its approval -- 1.5.0b9 waited three days, and cron-conda bumped three times meanwhile -- would put ``v`` on ``main``'s tip, and Create Release's tag check would then fail. On an existing release it calls Update a release with @@ -1336,7 +1338,8 @@ def test_no_release_skips_the_github_upload(self) -> None: self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(outputs, {'published': 'false'}) self.assertFalse(self.runs(outputs), 'the action would run, and create the release at main') - self.assertIn(f'::notice title=No GitHub upload for {self.TAG}::{self.TAG} does not exist', result.stdout) + self.assertIn(f'::notice title=No GitHub upload for {self.TAG}::{self.TAG} does not exist, and the ' + "upload would create it, tagging main's tip. Create Release makes it", result.stdout) self.assert_viewed_once() def test_a_draft_skips_the_github_upload(self) -> None: @@ -1345,8 +1348,9 @@ def test_a_draft_skips_the_github_upload(self) -> None: result, outputs = self.check(GH_DRAFT='true') self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(outputs, {'published': 'false'}) - self.assertFalse(self.runs(outputs), 'the action would run, and publish the draft') - self.assertIn(f'{self.TAG} is a draft, which the upload would publish', result.stdout) + self.assertFalse(self.runs(outputs), 'the action would run, and retarget the draft at main') + self.assertIn(f'::notice title=No GitHub upload for {self.TAG}::{self.TAG} is a draft, which the ' + 'upload would retarget at main. Create Release makes it', result.stdout) def test_a_published_release_is_uploaded_to(self) -> None: """With ``commit`` the default branch, so Update a release resolves ``main``.""" From edcf3439690af56f49d73acd78e3a5f536ff1f6d Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Tue, 13 Oct 2026 16:12:32 -0400 Subject: [PATCH 3/3] fix(ci): Conda Update skips a draft as a precaution, since Create Release resets its target (#1657) The previous commit said a re-run of Create Release would reuse a draft retargeted at main, target and all. softprops/action-gh-release v3.0.2 finds drafts (src/github.ts:832-866) and, given a non-empty target_commitish that differs, overwrites the target (:636-646); create-release.yml passes github.sha on workflow_run, so that run resets the draft's target to its own sha. - cron-conda.yml: the release check's comment gives tagging main's tip to the missing-release path only, and calls skipping a draft a precaution against retargeting a release Create Release owns. - releasing.rst and the TestCondaUpdateNeverMakesTheRelease docstring say the same; "either path would tag" is gone. The ::notice strings are unchanged, so no pin moves. The release, gates, #1657, unit-tests queue, doc-claims, workflow_run and #1633 tests pass. --- .github/workflows/cron-conda.yml | 15 +++++++++------ docs/source/contributing/releasing.rst | 19 +++++++++++-------- .../test_release_tags_the_run_sha_1634.py | 17 ++++++++++------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/cron-conda.yml b/.github/workflows/cron-conda.yml index 9622943e7..b358ea104 100644 --- a/.github/workflows/cron-conda.yml +++ b/.github/workflows/cron-conda.yml @@ -558,12 +558,15 @@ jobs: # #1634: the upload below never makes the release. ncipollo/release-action # v1.21.0 creates a missing one, and its tag, at ``commit`` - # (src/Action.ts:72-92), and retargets a draft at ``commit`` - # (:85-92,107-120): ``omitDraftDuringUpdate`` keeps it a draft, but a - # re-run of Create Release would reuse it, target and all. A bump made - # while Create Release waits on approval would then tag the pending - # version at main's tip, and Create Release's tag check would fail. Only - # the GitHub upload is skipped; the build is on anaconda.org regardless. + # (src/Action.ts:72-92), so a bump made while Create Release waits on + # approval would tag the pending version at main's tip, and Create + # Release's tag check would fail. It also retargets a draft at ``commit`` + # (:85-92,107-120), which ``omitDraftDuringUpdate`` keeps a draft, but + # Create Release's ``workflow_run`` run finds the draft and resets its + # target to the run's sha (softprops/action-gh-release v3.0.2, + # src/github.ts:636-646,832-866). Skipping a draft is a precaution + # against retargeting a release Create Release owns. Only the GitHub + # upload is skipped; the build is on anaconda.org regardless. - name: Check the GitHub release exists id: release if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/docs/source/contributing/releasing.rst b/docs/source/contributing/releasing.rst index c488a2931..6b81985c4 100644 --- a/docs/source/contributing/releasing.rst +++ b/docs/source/contributing/releasing.rst @@ -203,14 +203,17 @@ names the default branch -- ``github.ref_name`` on both its triggers -- as stays put, as Update a release calls the target "Unused if the Git tag already exists". It also uploads only to a published release, read first with ``gh release view``. With none, the action would create one, and its tag, at -``commit`` (``src/Action.ts:72-92``), and it would retarget a draft at -``commit`` (``src/Action.ts:85-92,107-120``), which stays a draft but which a -re-run of Create Release would reuse, target and all. Conda -Update bumped 1.5.0b9's build three times while that run waited on approval; -once ``conda-dist`` runs on every bump -(`#1632 `__), either path -would tag the pending version at ``main``'s tip and fail Create Release's tag -check. That build then goes to anaconda.org only, with a ``::notice``. The +``commit`` (``src/Action.ts:72-92``). Conda Update bumped 1.5.0b9's build +three times while that run waited on approval; once ``conda-dist`` runs on +every bump (`#1632 `__), +that would tag the pending version at ``main``'s tip and fail Create Release's +tag check. The action would also retarget a draft at ``commit`` +(``src/Action.ts:85-92,107-120``), which ``omitDraftDuringUpdate`` keeps a +draft, but Create Release's ``workflow_run`` run finds the draft and resets +its target to the run's sha (``action-gh-release`` 3.0.2's +``src/github.ts:636-646,832-866``), so skipping a draft is a precaution +against retargeting a release Create Release owns. +Either way the build goes to anaconda.org only, with a ``::notice``. The update also sends the release's name, notes and ``prerelease`` and ``draft`` flags, which unset inputs make the tag, ``""`` and ``false`` (``src/Inputs.ts:70-92,128-135,210-240``), so the step sets the four diff --git a/tests/project/test_release_tags_the_run_sha_1634.py b/tests/project/test_release_tags_the_run_sha_1634.py index b81138f33..b457a7cf2 100644 --- a/tests/project/test_release_tags_the_run_sha_1634.py +++ b/tests/project/test_release_tags_the_run_sha_1634.py @@ -1237,13 +1237,16 @@ class TestCondaUpdateNeverMakesTheRelease(CondaDistMixin, unittest.TestCase): reads the release by tag, which GitHub's REST docs give as "a published release"; on a 404 it updates a draft of that tag or else calls ``createRelease`` (``:72-92``), both with ``target_commitish: commit`` - (``:107-120``, ``src/Releases.ts:76-99,150-174``). ``omitDraftDuringUpdate`` - keeps the draft a draft (``src/Inputs.ts:210-213``), but retargeted at - ``commit``, which a re-run of Create Release would reuse, target and all. - Either way a bump made while Create Release waits on its approval -- - 1.5.0b9 waited three days, and cron-conda bumped three times meanwhile -- - would put ``v`` on ``main``'s tip, and Create Release's tag check - would then fail. On an existing release it calls Update a release with + (``:107-120``, ``src/Releases.ts:76-99,150-174``). With no release, a bump + made while Create Release waits on its approval -- 1.5.0b9 waited three + days, and cron-conda bumped three times meanwhile -- would put + ``v`` on ``main``'s tip, and Create Release's tag check would then + fail. A draft stays a draft (``omitDraftDuringUpdate``, + ``src/Inputs.ts:210-213``) but is retargeted at ``commit``; Create Release's + ``workflow_run`` run finds the draft and resets its target to the run's sha + (``action-gh-release`` 3.0.2's ``src/github.ts:636-646,832-866``), so + skipping a draft is a precaution against retargeting a release Create + Release owns. On an existing release it calls Update a release with ``target_commitish: commit`` (``src/Action.ts:107-120``, ``src/Releases.ts:150-174``), and GitHub's REST docs check "the new value of target_commitish if you are changing it, otherwise the existing target"