Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 56 additions & 107 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,8 @@ name: Release
on:
workflow_dispatch:
inputs:
package:
description: "Package to release"
required: true
type: choice
options:
- core
- viewer
- editor
- nodes
- mcp
- ifc-converter
- cli
- all
bump:
description: "Version bump (beta publishes a prerelease on the beta dist-tag; patch/minor/major on a prerelease graduates it to its base version on latest)"
description: "Version bump applied to every @pascal-app package at once, from the highest version in the tree (beta publishes a prerelease on the beta dist-tag; patch/minor/major on a prerelease graduates it to its base version on latest; none republishes the current version)"
required: true
type: choice
options:
Expand All @@ -34,7 +21,6 @@ on:

jobs:
cli-smoke:
if: inputs.package == 'cli' || inputs.package == 'all'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -62,7 +48,6 @@ jobs:

release:
needs: cli-smoke
if: ${{ !cancelled() && (needs.cli-smoke.result == 'success' || needs.cli-smoke.result == 'skipped') }}
runs-on: ubuntu-latest
environment: npm
permissions:
Expand Down Expand Up @@ -116,7 +101,7 @@ jobs:
- name: Bump versions and sync inter-package references
run: |
BUMP=${{ inputs.bump }}
TARGET=${{ inputs.package }}
PACKAGES="core viewer editor nodes mcp ifc-converter cli"

