v0.2.21 #69
Workflow file for this run
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: Publish to npm | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'Skip npm publish steps (build/validate only)' | |
| required: false | |
| default: false | |
| type: boolean | |
| dev: | |
| description: 'Publish a dev prerelease version (auto-bumps to -dev.<run_id> and publishes with the dev dist-tag)' | |
| required: false | |
| default: false | |
| type: boolean | |
| release: | |
| types: [published] | |
| jobs: | |
| # Step 0: Build UI (required before Rust binaries) | |
| build-ui: | |
| name: Build UI | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'pnpm' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile --ignore-scripts | |
| - name: Build dependencies | |
| run: | | |
| pnpm --filter @leanspec/ui-components build | |
| pnpm --filter @leanspec/ai-worker build | |
| - name: Build UI | |
| run: pnpm --filter @leanspec/ui build | |
| - name: Upload UI dist artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ui-dist | |
| path: packages/ui/dist | |
| retention-days: 30 | |
| # Step 1: Build Rust binaries for ALL platforms | |
| rust-binaries: | |
| needs: build-ui # UI must be built first | |
| name: Build Rust binaries (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| platform: darwin-x64 | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| platform: darwin-arm64 | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| platform: linux-x64 | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| platform: windows-x64 | |
| env: | |
| CARGO_TERM_COLOR: always | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js (for version sync) | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Compute effective version | |
| id: version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| base_version=$(node -p "require('./package.json').version.replace(/-dev\\..*$/, '')") | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dev }}" = "true" ]; then | |
| echo "version=${base_version}-dev.${{ github.run_id }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Sync Rust workspace version | |
| shell: bash | |
| env: | |
| LEANSPEC_VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| node <<'NODE' | |
| const fs = require('fs'); | |
| const version = process.env.LEANSPEC_VERSION; | |
| if (!version) { | |
| console.error('Missing LEANSPEC_VERSION'); | |
| process.exit(1); | |
| } | |
| const cargoTomlPath = 'rust/Cargo.toml'; | |
| let content = fs.readFileSync(cargoTomlPath, 'utf8'); | |
| const re = /(\[workspace\.package\][\s\S]*?\nversion = \")([^\"]+)(\")/m; | |
| if (!re.test(content)) { | |
| console.error('Unable to locate [workspace.package] version in rust/Cargo.toml'); | |
| process.exit(1); | |
| } | |
| content = content.replace(re, `$1${version}$3`); | |
| fs.writeFileSync(cargoTomlPath, content); | |
| console.log(`✓ Set rust workspace version to ${version}`); | |
| NODE | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Download UI dist | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ui-dist | |
| path: packages/ui/dist | |
| - name: Build CLI binary | |
| working-directory: rust | |
| run: cargo build --release --target ${{ matrix.target }} --package leanspec-cli | |
| - name: Build MCP binary | |
| working-directory: rust | |
| run: cargo build --release --target ${{ matrix.target }} --package leanspec-mcp | |
| - name: Build HTTP server binary | |
| working-directory: rust | |
| run: cargo build --release --target ${{ matrix.target }} --package leanspec-http | |
| - name: Verify HTTP server binary version | |
| shell: bash | |
| env: | |
| LEANSPEC_VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| expected="$LEANSPEC_VERSION" | |
| bin="rust/target/${{ matrix.target }}/release/leanspec-http" | |
| if [ "${{ runner.os }}" = "Windows" ]; then | |
| bin="${bin}.exe" | |
| fi | |
| actual=$($bin --version | awk '{print $2}') | |
| echo "Expected: $expected" | |
| echo "Actual: $actual" | |
| if [ "$actual" != "$expected" ]; then | |
| echo "❌ Version mismatch: binary reports $actual but expected $expected" | |
| exit 1 | |
| fi | |
| - name: Prepare artifacts | |
| shell: bash | |
| run: | | |
| mkdir -p dist/${{ matrix.platform }} | |
| if [ "${{ runner.os }}" = "Windows" ]; then | |
| cp rust/target/${{ matrix.target }}/release/lean-spec.exe dist/${{ matrix.platform }}/ | |
| cp rust/target/${{ matrix.target }}/release/leanspec-mcp.exe dist/${{ matrix.platform }}/ | |
| cp rust/target/${{ matrix.target }}/release/leanspec-http.exe dist/${{ matrix.platform }}/ | |
| else | |
| cp rust/target/${{ matrix.target }}/release/lean-spec dist/${{ matrix.platform }}/ | |
| cp rust/target/${{ matrix.target }}/release/leanspec-mcp dist/${{ matrix.platform }}/ | |
| cp rust/target/${{ matrix.target }}/release/leanspec-http dist/${{ matrix.platform }}/ | |
| fi | |
| echo "=== Prepared artifacts ===" | |
| ls -la dist/${{ matrix.platform }} | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binaries-${{ matrix.platform }} | |
| path: dist/${{ matrix.platform }} | |
| retention-days: 30 | |
| # Step 2: Publish platform-specific packages (MUST happen before main packages) | |
| publish-platform: | |
| needs: rust-binaries | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'pnpm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile --ignore-scripts | |
| - name: Download all platform binaries | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts/ | |
| - name: Copy binaries to platform packages | |
| run: ./scripts/copy-platform-binaries.sh artifacts/ | |
| - name: Generate platform package manifests | |
| run: pnpm tsx scripts/generate-platform-manifests.ts | |
| - name: Add platform dependencies to parent packages | |
| run: pnpm tsx scripts/add-platform-deps.ts | |
| - name: Validate platform binaries | |
| run: pnpm tsx scripts/validate-platform-binaries.ts | |
| - name: Auto-bump dev version | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.dev }} | |
| run: | | |
| pnpm tsx scripts/bump-dev-version.ts ${{ github.run_id }} | |
| pnpm sync-versions | |
| - name: Sync versions (stable release) | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dev }} | |
| run: | | |
| pnpm sync-versions | |
| - name: Publish platform packages | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} | |
| run: | | |
| TAG_ARG="" | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dev }}" = "true" ]; then | |
| TAG_ARG="--tag dev" | |
| fi | |
| pnpm tsx scripts/publish-platform-packages.ts $TAG_ARG | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # Step 3: Publish main packages (AFTER platform packages are available) | |
| publish-main: | |
| needs: publish-platform | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'pnpm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile --ignore-scripts | |
| - name: Download all platform binaries | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts/ | |
| - name: Download UI dist | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ui-dist | |
| path: packages/ui/dist | |
| - name: Copy binaries for validation | |
| run: ./scripts/copy-platform-binaries.sh artifacts/ | |
| - name: Generate platform package manifests | |
| run: pnpm tsx scripts/generate-platform-manifests.ts | |
| - name: Add platform dependencies to parent packages | |
| run: pnpm tsx scripts/add-platform-deps.ts | |
| - name: Ensure CLI binaries are executable | |
| run: | | |
| chmod +x packages/cli/binaries/linux-x64/lean-spec || true | |
| - name: Auto-bump dev version | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.dev }} | |
| run: | | |
| pnpm tsx scripts/bump-dev-version.ts ${{ github.run_id }} | |
| pnpm sync-versions | |
| - name: Sync versions (stable release) | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dev }} | |
| run: | | |
| pnpm sync-versions | |
| - name: Wait for platform packages to propagate | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="latest" | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dev }}" = "true" ]; then | |
| TAG="dev" | |
| fi | |
| packages=( | |
| "@leanspec/cli-darwin-arm64" | |
| "@leanspec/cli-linux-x64" | |
| "@leanspec/mcp-darwin-arm64" | |
| "@leanspec/mcp-linux-x64" | |
| "@leanspec/http-darwin-arm64" | |
| "@leanspec/http-linux-x64" | |
| ) | |
| echo "Waiting 30 seconds before checking to allow all packages to finish publishing..." | |
| sleep 30 | |
| # Exponential backoff: start with short waits, increase gradually | |
| wait_times=(5 5 10 10 15 15 20 20 30 30 30 30 40 40 40 40 50 50 60 60) | |
| for attempt in {1..20}; do | |
| wait_time=${wait_times[$((attempt-1))]} | |
| echo "" | |
| echo "Attempt ${attempt}/20: checking npm registry..." | |
| missing=0 | |
| missing_packages=() | |
| for pkg in "${packages[@]}"; do | |
| if npm view "$pkg@$TAG" version >/dev/null 2>&1; then | |
| echo " ✓ $pkg@$TAG available" | |
| else | |
| echo " ✗ $pkg@$TAG not available" | |
| missing=1 | |
| missing_packages+=("$pkg") | |
| fi | |
| done | |
| if [ "$missing" -eq 0 ]; then | |
| echo "" | |
| echo "✅ All platform packages available!" | |
| exit 0 | |
| fi | |
| echo "" | |
| echo "Missing ${#missing_packages[@]} package(s): ${missing_packages[*]}" | |
| echo "Waiting ${wait_time}s for npm registry propagation..." | |
| sleep "$wait_time" | |
| done | |
| echo "" | |
| echo "❌ Platform packages not visible after 20 attempts." | |
| echo "Missing packages: ${missing_packages[*]}" | |
| exit 1 | |
| - name: Build TypeScript packages | |
| run: pnpm build | |
| - name: Validate specs | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} | |
| run: node bin/lean-spec.js validate --warnings-only | |
| - name: Prepare packages for npm publish | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} | |
| run: pnpm prepare-publish | |
| - name: Verify no workspace:* protocol after preparation | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} | |
| run: pnpm tsx scripts/validate-no-workspace-protocol.ts | |
| - name: Publish main packages | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} | |
| run: | | |
| TAG_ARG="" | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dev }}" = "true" ]; then | |
| TAG_ARG="--tag dev" | |
| fi | |
| pnpm tsx scripts/publish-main-packages.ts $TAG_ARG | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Restore packages after publish | |
| if: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} | |
| run: pnpm restore-packages | |
| - name: Summary | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dry_run }}" = "true" ]; then | |
| echo "✅ Dry run completed successfully" | |
| echo "" | |
| echo "All checks passed. Ready to publish for real." | |
| exit 0 | |
| fi | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dev }}" = "true" ]; then | |
| echo "✅ Dev version published successfully" | |
| echo "" | |
| echo "Test installation:" | |
| echo " npm install -g lean-spec@dev" | |
| exit 0 | |
| fi | |
| echo "✅ Published main packages successfully" | |
| echo "" | |
| echo "Verify installation:" | |
| echo " npm install -g lean-spec@latest" | |
| echo " lean-spec --version" | |
| - name: Verify publish success | |
| run: | | |
| echo "" | |
| echo "✅ All packages published successfully!" | |
| echo "" | |
| echo "Published:" | |
| echo " - CLI with platform binaries (darwin-x64, darwin-arm64, linux-x64, windows-x64)" | |
| echo " - MCP with platform binaries" | |
| echo " - UI Components" | |
| echo " - HTTP Server" | |
| echo " - UI" | |
| echo "" | |
| echo "Users can now install with:" | |
| echo " npm install -g lean-spec" |