fix(native): break engine_version mismatch loop in CI hot-swap flows … #864
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 | |
| on: | |
| push: | |
| branches: [main] | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to publish (e.g. 2.1.0, without v prefix). Used to retry a failed stable release.' | |
| required: true | |
| type: string | |
| concurrency: | |
| group: publish-${{ github.event_name }} | |
| cancel-in-progress: true | |
| permissions: {} | |
| jobs: | |
| preflight: | |
| name: Preflight checks | |
| runs-on: ubuntu-latest | |
| # Skip dev publish when the push is a stable release version bump (direct push or merged PR) | |
| if: "${{ github.event_name != 'push' || (!startsWith(github.event.head_commit.message, 'chore: release v') && !contains(github.event.head_commit.message, 'release/v')) }}" | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| # Build the native addon from the current Rust source so build-parity | |
| # tests compare WASM and native engines built from the same commit. | |
| # Without this, npm install pulls the last-published binary, which lags | |
| # behind PR changes that touch both the TS and Rust extractors. | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Rust cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: crates/codegraph-core | |
| - name: Install napi-rs CLI | |
| timeout-minutes: 5 | |
| run: npm install -g @napi-rs/cli@3 | |
| - name: Install dependencies | |
| timeout-minutes: 20 | |
| run: npm install | |
| - name: Build native addon from current source | |
| working-directory: crates/codegraph-core | |
| run: napi build --release | |
| - name: Install native addon over published binary | |
| run: node scripts/ci-install-native.mjs | |
| - run: npm test | |
| compute-version: | |
| needs: preflight | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| version: ${{ steps.compute.outputs.version }} | |
| npm_tag: ${{ steps.compute.outputs.npm_tag }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Compute version | |
| id: compute | |
| run: | | |
| CURRENT=$(node -p "require('./package.json').version") | |
| if [ "${{ github.event_name }}" = "release" ]; then | |
| TAG="${{ github.event.release.tag_name }}" | |
| VERSION="${TAG#v}" | |
| NPM_TAG="latest" | |
| echo "Stable release: $VERSION (from tag $TAG)" | |
| elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| VERSION="${{ inputs.version }}" | |
| VERSION="${VERSION#v}" | |
| if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then | |
| echo "::error::Invalid version '$VERSION'. Expected semver (e.g. 2.1.0)." | |
| exit 1 | |
| fi | |
| NPM_TAG="latest" | |
| echo "Stable release (manual retry): $VERSION" | |
| else | |
| # Dev versions use the NEXT patch with a prerelease suffix: | |
| # v3.1.5 tag + 12 commits → 3.1.6-dev.12 | |
| # This keeps dev versions in the correct semver range (between | |
| # the current release and the next) and avoids inflating the | |
| # patch number (old scheme produced 3.1.17 for 12 commits after 3.1.5). | |
| RELEASE_TAG=$(git describe --tags --match "v*" --abbrev=0 2>/dev/null || echo "") | |
| if [ -n "$RELEASE_TAG" ]; then | |
| COMMITS=$(git rev-list "${RELEASE_TAG}..HEAD" --count) | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "${RELEASE_TAG#v}" | |
| else | |
| COMMITS=1 | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" | |
| fi | |
| if [ "$COMMITS" -eq 0 ]; then | |
| VERSION="${MAJOR}.${MINOR}.${PATCH}-dev.0" | |
| else | |
| VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))-dev.${COMMITS}" | |
| fi | |
| NPM_TAG="dev" | |
| echo "Dev release: $VERSION (${COMMITS} commits since ${RELEASE_TAG:-none})" | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "npm_tag=$NPM_TAG" >> "$GITHUB_OUTPUT" | |
| build-native: | |
| needs: [preflight, compute-version] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| node_arch: x64 | |
| node_os: linux | |
| artifact_key: linux-x64 | |
| - os: ubuntu-latest | |
| target: aarch64-unknown-linux-gnu | |
| node_arch: arm64 | |
| node_os: linux | |
| artifact_key: linux-arm64 | |
| cross: true | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| node_arch: x64 | |
| node_os: linux | |
| artifact_key: linux-x64-musl | |
| cross: true | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| node_arch: arm64 | |
| node_os: darwin | |
| artifact_key: darwin-arm64 | |
| - os: macos-14 | |
| target: x86_64-apple-darwin | |
| node_arch: x64 | |
| node_os: darwin | |
| artifact_key: darwin-x64 | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| node_arch: x64 | |
| node_os: win32 | |
| artifact_key: win32-x64 | |
| runs-on: ${{ matrix.os }} | |
| name: Build ${{ matrix.target }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Rust cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: crates/codegraph-core | |
| - name: Install cross-compilation tools (aarch64-gnu) | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gcc-aarch64-linux-gnu | |
| echo "CC=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" | |
| - name: Install cross-compilation tools (x86_64-musl) | |
| if: matrix.target == 'x86_64-unknown-linux-musl' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y musl-tools | |
| # Create linker script so -lgcc_s resolves to static libgcc | |
| MUSL_LIB_DIR="$(dirname "$(musl-gcc -print-libgcc-file-name)")" | |
| echo 'INPUT(-lgcc)' | sudo tee "${MUSL_LIB_DIR}/libgcc_s.so" > /dev/null | |
| echo "CC_x86_64_unknown_linux_musl=musl-gcc" >> "$GITHUB_ENV" | |
| echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc" >> "$GITHUB_ENV" | |
| # Symlink libgcc_s into Rust sysroot so rust-lld can find it for host builds | |
| GNU_LIB="$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/lib" | |
| sudo ln -sf /lib/x86_64-linux-gnu/libgcc_s.so.1 "${GNU_LIB}/libgcc_s.so" | |
| sudo ln -sf /lib/x86_64-linux-gnu/libgcc_s.so.1 "${GNU_LIB}/libgcc_s.so.1" | |
| - name: Sync Cargo.toml version | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| shell: bash | |
| run: | | |
| [[ -n "$VERSION" ]] || { echo "::error::VERSION is empty — compute-version output missing"; exit 1; } | |
| CARGO="crates/codegraph-core/Cargo.toml" | |
| awk -v v="$VERSION" '!done && /^version =/{$0="version = \""v"\""; done=1}1' "$CARGO" > "${CARGO}.tmp" | |
| mv "${CARGO}.tmp" "$CARGO" | |
| - name: Install napi-rs CLI | |
| timeout-minutes: 5 | |
| run: npm install -g @napi-rs/cli@3 | |
| - name: Build native addon | |
| working-directory: crates/codegraph-core | |
| run: napi build --release --target ${{ matrix.target }} | |
| - name: Upload native binary | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: native-${{ matrix.artifact_key }} | |
| path: crates/codegraph-core/*.node | |
| if-no-files-found: error | |
| # ── Pre-publish benchmark gate (stable releases only) ── | |
| # | |
| # Measures the just-built native artifact against the local source, writes | |
| # new entries into the benchmark history files, and runs the regression | |
| # guard. If the new version regresses beyond the threshold vs the previous | |
| # release, this job fails and the publish job is skipped — preventing the | |
| # bad code from reaching npm. The modified history files are uploaded as | |
| # an artifact so the post-publish Benchmark workflow can record them via | |
| # PR without re-measuring (single source of truth, half the CI minutes). | |
| pre-publish-benchmark: | |
| name: Pre-publish benchmark gate | |
| if: github.event_name != 'push' | |
| needs: [compute-version, build-native] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| # Surface why detectNoChanges returns false on each fast-skip pre-flight | |
| # so we can pinpoint the cause of the ~2s incremental-rebuild regression | |
| # observed in CI but not locally (#1066). Remove once root cause is fixed. | |
| CODEGRAPH_FAST_SKIP_DIAG: "1" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| cache: "npm" | |
| - name: Setup Python (for resolution benchmark + tracer validation) | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Setup Go (for resolution benchmark + tracer validation) | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: "stable" | |
| cache: false | |
| - name: Download native artifact (linux-x64) | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: native-linux-x64 | |
| path: crates/codegraph-core/ | |
| - name: Install dependencies | |
| timeout-minutes: 20 | |
| run: npm install | |
| - name: Install native addon over published binary | |
| # Pass NATIVE_BUILD_VERSION so the script also rewrites the platform | |
| # package.json's version to match the binary's CARGO_PKG_VERSION | |
| # (build-native bumps Cargo.toml to this same value before building). | |
| # Without this, the JS-side getNativePackageVersion() returns the | |
| # last-published version while the binary reports the bumped version, | |
| # and the Rust orchestrator's check_version_mismatch forces every | |
| # incremental rebuild back through the full pipeline (#1066). | |
| env: | |
| NATIVE_BUILD_VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: node scripts/ci-install-native.mjs | |
| # Build dist/ so benchmarks load the same compiled JS that ships to npm. | |
| # Historical baselines (v3.9.6 and earlier) were measured against dist/ | |
| # via the post-publish --npm path; running pre-publish against src/ with | |
| # --strip-types changed the load path and introduced version-to-version | |
| # noise unrelated to the code under test (#1055). | |
| - name: Build TypeScript | |
| run: npm run build | |
| - name: Run build benchmark | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG --import ./scripts/ts-resolve-loader.js scripts/benchmark.ts --version "$VERSION" --dist > benchmark-result.json | |
| - name: Run resolution benchmark | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG --import ./scripts/ts-resolve-loader.js scripts/resolution-benchmark.ts --version "$VERSION" --dist > resolution-result.json | |
| - name: Gate on resolution thresholds | |
| timeout-minutes: 30 | |
| run: npx vitest run tests/benchmarks/resolution/resolution-benchmark.test.ts --reporter=verbose | |
| - name: Run tracer validation (same-file edge recall) | |
| timeout-minutes: 10 | |
| run: npx vitest run tests/benchmarks/resolution/tracer/tracer-validation.test.ts --reporter=verbose | |
| - name: Merge resolution into build result | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const build = JSON.parse(fs.readFileSync('benchmark-result.json', 'utf8')); | |
| const resolution = JSON.parse(fs.readFileSync('resolution-result.json', 'utf8')); | |
| build.resolution = resolution; | |
| fs.writeFileSync('benchmark-result.json', JSON.stringify(build, null, 2)); | |
| " | |
| - name: Run query benchmark | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG --import ./scripts/ts-resolve-loader.js scripts/query-benchmark.ts --version "$VERSION" --dist > query-benchmark-result.json | |
| - name: Run incremental benchmark | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG --import ./scripts/ts-resolve-loader.js scripts/incremental-benchmark.ts --version "$VERSION" --dist > incremental-benchmark-result.json | |
| - name: Update build report | |
| run: | | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG scripts/update-benchmark-report.ts benchmark-result.json | |
| - name: Update query report | |
| run: | | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG scripts/update-query-report.ts query-benchmark-result.json | |
| - name: Update incremental report | |
| run: | | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG scripts/update-incremental-report.ts incremental-benchmark-result.json | |
| - name: Regression guard | |
| env: | |
| RUN_REGRESSION_GUARD: "1" | |
| run: npm run test:regression-guard | |
| - name: Upload benchmark history files | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: benchmark-files | |
| path: | | |
| generated/benchmarks/BUILD-BENCHMARKS.md | |
| generated/benchmarks/QUERY-BENCHMARKS.md | |
| generated/benchmarks/INCREMENTAL-BENCHMARKS.md | |
| README.md | |
| if-no-files-found: error | |
| # Raw JSON used by post-publish soft-signal jobs (e.g. engine-parity | |
| # gate in the Benchmark workflow). Separated from the history-files | |
| # artifact because consumers read different shapes. | |
| - name: Upload benchmark JSON results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: benchmark-results-json | |
| path: | | |
| benchmark-result.json | |
| query-benchmark-result.json | |
| incremental-benchmark-result.json | |
| if-no-files-found: error | |
| # ── Dev builds: GitHub pre-release with tarballs ── | |
| publish-dev: | |
| if: github.event_name == 'push' | |
| needs: [compute-version, build-native] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - name: Install dependencies | |
| timeout-minutes: 20 | |
| run: npm install | |
| - name: Set version | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| npm version "$VERSION" --no-git-tag-version --allow-same-version | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG scripts/sync-native-versions.ts --strip | |
| echo "Packaging version $VERSION" | |
| - name: Build TypeScript | |
| run: npm run build | |
| - name: Disable prepublishOnly | |
| run: npm pkg delete scripts.prepublishOnly | |
| - name: Download native artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: ${{ runner.temp }}/artifacts/ | |
| - name: Pack main package | |
| run: npm pack | |
| - name: Pack platform packages | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| shell: bash | |
| run: | | |
| # Format: pkg_name|os|cpu|libc (libc empty for non-linux) | |
| declare -A PACKAGES=( | |
| ["linux-x64"]="@optave/codegraph-linux-x64-gnu|linux|x64|glibc" | |
| ["linux-x64-musl"]="@optave/codegraph-linux-x64-musl|linux|x64|musl" | |
| ["linux-arm64"]="@optave/codegraph-linux-arm64-gnu|linux|arm64|glibc" | |
| ["darwin-arm64"]="@optave/codegraph-darwin-arm64|darwin|arm64|" | |
| ["darwin-x64"]="@optave/codegraph-darwin-x64|darwin|x64|" | |
| ["win32-x64"]="@optave/codegraph-win32-x64-msvc|win32|x64|" | |
| ) | |
| ARTIFACTS="${RUNNER_TEMP}/artifacts" | |
| PKG_DIR="${RUNNER_TEMP}/pkg" | |
| for artifact_dir in "${ARTIFACTS}"/native-*/; do | |
| platform=$(basename "$artifact_dir" | sed 's/^native-//') | |
| entry=${PACKAGES[$platform]} | |
| if [ -z "$entry" ]; then | |
| echo "::warning::Unknown platform artifact: $platform — skipping" | |
| continue | |
| fi | |
| IFS='|' read -r pkg_name node_os node_arch libc <<< "$entry" | |
| mkdir -p "${PKG_DIR}/$platform" | |
| cp "$artifact_dir"/*.node "${PKG_DIR}/$platform/codegraph-core.node" | |
| # Build libc field for Linux packages | |
| libc_field="" | |
| if [ -n "$libc" ]; then | |
| libc_field="\"libc\": [\"${libc}\"]," | |
| fi | |
| cat > "${PKG_DIR}/$platform/package.json" <<PKGJSON | |
| { | |
| "name": "${pkg_name}", | |
| "version": "${VERSION}", | |
| "description": "Native codegraph-core binary for ${node_os}-${node_arch}", | |
| "os": ["${node_os}"], | |
| "cpu": ["${node_arch}"], | |
| ${libc_field} | |
| "main": "codegraph-core.node", | |
| "files": ["codegraph-core.node"], | |
| "license": "Apache-2.0", | |
| "repository": { | |
| "type": "git", | |
| "url": "https://github.com/optave/ops-codegraph-tool.git" | |
| } | |
| } | |
| PKGJSON | |
| npm pack "${PKG_DIR}/$platform" | |
| done | |
| - name: Create GitHub pre-release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| TAG="dev-v${VERSION}" | |
| gh release create "$TAG" \ | |
| --prerelease \ | |
| --title "Dev build ${VERSION}" \ | |
| --notes "Dev build from commit \`${{ github.sha }}\` on \`main\`." \ | |
| *.tgz | |
| - name: Prune old dev releases | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # List dev releases sorted newest-first, skip the first 5, delete the rest | |
| # Non-critical: failures here should not fail the workflow | |
| TAGS=$(gh release list --limit 100 --json tagName,isPrerelease,createdAt 2>&1) || { | |
| echo "::warning::Failed to list releases for pruning: ${TAGS}" | |
| exit 0 | |
| } | |
| OLD_TAGS=$(echo "$TAGS" | jq -r ' | |
| [ .[] | select(.isPrerelease and (.tagName | startswith("dev-v"))) ] | |
| | sort_by(.createdAt) | reverse | |
| | .[5:] | |
| | .[].tagName | |
| ' 2>&1) || { | |
| echo "::warning::Failed to parse release list for pruning: ${OLD_TAGS}" | |
| exit 0 | |
| } | |
| # When fewer than 5 dev releases exist, OLD_TAGS is empty and the loop is a no-op | |
| echo "$OLD_TAGS" | while read -r tag; do | |
| [ -z "$tag" ] && continue | |
| echo "Deleting old dev release: $tag" | |
| gh release delete "$tag" --yes --cleanup-tag || echo "::warning::Failed to delete release ${tag}" | |
| done | |
| - name: Summary | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| TAG="dev-v${VERSION}" | |
| cat >> "$GITHUB_STEP_SUMMARY" <<EOF | |
| ## Dev Build Published | |
| **Version:** \`${VERSION}\` | |
| **Commit:** \`${{ github.sha }}\` | |
| ### Download | |
| Tarballs attached to [GitHub release \`${TAG}\`](${{ github.server_url }}/${{ github.repository }}/releases/tag/${TAG}). | |
| \`\`\`bash | |
| # Install the main package (uses WASM fallback): | |
| npm install ${{ github.server_url }}/${{ github.repository }}/releases/download/${TAG}/optave-codegraph-${VERSION}.tgz | |
| # For native performance, also install your platform package: | |
| # Linux x64 (glibc): | |
| npm install ${{ github.server_url }}/${{ github.repository }}/releases/download/${TAG}/optave-codegraph-linux-x64-gnu-${VERSION}.tgz | |
| # Linux x64 (musl/Alpine): | |
| npm install ${{ github.server_url }}/${{ github.repository }}/releases/download/${TAG}/optave-codegraph-linux-x64-musl-${VERSION}.tgz | |
| # Linux arm64 (glibc): | |
| npm install ${{ github.server_url }}/${{ github.repository }}/releases/download/${TAG}/optave-codegraph-linux-arm64-gnu-${VERSION}.tgz | |
| # macOS arm64: | |
| npm install ${{ github.server_url }}/${{ github.repository }}/releases/download/${TAG}/optave-codegraph-darwin-arm64-${VERSION}.tgz | |
| \`\`\` | |
| EOF | |
| # ── Stable releases: publish to npm ── | |
| publish: | |
| if: github.event_name != 'push' | |
| needs: [compute-version, build-native, pre-publish-benchmark] | |
| runs-on: ubuntu-latest | |
| environment: npm-publish | |
| permissions: | |
| contents: write | |
| id-token: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: main | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # Node 24 required: ships with npm v11 which supports OIDC trusted publishing. | |
| # Node 22 ships with npm v10 which lacks the OIDC handshake protocol needed by the npm registry. | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Install dependencies | |
| timeout-minutes: 20 | |
| run: npm install | |
| - name: Set version | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| git checkout -- package-lock.json | |
| npm version "$VERSION" --no-git-tag-version --allow-same-version | |
| STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") | |
| node $STRIP_FLAG scripts/sync-native-versions.ts | |
| echo "Publishing version $VERSION" | |
| - name: Build TypeScript | |
| run: npm run build | |
| - name: Disable prepublishOnly | |
| run: npm pkg delete scripts.prepublishOnly | |
| - name: Download native artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: ${{ runner.temp }}/artifacts/ | |
| - name: Check if main package already published | |
| id: check-main | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| PKG="@optave/codegraph" | |
| echo "Checking if $PKG@$VERSION already exists on npm..." | |
| if npm view "$PKG@$VERSION" version 2>/dev/null; then | |
| echo "⚠️ $PKG@$VERSION is already published — will skip publish steps" | |
| echo "already_published=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "$PKG@$VERSION is not yet published — proceeding" | |
| echo "already_published=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Debug npm auth | |
| if: steps.check-main.outputs.already_published == 'false' | |
| run: | | |
| echo "npm version: $(npm --version)" | |
| echo "node version: $(node --version)" | |
| npm whoami --registry https://registry.npmjs.org/ 2>&1 || echo "::warning::npm whoami failed — OIDC auth may not be working" | |
| echo "--- .npmrc ---" | |
| grep 'registry' ~/.npmrc 2>/dev/null || echo "no ~/.npmrc or no registry lines" | |
| grep 'registry' .npmrc 2>/dev/null || echo "no .npmrc or no registry lines" | |
| - name: Publish platform packages | |
| if: steps.check-main.outputs.already_published == 'false' | |
| env: | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| NPM_TAG: ${{ needs.compute-version.outputs.npm_tag }} | |
| shell: bash | |
| run: | | |
| source scripts/npm-publish-retry.sh | |
| # Format: pkg_name|os|cpu|libc (libc empty for non-linux) | |
| declare -A PACKAGES=( | |
| ["linux-x64"]="@optave/codegraph-linux-x64-gnu|linux|x64|glibc" | |
| ["linux-x64-musl"]="@optave/codegraph-linux-x64-musl|linux|x64|musl" | |
| ["linux-arm64"]="@optave/codegraph-linux-arm64-gnu|linux|arm64|glibc" | |
| ["darwin-arm64"]="@optave/codegraph-darwin-arm64|darwin|arm64|" | |
| ["darwin-x64"]="@optave/codegraph-darwin-x64|darwin|x64|" | |
| ["win32-x64"]="@optave/codegraph-win32-x64-msvc|win32|x64|" | |
| ) | |
| ARTIFACTS="${RUNNER_TEMP}/artifacts" | |
| PKG_DIR="${RUNNER_TEMP}/pkg" | |
| for artifact_dir in "${ARTIFACTS}"/native-*/; do | |
| platform=$(basename "$artifact_dir" | sed 's/^native-//') | |
| entry=${PACKAGES[$platform]} | |
| if [ -z "$entry" ]; then | |
| echo "::warning::Unknown platform artifact: $platform — skipping" | |
| continue | |
| fi | |
| IFS='|' read -r pkg_name node_os node_arch libc <<< "$entry" | |
| mkdir -p "${PKG_DIR}/$platform" | |
| cp "$artifact_dir"/*.node "${PKG_DIR}/$platform/codegraph-core.node" | |
| # Build libc field for Linux packages | |
| libc_field="" | |
| if [ -n "$libc" ]; then | |
| libc_field="\"libc\": [\"${libc}\"]," | |
| fi | |
| cat > "${PKG_DIR}/$platform/package.json" <<PKGJSON | |
| { | |
| "name": "${pkg_name}", | |
| "version": "${VERSION}", | |
| "description": "Native codegraph-core binary for ${node_os}-${node_arch}", | |
| "os": ["${node_os}"], | |
| "cpu": ["${node_arch}"], | |
| ${libc_field} | |
| "main": "codegraph-core.node", | |
| "files": ["codegraph-core.node"], | |
| "license": "Apache-2.0", | |
| "repository": { | |
| "type": "git", | |
| "url": "https://github.com/optave/ops-codegraph-tool.git" | |
| } | |
| } | |
| PKGJSON | |
| # Skip if this exact version is already published (idempotent re-runs) | |
| if npm view "${pkg_name}@${VERSION}" version 2>/dev/null; then | |
| echo "⚠️ ${pkg_name}@${VERSION} already published — skipping" | |
| continue | |
| fi | |
| echo "Publishing ${pkg_name}@${VERSION} with --tag ${NPM_TAG}" | |
| npm_publish_retry "${pkg_name}@${VERSION}" npm publish "${PKG_DIR}/$platform" --access public --provenance --tag "$NPM_TAG" | |
| done | |
| - name: Publish main package | |
| if: steps.check-main.outputs.already_published == 'false' | |
| env: | |
| NPM_TAG: ${{ needs.compute-version.outputs.npm_tag }} | |
| shell: bash | |
| run: | | |
| source scripts/npm-publish-retry.sh | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| PKG_VERSION=$(node -p "require('./package.json').version") | |
| npm_publish_retry "${PKG_NAME}@${PKG_VERSION}" npm publish --access public --provenance --tag "$NPM_TAG" | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Refresh node_modules after publish | |
| timeout-minutes: 10 | |
| run: npm install --ignore-scripts | |
| - name: Generate DEPENDENCIES.json | |
| run: mkdir -p generated && npm ls --json --all --omit=dev > generated/DEPENDENCIES.json 2>/dev/null || true | |
| - name: Push version bump via PR | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.compute-version.outputs.version }} | |
| run: | | |
| TAG="v${VERSION}" | |
| BRANCH="release/v${VERSION}" | |
| # Check if there are version bump changes to push | |
| if git diff --quiet HEAD -- package.json package-lock.json CHANGELOG.md generated/DEPENDENCIES.json crates/codegraph-core/Cargo.toml; then | |
| echo "No version bump commit to push — skipping PR" | |
| else | |
| git add -f package.json package-lock.json CHANGELOG.md generated/DEPENDENCIES.json crates/codegraph-core/Cargo.toml | |
| git commit -m "chore: release v${VERSION}" | |
| git push origin "HEAD:refs/heads/${BRANCH}" | |
| gh pr create \ | |
| --base main \ | |
| --head "$BRANCH" \ | |
| --title "chore: release v${VERSION}" \ | |
| --body "Automated version bump to \`${VERSION}\` and CHANGELOG update from publish workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." | |
| echo "::notice::Version bump PR created for ${BRANCH} → main" | |
| fi | |
| # Push tag (skip if it already exists on remote) | |
| if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then | |
| echo "Tag $TAG already exists on remote — skipping tag push" | |
| else | |
| git tag -a "$TAG" -m "release: $TAG" | |
| git push origin "$TAG" | |
| fi |