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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user