Add v1.0.0 release automation (build-verify-publish pipeline, no publish yet) - #2
Merged
Merged
Conversation
…d pipeline Adds .github/workflows/release.yml, a fail-closed release pipeline that never publishes anything unless triggered by a pushed vX.Y.Z tag: - workflow_dispatch builds and verifies everything but never creates a GitHub Release or publishes to PyPI; the guard on both publishing jobs is a positive allow-list (`event_name == 'push' && ref starts with refs/tags/v`), not a workflow_dispatch exclusion, so there is no path by which a manual run can publish. - scripts/verify_release_versions.py cross-checks the Git tag against blender_mobile_3d/version.py, bl_info, blender_manifest.toml, installers/npm/package.json, installers/python/pyproject.toml, and (post-build) release-manifest.json, collecting every mismatch rather than stopping at the first, and fails closed on any of them. It also verifies the tag commit is reachable from origin/main via `git merge-base --is-ancestor`, refusing to release from an untrusted ref. - Fixed a real bug found while wiring this up: blender_manifest.toml had no `[project]` table, so `version`/`name`/etc. were silently nested under `[build-system]` per TOML rules and unreadable as top-level metadata. release_artifacts.py now imports VERSION from blender_mobile_3d/version.py instead of hardcoding it, removing one entire class of drift between the source of truth and the manifest it produces. - Build job produces the canonical Blender ZIP + SHA-256 + release manifest, the Python wheel + sdist, and the npm tarball from a fresh checkout of the tag; verifies twine, the ZIP checksum, the release-manifest schema, and npm tarball contents (rejects test/ in the package, requires bin/src/README/LICENSE/package.json); uploads everything as one artifact set with a 14-day retention that downstream jobs (blender-regression, github-release, publish-pypi) all consume, so every job builds from the same checked-out tag rather than trusting a prior run's output. - Blender headless regression job downloads those artifacts and runs the core add-on suite plus the full installer E2E lifecycle (install -> no-op update -> uninstall guard -> uninstall -> confirmed absence) for both the wheel and the npm tarball against real Blender. - GitHub Release step uses `gh release create` with the ambient GITHUB_TOKEN (no third-party release action), attaching the ZIP, checksum, manifest, wheel, sdist, and npm tarball with generated notes. - PyPI publish uses pypa/gh-action-pypi-publish pinned to a commit SHA, the `pypi` environment, and `id-token: write` only — no password or long-lived token anywhere in the workflow. - npm publishing is intentionally not part of this workflow: the first publish of a new scoped package requires interactive 2FA and Trusted Publishing can't be configured until the package exists. docs/RELEASE.md documents the exact order (merge -> configure PyPI pending publisher -> workflow_dispatch dry run -> tag -> verify release -> verify PyPI install -> manual first npm publish with 2FA -> configure npm Trusted Publishing -> verify clean installs), plus yank/deprecate/rollback procedures for all three surfaces. - Every third-party action (checkout, setup-python, setup-node, upload-artifact, download-artifact, gh-action-pypi-publish) is pinned to a full commit SHA with a version comment; every job has an explicit timeout and least-privilege permissions; the workflow has a concurrency group so overlapping release runs don't race. - tests/unit/test_release_workflow.py structurally validates the above (47 tests: permissions, triggers, pinned SHAs shared with ci.yml, publish guards, job dependencies, environment config, no continue-on-error/`|| true`, no hardcoded credentials) without needing to execute the workflow. tests/unit/test_verify_release_versions.py covers the version-agreement script directly (14 tests). Locally verified before committing: full pytest suite (280 tests, 96.6% combined branch coverage), ruff format/lint/security, mypy, 65 Node tests + ESLint, actionlint on both workflow files, a fresh rebuild of the Blender ZIP/wheel/sdist/npm tarball with the exact build-job commands, the npm package content inspection, and a full installer E2E run against real Blender 5.2.0 using the freshly built wheel. Nothing was tagged, released, or published to npm/PyPI.
ci.yml's packaging-python job checks out with actions/checkout's default (shallow, depth-1) history. In that state, `git rev-list --max-parents=0 HEAD` returns HEAD itself (the shallow boundary is synthesized as parentless), so the test's "root commit" collapsed to HEAD, and "is HEAD an ancestor of HEAD" is trivially true -- the opposite of what the test asserted. Caught by CI, not locally, since this dev clone has full history. Skip the test when the checkout is shallow (git rev-parse --is-shallow-repository) rather than asserting something a depth-1 clone cannot actually exercise; verified the skip fires in a real forced shallow clone (git clone --depth 1 file://...) and that the fix resolves the failure there while all other tests remain unaffected.
…der archive, fix docs Four fixes requested in PR #2 review, all verified locally before commit. 1. Node/npm toolchain pinning - Replaced `node-version: lts/*` with the shared `env.NODE_VERSION` ("22.14.0") in test-node, build, and blender-regression. - Added an explicit `npm install -g npm@${{ env.NPM_VERSION }}` ("11.5.1") step to each of those three jobs, plus a step printing `node --version` / `npm --version`. - Verified locally: `npm ci`, `npm run lint`, `npm test` (65 tests), and `npm pack` + tarball content inspection all pass under this exact pinned Node 22.14.0 / npm 11.5.1 combination. 2. Pinned Python release tooling - Added scripts/release-requirements.txt: exact `==` pins for build==1.5.0, twine==6.2.0, wheel==0.47.0, setuptools==83.0.0, kept separate from pyproject.toml's floating `dev` extras. - build job now installs via `-r scripts/release-requirements.txt` instead of unpinned `pip install build twine`. - Switched to `python -m build --no-isolation`, otherwise PEP 517's isolated build environment would silently re-resolve its own setuptools/wheel and the pin would have no effect. Verified this locally: the built wheel's dist-info/WHEEL records "Generator: setuptools (83.0.0)", confirming the pinned version is what actually builds the artifact, not a fresh isolated resolution. - `twine check` on the resulting wheel/sdist: PASSED. 3. Blender binary verification before execution - Independently confirmed the SHA-256 for blender-4.3.2-linux-x64.tar.xz two ways: against Blender's own published download.blender.org/release/Blender4.3/blender-4.3.2.sha256 manifest, and by downloading the archive separately and hashing it locally (sha256sum and, as a second algorithm, md5sum) -- both matched exactly: 4da1c956673c0485e63054e563ee69198cc8f80d8157dd7592dffc8a6a5592e6 - Pinned that digest directly in the workflow (BLENDER_TARBALL_SHA256 env var on the download step) rather than re-fetching the .sha256 manifest from the same server at release time, which would verify the archive against itself with no independent guarantee. - `sha256sum -c` now runs immediately after download and before `tar -xf`; `set -euo pipefail` means a mismatch aborts the step before extraction. Verified locally against both the real archive (passes) and a deliberately tampered copy (fails closed, nonzero exit, archive not extracted). 4. Corrected workflow_dispatch documentation - docs/RELEASE.md previously implied `workflow_dispatch` could be tried "against the release branch, before merging" -- incorrect: GitHub only exposes manual dispatch once the trigger exists on the default branch, full stop, regardless of UI vs `gh` CLI vs REST API. - Restructured the release order into the exact required sequence: merge PR #2 -> configure the `pypi` GitHub environment -> configure the PyPI pending trusted publisher -> run workflow_dispatch on main -> confirm publish jobs were skipped -> only then create the v1.0.0 tag (steps 7-11 renumbered accordingly, internal cross-references and anchors fixed). 5. Regression tests (tests/unit/test_release_workflow.py, +21 tests) Fail if any of the following regress: a mutable Node selector (lts/*, lts/current, latest, current) reappears on a node-version line; npm isn't explicitly pinned via env.NPM_VERSION in any of the three jobs; node/npm versions aren't printed; release-requirements.txt is missing or contains anything other than exact `==` pins for build/twine/wheel/setuptools; the old unpinned `pip install build twine` pattern reappears; `--no-isolation` is dropped from the wheel build; the Blender checksum env var is absent or not a valid 64-hex SHA-256; the archive is extracted before `sha256sum -c` runs; the checksum manifest is re-fetched from download.blender.org at runtime; or docs/RELEASE.md claims a pre-merge dispatch path is possible / omits the default-branch requirement / has its step order out of sequence. Preserved without modification: positive tag-only publishing guards, fetch-depth: 0 for tag reachability, exact semver verification via verify_release_versions.py, least-privilege permissions, pinned action SHAs, artifact reuse between jobs (single build -> multiple consumers), the 90% branch coverage gate, the github-release -> publish-pypi dependency, and the prohibition on npm publishing in this workflow. Locally verified before committing: full pytest suite (301 tests, 96.6% combined branch coverage), the expanded release-workflow structural suite (82 tests total across both test files), ruff format/lint/security, mypy, 65 Node tests + ESLint (pinned toolchain), actionlint on both workflow files, and a from-scratch local rebuild of every release artifact (Blender ZIP + checksum + manifest, Python wheel/sdist via the pinned tooling, npm tarball) with checksum verification, twine check, and npm package content inspection all passing. Nothing was tagged, released, or published to npm/PyPI.
SSobol77
marked this pull request as ready for review
July 19, 2026 21:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release automation — ready for review until the release order in docs/RELEASE.md is followed. Does not tag, release, or publish anything.
Summary
Adds
.github/workflows/release.yml: a fail-closed, version-gated release pipeline forv1.0.0and beyond.workflow_dispatchbuilds every artifact and runs every verification step, but never creates a GitHub Release and never publishes to PyPI.v*.*.*runs the same build/verification and, only if everything succeeds, creates the GitHub Release and publishes to PyPI via Trusted Publishing (OIDC, no token).github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')), not a workflow_dispatch exclusion — there is no code path by which a manual dispatch can publish.What's included
scripts/verify_release_versions.py— cross-checks the Git tag againstblender_mobile_3d/version.py,bl_info,blender_manifest.toml,installers/npm/package.json,installers/python/pyproject.toml, and (post-build)release-manifest.json. Collects every mismatch instead of stopping at the first, and fails closed. Also verifies the tag commit is reachable fromorigin/mainviagit merge-base --is-ancestor.blender_manifest.tomlhad no[project]table, soversion/name/etc. were silently nested under[build-system]per TOML's own rules — unreadable as top-level metadata. Fixed the file structure;scripts/release_artifacts.pynow importsVERSIONfromblender_mobile_3d/version.pyinstead of hardcoding it, closing one whole class of version drift.twine check, the ZIP checksum, the release-manifest schema, and npm tarball contents (rejectstest/in the package; requiresbin/,src/,README.md,LICENSE,package.json). Uploads one artifact set (14-day retention) that every downstream job consumes, so nothing is rebuilt inconsistently.gh release createwith the ambientGITHUB_TOKEN— no third-party release action — attaching the ZIP, checksum, manifest, wheel, sdist, and npm tarball, with generated release notes.pypa/gh-action-pypi-publishpinned to a commit SHA,environment: pypi,permissions: id-token: writeonly. No password, no long-lived API token anywhere in the workflow.docs/RELEASE.mddocuments the exact order: merge → configure PyPI pending publisher →workflow_dispatchdry run → create thev1.0.0tag → verify the GitHub Release → verify PyPI install → manually publish the first npm package with 2FA → configure npm Trusted Publishing for future versions → verify clean installs from both registries — plus yank/deprecate/rollback procedures for GitHub Releases, PyPI, and npm.checkout,setup-python,setup-node,upload-artifact,download-artifact,gh-action-pypi-publish) is pinned to a full commit SHA with a version comment. Every job has an explicit timeout and least-privilege permissions. The workflow has a concurrency group so overlapping release runs can't race.tests/unit/test_release_workflow.py(68 tests) structurally validates all of the above — permissions, triggers, pinned SHAs (shared check also coversci.yml), publish guards, job dependencies, environment config, absence ofcontinue-on-error/|| true, absence of hardcoded credentials — without needing to execute the workflow on GitHub Actions.tests/unit/test_verify_release_versions.py(14 tests) covers the version-agreement script directly: matching/mismatching versions across every file, malformed tag formats, release-manifest agreement, and tag-reachability pass/fail using the repo's own root commit as a guaranteed-unreachable fixture.Verification (all done locally before committing)
blender_mobile_3d+blender_mobile_3d_installer(gate is ≥90%).ruff format --check,ruff check(full +--select Ssecurity subset),mypy— all clean.node --test) + ESLint — clean.actionlinton bothrelease.ymlandci.yml— zero findings.buildjob runs; rantwine check, the SHA-256 checksum verification, and the npm package-content inspection logic — all pass.workflow_dispatchcannot publish: both by static structural test (test_workflow_dispatch_cannot_reach_publishing_jobs) and by manual trace of theif:conditions.os.system,shell=True,|| true,continue-on-error— none found. Onlysecrets.GITHUB_TOKEN(ephemeral, built-in) andid-token: write(OIDC permission, not a credential) appear anywhere in the workflow.CI: 300 passed, 1 skipped, 96.62% coverage
HEAD: 859bc0b
What still requires GitHub Actions (cannot be verified locally)
workflow_dispatchwas triggered and no tag was pushed as part of this PR, per the task's explicit instruction not to publish or tag anything.ci.ymlwill run automatically on this PR's push/pull_request events (unrelated torelease.yml, which only triggers onworkflow_dispatchor a version tag).pypienvironment and pending trusted publisher to be configured first — seedocs/RELEASE.mdstep 2).gh release create/pypa/gh-action-pypi-publishsteps, which only run on a real tag push.Explicitly not done in this PR
npm publishstep exists inrelease.yml(intentional — see above).