diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3242c33854..81af7059d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: @@ -34,7 +21,6 @@ on: jobs: cli-smoke: - if: inputs.package == 'cli' || inputs.package == 'all' runs-on: macos-latest steps: - uses: actions/checkout@v4 @@ -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: @@ -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 @@ -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 @@ -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: | @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 @@ -354,35 +320,10 @@ 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 - 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" @@ -390,16 +331,24 @@ jobs: 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 }} diff --git a/package.json b/package.json index 11827fd0b1..04470f7ae0 100644 --- a/package.json +++ b/package.json @@ -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",