Fix the release pipeline (#1810)

* Resolve the deployed commit after checkout and use that for the artifact lookup instead of github.sha.

* publish_npm.yml builds from the checked-out commit instead of downloading a master artifact

* derive version into files

* Six files that were stale at 3.5.0 are now 3.5.1.
This commit is contained in:
Anshul
2026-09-02 22:01:20 +12:00
committed by GitHub
parent 0d9abcd72a
commit 2e2ed36968
11 changed files with 274 additions and 23 deletions
+11 -2
View File
@@ -27,6 +27,13 @@ jobs:
with:
ref: ${{ (github.event_name == 'workflow_dispatch' && inputs.ref) || (github.event_name == 'workflow_run' && github.event.workflow_run.head_sha) || github.ref }}
# github.sha is the ref the workflow was launched from, which on a manual
# dispatch is not the ref being deployed. Use what checkout landed on so
# the artifact matches the code.
- name: Resolve deployed commit
id: deployed
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Doxygen Action
uses: mattnotmitt/doxygen-action@edge
@@ -37,14 +44,16 @@ jobs:
mv ./docs ./public
mv ./dashboard ./public
# No continue-on-error: a missing artifact means the editor, examples and
# TypeScript docs would be absent, and publishing that silently is worse
# than failing.
- name: Download manifoldCAD editor, examples and documentation
continue-on-error: ${{ github.event_name == 'workflow_dispatch' }}
uses: dawidd6/action-download-artifact@v6
with:
workflow: manifold.yml
workflow_conclusion: success
# Pin to the artifact from the CI run that triggered us
commit: ${{ (github.event_name != 'workflow_run' && github.sha) || '' }}
commit: ${{ (github.event_name != 'workflow_run' && steps.deployed.outputs.sha) || '' }}
run_id: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.id) || '' }}
check_artifacts: true
name: wasmpublic
+64
View File
@@ -0,0 +1,64 @@
name: Prepare release
# Bumps the version everywhere it appears and opens a PR with the result.
# Run this before tagging, so the commit you tag already carries the right
# version. Deriving it here rather than at publish time means what ships
# always corresponds to a real commit in history.
on:
workflow_dispatch:
inputs:
version:
description: 'Release version, without a leading v (e.g. 3.6.0)'
required: true
jobs:
prepare_release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: '3.x'
# Nothing is built or tested here; that all happens on the resulting PR.
# The lockfiles are edited in place rather than regenerated with npm
# install, which keeps this fast and limits the commit to the version.
- name: Set the version
run: python3 scripts/set_version.py "${{ inputs.version }}"
# gh rather than a third-party action; release_file.yml already uses it.
- name: Commit and open a pull request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
set -e
if git diff --quiet; then
echo "Already at $VERSION; nothing to do."
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git checkout -b "release/v$VERSION"
git commit -am "Set version to $VERSION"
git push origin "release/v$VERSION"
# Heredoc so the body is not indented by the YAML block scalar,
# which markdown would render as a code block.
cat > /tmp/pr-body.md <<EOF
Generated by the Prepare release workflow.
Sets the version to \`$VERSION\` across CMakeLists.txt, pyproject.toml,
flake.nix, bindings/wasm/package.json and scripts/test-cmake.sh, and
refreshes the package-lock files.
Once merged, tag the merge commit \`v$VERSION\` and publish the release.
The tagged commit then carries the matching version, which is what
publish_npm reads.
EOF
sed -i 's/^ //' /tmp/pr-body.md
gh pr create --title "Release v$VERSION" --body-file /tmp/pr-body.md
+47 -9
View File
@@ -4,15 +4,23 @@ on:
release:
types: [published]
workflow_dispatch:
inputs:
ref:
description: 'Branch or tag to publish from (e.g. v3.5.2)'
required: true
jobs:
publish_npm:
runs-on: ubuntu-latest
# Matches the WASM build environment in manifold.yml.
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v6
with:
ref: ${{ (github.event_name == 'workflow_dispatch' && inputs.ref) || github.ref }}
- name: Setup Node
uses: actions/setup-node@v6
@@ -23,15 +31,45 @@ jobs:
- name: Update npm # Ensure npm 11.5.1 or later for trusted publishing
run: npm install -g npm@latest
- name: Download built examples
uses: dawidd6/action-download-artifact@v6
- name: Install apt packages
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
with:
workflow: manifold.yml
workflow_conclusion: completed
branch: master
check_artifacts: true
name: wasm
path: ./bindings/wasm/
packages: cmake
version: 1
- name: Setup EMSDK
uses: mymindstorm/setup-emsdk@v14
with:
version: 5.0.2
- uses: ./.github/actions/git-clone
with:
name: tbb
url: https://github.com/oneapi-src/oneTBB.git
tag: v2022.3.0
- uses: ./.github/actions/git-clone
with:
name: gtest
url: https://github.com/google/googletest.git
tag: v1.17.0
# Built here rather than downloaded from a CI artifact, so what ships is
# always the checked-out commit.
- name: Build WASM
run: |
emcmake cmake \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DMANIFOLD_STRICT=ON \
-DMANIFOLD_PAR=OFF \
-DMANIFOLD_USE_BUILTIN_TBB=ON \
-DMANIFOLD_DEBUG=OFF \
-DMANIFOLD_ASSERT=OFF \
-DFETCHCONTENT_SOURCE_DIR_TBB=tbb \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=gtest \
. -B build
cd build
emmake make
- name: Publish to npm
run: |
+2 -5
View File
@@ -3,11 +3,8 @@
1. Verify our three.js [example](https://manifoldcad.org/three) is functional.
1. Verify our model-viewer [example](https://manifoldcad.org/model-viewer) is functional - select Union and Intersection.
1. Verify [make-manifold](https://manifoldcad.org/make-manifold) is functional. Try dropping [DragonAttenuation.glb](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/DragonAttenuation/glTF-Binary/DragonAttenuation.glb) in and verify you can select "View Manifold GLB" and that the dragon is still present while the backdrop is removed. Download the GLB.
1. Make a new branch called the version, e.g. v2.3.0.
1. Use VSCode to search and replace the old version with the new - so far in test-cmake.sh, flake.nix, pyproject.toml, and package.json.
1. Also update CMakeLists.txt version by searching for "set(MANIFOLD_VERSION_".
1. in `bindings/wasm`, run `npm run install:all` to update the package-lock files.
1. Commit, push, open a PR, verify tests pass, manually trigger build_wheels CI, merge.
1. Run the Prepare release workflow from the Actions tab with the new version number. It sets the version in CMakeLists.txt, pyproject.toml, flake.nix, package.json and test-cmake.sh, refreshes the package-lock files, and opens a PR.
1. Verify tests pass on that PR, manually trigger build_wheels CI, merge.
1. On Github, draft a new release, make a new tag with the version number, add release notes, and publish.
1. Check the Actions and verify that both PyPI and npm publishing actions ran successfully.
1. Verify the npm [package](https://www.npmjs.com/package/manifold-3d?activeTab=code) looks good - unpacked size should be close to 1MB.
+1 -1
View File
@@ -27,7 +27,7 @@
},
"../..": {
"name": "manifold-3d",
"version": "3.5.0",
"version": "3.5.1",
"license": "Apache-2.0",
"dependencies": {
"@gltf-transform/core": "^4.2.0",
+1 -1
View File
@@ -23,7 +23,7 @@
},
"../..": {
"name": "manifold-3d",
"version": "3.5.0",
"version": "3.5.1",
"license": "Apache-2.0",
"dependencies": {
"@gltf-transform/core": "^4.2.0",
+1 -1
View File
@@ -21,7 +21,7 @@
},
"../..": {
"name": "manifold-3d",
"version": "3.5.0",
"version": "3.5.1",
"license": "Apache-2.0",
"dependencies": {
"@gltf-transform/core": "^4.2.0",
+1 -1
View File
@@ -20,7 +20,7 @@
},
"../..": {
"name": "manifold-3d",
"version": "3.5.0",
"version": "3.5.1",
"license": "Apache-2.0",
"dependencies": {
"@gltf-transform/core": "^4.2.0",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "manifold-3d",
"version": "3.5.0",
"version": "3.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "manifold-3d",
"version": "3.5.0",
"version": "3.5.1",
"license": "Apache-2.0",
"dependencies": {
"@gltf-transform/core": "^4.2.0",
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Set the release version across every file that carries it.
The version lives in five places in five different formats, and keeping them
in sync by hand is how master ended up on 3.5.1 while v3.5.2 was already
tagged. Run this instead:
python3 scripts/set_version.py 3.6.0
Prints the files it changed. Exits non-zero if any file was already at the
requested version but others were not, since that means a previous bump was
only partly applied.
"""
import argparse
import re
import sys
from pathlib import Path
VERSION_PATTERN = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
def replace_all(path: Path, pattern: re.Pattern, replacement: str) -> bool:
"""Rewrite every match of pattern in path. True if the file changed.
A file with no match at all is an error: it means the format changed and
a version would silently go un-bumped. Lockfiles carry the version more
than once, so every occurrence is replaced rather than just the first.
newline="" on both ends so line endings survive untouched; without it
Python rewrites every line of the file and the diff becomes unreadable.
"""
with open(path, encoding="utf-8", newline="") as f:
original = f.read()
updated, count = pattern.subn(replacement, original)
if count == 0:
raise SystemExit(
f"{path}: no match for {pattern.pattern!r}. "
"The file's format has changed; update this script."
)
if updated == original:
return False
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(updated)
return True
def set_version(root: Path, major: str, minor: str, patch: str) -> list[Path]:
dotted = f"{major}.{minor}.{patch}"
edits = [
(
Path("bindings/wasm/package.json"),
re.compile(r'("version":\s*")\d+\.\d+\.\d+(")'),
rf"\g<1>{dotted}\g<2>",
),
(
Path("pyproject.toml"),
re.compile(r'(?m)^(version\s*=\s*")\d+\.\d+\.\d+(")'),
rf"\g<1>{dotted}\g<2>",
),
(
Path("flake.nix"),
re.compile(r'(manifold-version\s*=\s*")\d+\.\d+\.\d+(")'),
rf"\g<1>{dotted}\g<2>",
),
(
Path("CMakeLists.txt"),
re.compile(
r"(set\(MANIFOLD_VERSION_MAJOR )\d+(\)\s*\n"
r"set\(MANIFOLD_VERSION_MINOR )\d+(\)\s*\n"
r"set\(MANIFOLD_VERSION_PATCH )\d+(\))"
),
rf"\g<1>{major}\g<2>{minor}\g<3>{patch}\g<4>",
),
# scripts/test-cmake.sh carries the version twice: once as the
# find_package() minimum and once as the MANIFOLD_VERSION_NUMBER check.
# Both track the release so there is only ever one version in play.
(
Path("scripts/test-cmake.sh"),
re.compile(r'(find_package\(manifold ")\d+\.\d+\.\d+(")'),
rf"\g<1>{dotted}\g<2>",
),
(
Path("scripts/test-cmake.sh"),
re.compile(r"(MANIFOLD_VERSION_NUMBER\()\d+,\s*\d+,\s*\d+(\))"),
rf"\g<1>{major}, {minor}, {patch}\g<2>",
),
]
# The lockfiles embed the version in their manifold-3d entries. Editing
# them directly rather than running npm install keeps the release commit
# to just the version change, and keeps the workflow fast.
lock_pattern = re.compile(
r'("name":\s*"manifold-3d",\s*\n\s*"version":\s*")\d+\.\d+\.\d+(")'
)
# Globbed non-recursively; "**" would descend into node_modules.
lockfiles = [Path("bindings/wasm/package-lock.json")] + sorted(
p.relative_to(root)
for p in root.glob("bindings/wasm/examples/*/package-lock.json")
)
for lockfile in lockfiles:
edits.append((lockfile, lock_pattern, rf"\g<1>{dotted}\g<2>"))
changed = []
for relative, pattern, replacement in edits:
path = root / relative
if not path.exists():
raise SystemExit(f"{relative}: not found. Has it moved?")
if replace_all(path, pattern, replacement) and relative not in changed:
changed.append(relative)
return changed
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("version", help="release version, e.g. 3.6.0")
parser.add_argument(
"--root",
type=Path,
default=Path(__file__).resolve().parent.parent,
help="repository root (defaults to this script's parent directory)",
)
args = parser.parse_args()
match = VERSION_PATTERN.match(args.version)
if not match:
raise SystemExit(
f"'{args.version}' is not a MAJOR.MINOR.PATCH version. "
"Strip any leading 'v'."
)
changed = set_version(args.root, *match.groups())
if not changed:
print(f"Already at {args.version}; nothing to do.")
return 0
print(f"Set version to {args.version} in:")
for path in changed:
print(f" {path.as_posix()}")
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -5,7 +5,7 @@ cd cmake-consumer
cat <<EOT > CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(testing LANGUAGES CXX)
find_package(manifold "3.5.0" REQUIRED)
find_package(manifold "3.5.1" REQUIRED)
add_executable(testing test.cpp)
target_link_libraries(testing PRIVATE manifold::manifold)
EOT