Package Preview Publish #1654
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
| name: Package Preview Publish | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| # This workflow deliberately does NOT run the Wix gateway proxy. | |
| # | |
| # secplatform's interim policy for OSS repos (Dima Ryskin): use embargo for | |
| # non-publish tasks, and protect publish tasks with an enforced lockfile plus a | |
| # package manager honoring a minimal-age directive — here `minimumReleaseAge` in | |
| # bunfig.toml. The gateway cannot carry a publish today: `npm publish` sends | |
| # `PUT /<package>`, which matches neither its `^~ /-/` passthrough block nor | |
| # `~ \.tgz$` and so lands in `location /` (proxy_metadata, a read path with | |
| # caching); it also sets no client_max_body_size, so nginx's 1 MB default rejects | |
| # a packument carrying the base64 tarball. This workflow used to pin the registry | |
| # and then strip the pin from /etc/hosts just before publishing; that hack is gone. | |
| # | |
| # `bun install --frozen-lockfile` below installs bun.lock verbatim and resolves | |
| # nothing, so removing the gateway does not widen what this job can pull. | |
| # | |
| # check-wix-proxy.yml enforces the split: the proxy is mandatory everywhere | |
| # except the two publish workflows, where it is forbidden. | |
| jobs: | |
| publish-preview: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # id-token: write for npm trusted publishing (OIDC). | |
| # pull-requests: write for the install-instructions comment. | |
| contents: read | |
| id-token: write | |
| pull-requests: write | |
| defaults: | |
| run: | |
| working-directory: packages/cli | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| # No `npm install -g npm@latest`. Trusted publishing needs npm >= 11.5.1 and | |
| # setup-node resolves `.node-version` (24) to the newest 24.x, which bundles | |
| # npm >= 11.17.0. The upgrade was also a needless registry fetch. | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: ".node-version" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Setup Bun | |
| id: setup-bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| working-directory: . | |
| - name: Build package | |
| run: bun run build | |
| - name: Generate preview package name and version | |
| id: preview_info | |
| run: | | |
| PR_NUMBER="${{ github.event.number }}" | |
| COMMIT_HASH="${{ github.sha }}" | |
| SHORT_COMMIT="${COMMIT_HASH:0:7}" | |
| # Get current version from package.json | |
| BASE_VERSION=$(node -p "require('./package.json').version") | |
| if [ ! -z "$PR_NUMBER" ]; then | |
| # Format: 0.0.1-pr.123.abc1234 (valid semver prerelease) | |
| PREVIEW_VERSION="$BASE_VERSION-pr.$PR_NUMBER.$SHORT_COMMIT" | |
| else | |
| # Format: 0.0.1-dev.abc1234 (valid semver prerelease) | |
| PREVIEW_VERSION="$BASE_VERSION-dev.$SHORT_COMMIT" | |
| fi | |
| echo "version=$PREVIEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "package_name=@base44-preview/cli" >> $GITHUB_OUTPUT | |
| echo "full_package=@base44-preview/cli@$PREVIEW_VERSION" >> $GITHUB_OUTPUT | |
| - name: Update package.json for preview | |
| run: | | |
| # Create a backup of original package.json | |
| cp package.json package.json.bak | |
| # Get the official package name for safety checks | |
| OFFICIAL_PACKAGE=$(node -p "require('./package.json').name") | |
| PREVIEW_PACKAGE="${{ steps.preview_info.outputs.package_name }}" | |
| echo "Official package: $OFFICIAL_PACKAGE" | |
| echo "Preview package: $PREVIEW_PACKAGE" | |
| # Safety check: Ensure we're not accidentally using the official package name | |
| if [ "$PREVIEW_PACKAGE" = "$OFFICIAL_PACKAGE" ]; then | |
| echo "❌ ERROR: Preview package name matches official package name!" | |
| echo "This would overwrite the official package. Aborting." | |
| exit 1 | |
| fi | |
| # `npm pkg set` replaces `bunx json-bump`: json-bump is declared in no | |
| # manifest, so bunx fetched it from the registry at run time — outside | |
| # bun.lock, and outside the cooldown, since bunx accepts | |
| # --minimum-release-age without enforcing it (oven-sh/bun#30748). | |
| if ! npm pkg set name="$PREVIEW_PACKAGE"; then | |
| echo "❌ ERROR: Failed to set package name to $PREVIEW_PACKAGE" | |
| exit 1 | |
| fi | |
| if ! npm pkg set version="${{ steps.preview_info.outputs.version }}"; then | |
| echo "❌ ERROR: Failed to set package version to ${{ steps.preview_info.outputs.version }}" | |
| exit 1 | |
| fi | |
| echo "✅ Package.json updated successfully" | |
| - name: Final safety check before publish | |
| run: | | |
| # Double-check package name one more time before publishing | |
| CURRENT_PACKAGE_NAME=$(node -p "require('./package.json').name") | |
| OFFICIAL_PACKAGE=$(jq -r '.name' package.json.bak) | |
| echo "About to publish: $CURRENT_PACKAGE_NAME" | |
| if [ "$CURRENT_PACKAGE_NAME" = "$OFFICIAL_PACKAGE" ]; then | |
| echo "❌ CRITICAL ERROR: About to publish to official package name!" | |
| echo "This is not allowed. Check the workflow configuration." | |
| exit 1 | |
| fi | |
| echo "✅ Safety check passed. Package name is safe to publish." | |
| - name: Publish preview package | |
| # Authenticates via npm trusted publishing (OIDC) using `id-token: write` | |
| # above — NODE_AUTH_TOKEN/secrets.NPM_TOKEN removed so no npm credential | |
| # reaches the build. Requires a trusted publisher for `@base44-preview/cli` | |
| # registered on npmjs.com against this repo AND this workflow filename (the | |
| # registry keys on the filename, so this workflow needs its own entry, | |
| # separate from manual-publish.yml). | |
| run: | | |
| # Remove devDependencies before publish (everything is bundled) | |
| jq 'del(.devDependencies)' package.json > package.json.tmp && mv package.json.tmp package.json | |
| if npm publish --tag preview; then | |
| echo "✅ Package published successfully" | |
| else | |
| echo "❌ Package publish failed" | |
| exit 1 | |
| fi | |
| - name: Restore original package.json | |
| if: always() | |
| run: | | |
| if [ -f package.json.bak ]; then | |
| mv package.json.bak package.json | |
| echo "✅ Original package.json restored" | |
| else | |
| echo "❌ WARNING: Backup file package.json.bak not found" | |
| echo "This could indicate an earlier step failed" | |
| fi | |
| - name: Comment PR with install instructions | |
| uses: actions/github-script@v6 | |
| with: | |
| script: | | |
| const fullPackage = '${{ steps.preview_info.outputs.full_package }}'; | |
| const installCmd = `npm i ${fullPackage}`; | |
| const aliasInstallCmd = `npm i "base44@npm:${fullPackage}"`; | |
| const body = `### 🚀 Package Preview Available! | |
| --- | |
| **Install this PR's preview build with npm:** | |
| \`\`\`sh | |
| ${installCmd} | |
| \`\`\` | |
| **Prefer not to change any import paths? Install using npm alias so your code still imports \`base44\`:** | |
| \`\`\`sh | |
| ${aliasInstallCmd} | |
| \`\`\` | |
| Or add it to your \`package.json\` dependencies: | |
| \`\`\`json | |
| { | |
| "dependencies": { | |
| "base44": "npm:${fullPackage}" | |
| } | |
| } | |
| \`\`\` | |
| - 📦 **Preview Package**: \`${fullPackage}\` | |
| - 🔗 [View this commit on GitHub](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) | |
| --- | |
| <sub>Preview published to npm registry — try new features instantly!</sub>`; | |
| const botCommentIdentifier = '### 🚀 Package Preview Available!'; | |
| async function findBotComment(issueNumber) { | |
| if (!issueNumber) return null; | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| }); | |
| return comments.data.find((comment) => | |
| comment.body.includes(botCommentIdentifier) | |
| ); | |
| } | |
| async function createOrUpdateComment(issueNumber) { | |
| if (!issueNumber) { | |
| console.log('No issue number provided. Cannot post or update comment.'); | |
| return; | |
| } | |
| const existingComment = await findBotComment(issueNumber); | |
| if (existingComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existingComment.id, | |
| body: body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| issue_number: issueNumber, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body, | |
| }); | |
| } | |
| } | |
| if (context.eventName === 'pull_request') { | |
| if (context.issue.number) { | |
| await createOrUpdateComment(context.issue.number); | |
| } | |
| } |