bump_version() {
local v=$1
Expand Down Expand Up @@ -152,32 +137,50 @@ jobs:
echo "NPM_TAG=latest" >> "$GITHUB_ENV"
fi

# Track new versions in shell-local vars.
# NOTE: $GITHUB_ENV writes don't surface within the same step, so the
# peerDeps sync below must use shell vars, not env indirection.
declare -A NEW_VERSIONS

for pkg in core viewer editor nodes mcp ifc-converter cli; do
if [ "$TARGET" = "$pkg" ] || [ "$TARGET" = "all" ]; then
CUR=$(jq -r '.version' packages/$pkg/package.json)
NEW=$(bump_version "$CUR")
jq --arg v "$NEW" '.version = $v' packages/$pkg/package.json > tmp.json && mv tmp.json packages/$pkg/package.json
NEW_VERSIONS[$pkg]=$NEW
UPPER=$(echo "$pkg" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
# Also export for the publish/commit/tag steps that follow.
echo "${UPPER}_VERSION=$NEW" >> $GITHUB_ENV
echo "Bumped @pascal-app/$pkg: $CUR → $NEW"
# Every @pascal-app package ships at the same version. The next one is
# computed once, from the highest version currently in the tree, so a
# package that ran ahead (a cli-only hotfix) pulls the others up to it
# instead of being republished under an older number.
version_key() {
# Fixed-width key that sorts as a string: MAJOR MINOR PATCH then a
# stable flag, so 1.0.0 outranks every 1.0.0-beta.N.
local v=$1
if [[ "$v" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)-beta\.([0-9]+)$ ]]; then
printf '%08d%08d%08d0%08d\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}"
else
IFS='.' read -r MAJ MIN PAT <<< "$v"
printf '%08d%08d%08d1%08d\n' "$MAJ" "$MIN" "$PAT" 0
fi
}
BASE=""
BASE_KEY=""
for pkg in $PACKAGES; do
CUR=$(jq -r '.version' packages/$pkg/package.json)
KEY=$(version_key "$CUR")
if [ -z "$BASE" ] || [[ "$KEY" > "$BASE_KEY" ]]; then
BASE=$CUR
BASE_KEY=$KEY
fi
done
NEW=$(bump_version "$BASE")
echo "RELEASE_VERSION=$NEW" >> "$GITHUB_ENV"
echo "Releasing every @pascal-app package at $NEW (highest version in the tree: $BASE)"

for pkg in $PACKAGES; do
CUR=$(jq -r '.version' packages/$pkg/package.json)
jq --arg v "$NEW" '.version = $v' packages/$pkg/package.json > tmp.json && mv tmp.json packages/$pkg/package.json
UPPER=$(echo "$pkg" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
# Also export for the publish/commit/tag steps that follow.
echo "${UPPER}_VERSION=$NEW" >> $GITHUB_ENV
echo "Bumped @pascal-app/$pkg: $CUR → $NEW"
done

# Sync inter-package references in dependencies, peerDependencies, and devDependencies.
# Anything that references a bumped @pascal-app/* package is updated to ^NEW.
for pkg in core viewer editor nodes mcp ifc-converter cli; do
# Every @pascal-app/* range becomes ^NEW.
for pkg in $PACKAGES; do
FILE=packages/$pkg/package.json
for dep in core viewer editor nodes mcp ifc-converter cli; do
VAL="${NEW_VERSIONS[$dep]}"
[ -z "$VAL" ] && continue
jq --arg name "@pascal-app/$dep" --arg v "^$VAL" '
for dep in $PACKAGES; do
jq --arg name "@pascal-app/$dep" --arg v "^$NEW" '
if .dependencies[$name] then .dependencies[$name] = $v else . end
| if .peerDependencies[$name] then .peerDependencies[$name] = $v else . end
| if .devDependencies[$name] then .devDependencies[$name] = $v else . end
Expand All @@ -195,37 +198,7 @@ jobs:
# Refresh the lockfile so the release commit remains reproducible.
bun install

# A single-package release may depend on another package introduced
# by this monorepo. Refuse to publish an uninstallable package when
# that dependency is not part of this run and is absent from npm.
RELEASE_PACKAGES="core viewer editor nodes mcp ifc-converter cli"
if [ "$TARGET" != "all" ]; then
FILE="packages/$TARGET/package.json"
while IFS=$'\t' read -r DEP RANGE; do
SLUG="${DEP#@pascal-app/}"
case " $RELEASE_PACKAGES " in
*" $SLUG "*)
if ! npm view "$DEP@$RANGE" version >/dev/null 2>&1; then
echo "Missing required published dependency: $DEP@$RANGE"
echo "Release $SLUG first or use the all-package release."
exit 1
fi
;;
esac
done < <(
jq -r '
[(.dependencies // {}), (.peerDependencies // {})]
| add
| to_entries[]
| select(.key | startswith("@pascal-app/"))
| [.key, .value]
| @tsv
' "$FILE"
)
fi

- name: Validate portable editor runtime
if: inputs.package == 'cli' || inputs.package == 'all'
env:
PASCAL_PORTABLE_BUILD: "1"
run: |
Expand All @@ -236,7 +209,6 @@ jobs:
bun run smoke-runtime

- name: Build & publish core
if: inputs.package == 'core' || inputs.package == 'all'
working-directory: packages/core
run: |
bun run build
Expand All @@ -251,7 +223,6 @@ jobs:
fi

- name: Build & publish viewer
if: inputs.package == 'viewer' || inputs.package == 'all'
working-directory: packages/viewer
run: |
bun run build
Expand All @@ -266,7 +237,6 @@ jobs:
fi

- name: Publish editor
if: inputs.package == 'editor' || inputs.package == 'all'
working-directory: packages/editor
run: |
if [ "${{ inputs.dry-run }}" = "true" ]; then
Expand All @@ -280,7 +250,6 @@ jobs:
fi

- name: Build & publish nodes
if: inputs.package == 'nodes' || inputs.package == 'all'
working-directory: packages/nodes
run: |
bun run build
Expand All @@ -295,7 +264,6 @@ jobs:
fi

- name: Build & publish mcp
if: inputs.package == 'mcp' || inputs.package == 'all'
working-directory: packages/mcp
run: |
bun run build
Expand All @@ -310,7 +278,6 @@ jobs:
fi

- name: Build & publish ifc-converter
if: inputs.package == 'ifc-converter' || inputs.package == 'all'
run: |
# ifc-converter depends on @pascal-app/core (workspace) — build it first
bun run build --filter @pascal-app/core 2>/dev/null || (cd packages/core && bun run build)
Expand All @@ -327,7 +294,6 @@ jobs:
fi

- name: Publish CLI
if: inputs.package == 'cli' || inputs.package == 'all'
# Keep token auth unset so npm can exchange the GitHub OIDC identity.
run: |
cd packages/cli
Expand All @@ -354,52 +320,35 @@ jobs:
git add -A
PKGS=""
TAGS=""

if [ -n "$CORE_VERSION" ]; then
PKGS="$PKGS @pascal-app/core@$CORE_VERSION"
TAGS="$TAGS @pascal-app/core@$CORE_VERSION"
fi
if [ -n "$VIEWER_VERSION" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recovery overwrites published CLI runtime

High Severity

none recovery (and any re-run that skips CLI publish) still rebuilds the portable runtime and --clobbers the GitHub Release archive for the already-published CLI version. The npm package keeps the original digest in runtime-source.json, so later downloads fail verification and the editor cannot start.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2486ced. Configure here.

PKGS="$PKGS @pascal-app/viewer@$VIEWER_VERSION"
TAGS="$TAGS @pascal-app/viewer@$VIEWER_VERSION"
fi
if [ -n "$EDITOR_VERSION" ]; then
PKGS="$PKGS @pascal-app/editor@$EDITOR_VERSION"
TAGS="$TAGS @pascal-app/editor@$EDITOR_VERSION"
fi
if [ -n "$NODES_VERSION" ]; then
PKGS="$PKGS @pascal-app/nodes@$NODES_VERSION"
TAGS="$TAGS @pascal-app/nodes@$NODES_VERSION"
fi
if [ -n "$MCP_VERSION" ]; then
PKGS="$PKGS @pascal-app/mcp@$MCP_VERSION"
TAGS="$TAGS @pascal-app/mcp@$MCP_VERSION"
fi
if [ -n "$IFC_CONVERTER_VERSION" ]; then
PKGS="$PKGS @pascal-app/ifc-converter@$IFC_CONVERTER_VERSION"
TAGS="$TAGS @pascal-app/ifc-converter@$IFC_CONVERTER_VERSION"
fi
if [ -n "$CLI_VERSION" ]; then
PKGS="$PKGS @pascal-app/cli@$CLI_VERSION"
TAGS="$TAGS @pascal-app/cli@$CLI_VERSION"
fi
for pkg in core viewer editor nodes mcp ifc-converter cli; do
PKGS="$PKGS @pascal-app/$pkg@$RELEASE_VERSION"
TAGS="$TAGS @pascal-app/$pkg@$RELEASE_VERSION"
done

if git diff --cached --quiet; then
echo "No version-file changes; tagging the current release commit"
else
git commit -m "release:${PKGS}"
fi

# A `none` run republishes the current version: a package that was
# already released at it keeps its tag, the rest are tagged now.
NEW_TAGS=""
for TAG in $TAGS; do
git tag "$TAG"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "Tag $TAG already exists; keeping it"
else
git tag "$TAG"
NEW_TAGS="$NEW_TAGS $TAG"
fi
done

git push --atomic origin HEAD:main $TAGS
git push --atomic origin HEAD:main $NEW_TAGS

# The npm package points at this asset, so it is uploaded in the same job, immediately
# after the tag it hangs off exists on the remote.
- name: Upload the CLI web runtime release asset
if: ${{ inputs.dry-run == false && (inputs.package == 'cli' || inputs.package == 'all') }}
if: inputs.dry-run == false
working-directory: packages/cli
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
14 changes: 4 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,10 @@
"kill": "lsof -ti:3002 | xargs kill -9 2>/dev/null || echo 'No processes found on port 3002'",
"clean:cache": "rm -rf apps/*/.next apps/*/.swc apps/*/.turbo packages/*/.turbo tooling/*/.turbo .turbo node_modules/.cache",
"restart": "bun kill && bun clean:cache && bun dev",
"release": "gh workflow run release.yml -f package=all -f bump=patch",
"release:beta": "gh workflow run release.yml -f package=all -f bump=beta",
"release:viewer": "gh workflow run release.yml -f package=viewer -f bump=patch",
"release:core": "gh workflow run release.yml -f package=core -f bump=patch",
"release:editor": "gh workflow run release.yml -f package=editor -f bump=patch",
"release:nodes": "gh workflow run release.yml -f package=nodes -f bump=patch",
"release:mcp": "gh workflow run release.yml -f package=mcp -f bump=patch",
"release:cli": "gh workflow run release.yml -f package=cli -f bump=patch",
"release:minor": "gh workflow run release.yml -f package=all -f bump=minor",
"release:major": "gh workflow run release.yml -f package=all -f bump=major"
"release": "gh workflow run release.yml -f bump=patch",
"release:beta": "gh workflow run release.yml -f bump=beta",
"release:minor": "gh workflow run release.yml -f bump=minor",
"release:major": "gh workflow run release.yml -f bump=major"
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
Expand Down
Loading