From f7897a269cc3ad5373fe202f41497d6d1936cd05 Mon Sep 17 00:00:00 2001 From: Oluwatosin Amokeodo Date: Fri, 6 Nov 2026 13:41:39 +0000 Subject: [PATCH] release: render the extension's icon before packaging it, and recover a missed release page The 0.7.0 release published to PyPI, confirmed the digests, then stopped at 'create the GitHub release': vsce refused to package editors/vscode, whose package.json names images/icon.png. The PNG is rendered from images/icon.svg at build time and never committed (31473955); the extension's Dockerfile renders it, the release step did not. The release page for 0.7.0 was created by hand from the run's own dist artefact (27 files, each matching PyPI's digest) and a .vsix built from the tagged source. - The release step renders the icon with rsvg-convert, as the Dockerfile does. - The Dockerfile packages with vsce 3.6.0, the version the release uses (it had 4.0.0). - 'create the GitHub release' also runs on the second run of a tag once PyPI has the version (publish skipped, confirm passed), so a missed page is created then; !cancelled() keeps the skipped publish from skipping it. - Tests: every file the manifest names is committed or rendered before vsce; one vsce version; the recovery condition. All three fail on main. --- .cordon-guard.sha256 | 2 +- .github/workflows/release.yml | 14 +++++- editors/vscode/Dockerfile | 2 +- .../release/test_release_artefacts.py | 43 +++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/.cordon-guard.sha256 b/.cordon-guard.sha256 index a49162055..7f53052fc 100644 --- a/.cordon-guard.sha256 +++ b/.cordon-guard.sha256 @@ -10,7 +10,7 @@ 8f18558b9b0f22dbafd357a754153b9bfa1a062c225d31e6e968faf7174b4800 .github/workflows/refresh-advisories.yml 780f65837105fbf623728041037e989b8e1fcc72e8833fae7165fcc1b1fc7259 .github/workflows/refresh-iac-policies.yml 5558889702d2cb8eeccdc749ca71523049258ceec6370c900cfe88e6891ab05f .github/workflows/refresh-intel.yml -dea3355632950d45a1e8be75bf5927925e684daac0fc051ab31ea501e8b4271c .github/workflows/release.yml +eccf8d9dd77ed2023968ea91ad34faab2cd6f0a0a409ce882b23ca8250e8a89b .github/workflows/release.yml e5274385e6ab7ef1e65b0de1eba419e21c5d745d9436d10c994ec7d184f9db10 .github/workflows/tests-linux.yml 3fb78c694f2444babd21eaa2201b645285dce3e5d89dc30f17e88b45faf61cc8 .github/workflows/verify-refresh.yml 0c724d7f65652eed6bcab9a3e3c5cb9559b5c63cb998728c81c65d71e05d2ac5 cordon-history-baseline.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69a3f7f8f..eb4e3407e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -549,10 +549,14 @@ jobs: needs: [build, publish, confirm] # Only for a real tag push. A `workflow_dispatch` verification run must not # create a release object for a version that already exists. + # Also on the second run a tag push makes once PyPI has the version (publish skipped, confirm + # passed): a release page this job failed to create the first time is created then, and an + # existing one only has its notes refreshed. if: >- - startsWith(github.ref, 'refs/tags/v') + !cancelled() && startsWith(github.ref, 'refs/tags/v') && needs.build.result == 'success' - && needs.publish.result == 'success' + && (needs.publish.result == 'success' + || (needs.publish.result == 'skipped' && needs.confirm.result == 'success')) runs-on: ubuntu-latest permissions: contents: write @@ -572,8 +576,14 @@ jobs: # Built from the tagged source, tested, and attached beside the wheel; the editor # extension and the scanner it runs are released together and carry one version. working-directory: editors/vscode + # The marketplace icon is a PNG rendered from images/icon.svg, never committed: rendered + # here as editors/vscode/Dockerfile renders it. Without it vsce refuses to package, which + # stopped the 0.7.0 release here after PyPI had published (6 Nov). run: | set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -qq -y --no-install-recommends librsvg2-bin + rsvg-convert -w 256 -h 256 images/icon.svg -o images/icon.png npm ci --no-audit --no-fund npm test npx --yes @vscode/vsce@3.6.0 package --no-dependencies -o "../../artefacts/cordon-${GITHUB_REF_NAME#v}.vsix" diff --git a/editors/vscode/Dockerfile b/editors/vscode/Dockerfile index 5da0e6bd8..1d9bb6222 100644 --- a/editors/vscode/Dockerfile +++ b/editors/vscode/Dockerfile @@ -8,7 +8,7 @@ COPY . . RUN rsvg-convert -w 256 -h 256 images/icon.svg -o images/icon.png \ && npm ci --ignore-scripts \ && npm test \ - && npx --yes @vscode/vsce@4.0.0 package --no-dependencies -o /build/cordon.vsix + && npx --yes @vscode/vsce@3.6.0 package --no-dependencies -o /build/cordon.vsix FROM scratch COPY --from=build /build/cordon.vsix / diff --git a/tests/unit/repository/release/test_release_artefacts.py b/tests/unit/repository/release/test_release_artefacts.py index ff6f8e4c2..d2dafbbcf 100644 --- a/tests/unit/repository/release/test_release_artefacts.py +++ b/tests/unit/repository/release/test_release_artefacts.py @@ -278,6 +278,49 @@ def test_the_action_pin_is_still_generated(self, step: str) -> None: assert step in self.body() +class TestTheReleasePackagesTheExtension: + """The 0.7.0 release published to PyPI, then stopped creating its GitHub release: vsce refused + to package an extension whose icon, images/icon.png, is rendered at build time and was not + rendered in the release step (6 Nov).""" + + EXTENSION = ROOT / "editors" / "vscode" + + @staticmethod + def packaging_step() -> str: + text = RELEASE.read_text(encoding="utf-8") + start = text.index("- name: Build the VS Code extension") + return text[start : text.index("- name:", start + 1)] + + def test_every_file_the_manifest_names_is_committed_or_rendered_first(self) -> None: + manifest = json.loads((self.EXTENSION / "package.json").read_text(encoding="utf-8")) + step = self.packaging_step() + named = [manifest["icon"]] if "icon" in manifest else [] + for path in named: + if (self.EXTENSION / path).is_file(): + continue + assert f"-o {path}" in step, ( + f"{path} is neither committed nor rendered before packaging" + ) + assert step.index(f"-o {path}") < step.index("vsce@"), f"{path} is rendered after vsce" + + def test_the_release_and_the_dockerfile_package_with_one_vsce(self) -> None: + import re + + dockerfile = (self.EXTENSION / "Dockerfile").read_text(encoding="utf-8") + versions = set(re.findall(r"@vscode/vsce@([0-9.]+)", RELEASE.read_text(encoding="utf-8"))) + assert versions == set(re.findall(r"@vscode/vsce@([0-9.]+)", dockerfile)), versions + + def test_the_release_page_is_created_on_the_run_after_publishing_too(self) -> None: + text = RELEASE.read_text(encoding="utf-8") + job = text[ + text.index(" github-release:") : text.index( + " steps:", text.index(" github-release:") + ) + ] + assert "!cancelled()" in job, "a skipped publish would skip the job without it" + assert "needs.publish.result == 'skipped' && needs.confirm.result == 'success'" in job + + class TestReleaseWorkflowPublishesTheImage: """The Dockerfile existed and nothing built or pushed it -- README called this out explicitly ("no published container image yet"). These assert