v0.4.0 #14
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
| # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json | |
| name: Publish | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to publish (e.g., 0.1.0)" | |
| required: true | |
| type: string | |
| # Workflow-level permissions use least privilege (read-only). | |
| # Jobs that need elevated permissions (npm OIDC, GHCR push) declare them | |
| # individually on the job — see publish-npm and publish-docker. | |
| permissions: | |
| contents: read | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| # Build native addons on each platform and upload as artifacts. | |
| # These are combined in the publish-npm job to create a cross-platform package. | |
| # | |
| # gnu and win32 builds run tests natively on their platform. | |
| # musl is cross-compiled from the glibc runner (can't run tests on glibc host). | |
| build-native: | |
| name: Build (${{ matrix.build }}) | |
| strategy: | |
| fail-fast: true | |
| matrix: | |
| build: [linux-kvm, linux-musl, windows-whp] | |
| include: | |
| - build: linux-kvm | |
| hypervisor: kvm | |
| run_tests: true | |
| - build: linux-musl | |
| hypervisor: kvm | |
| run_tests: false # musl .node can't run on glibc host | |
| - build: windows-whp | |
| hypervisor: whp | |
| run_tests: true | |
| runs-on: ${{ fromJson( | |
| format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-amd", "JobId={2}-{3}-{4}-{5}"]', | |
| matrix.hypervisor == 'whp' && 'Windows' || 'Linux', | |
| matrix.hypervisor == 'whp' && 'win2025' || 'kvm', | |
| matrix.build, | |
| github.run_id, | |
| github.run_number, | |
| github.run_attempt)) }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - uses: hyperlight-dev/ci-setup-workflow@v1.9.0 | |
| with: | |
| rust-toolchain: "1.89" | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup debug build for tests | |
| run: just setup | |
| - name: Install musl tools and rebuild for musl target | |
| if: matrix.build == 'linux-musl' | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y musl-tools | |
| rustup target add x86_64-unknown-linux-musl | |
| # Rebuild hyperlight-js NAPI addon targeting musl in release mode. | |
| hl_dir=$(just resolve-hyperlight-dir) | |
| cd "${hl_dir}/src/js-host-api" | |
| npx napi build --platform --release --strip --target x86_64-unknown-linux-musl | |
| # Rebuild hyperlight-analysis NAPI addon targeting musl in release mode. | |
| cd "$GITHUB_WORKSPACE/src/code-validator/guest" | |
| npx napi build --platform --release --strip --target x86_64-unknown-linux-musl --manifest-path host/Cargo.toml | |
| node -e "require('fs').readdirSync('host').filter(f=>f.endsWith('.node')).forEach(f=>require('fs').copyFileSync('host/'+f,f))" | |
| # This matrix leg must upload only musl artifacts; the setup step also | |
| # creates host-platform glibc addons, which are supplied by linux-kvm. | |
| rm -f "${hl_dir}/src/js-host-api/"*.linux-x64-gnu.node | |
| rm -f "$GITHUB_WORKSPACE/src/code-validator/guest/host/"*.linux-x64-gnu.node | |
| rm -f "$GITHUB_WORKSPACE/src/code-validator/guest/"*.linux-x64-gnu.node | |
| # Verify musl .node files were actually produced | |
| ls -la "${hl_dir}/src/js-host-api/"*.linux-x64-musl.node | |
| ls -la "$GITHUB_WORKSPACE/src/code-validator/guest/"*linux-x64-musl* || ls -la "$GITHUB_WORKSPACE/src/code-validator/guest/host/"*linux-x64-musl* | |
| - name: Build release binary | |
| if: matrix.run_tests | |
| run: node scripts/build-binary.js --release | |
| env: | |
| VERSION: ${{ github.event.release.tag_name || inputs.version }} | |
| - name: Run tests | |
| if: matrix.run_tests | |
| run: just test | |
| - name: Rebuild native addons for release packaging | |
| if: matrix.run_tests | |
| run: just build-release | |
| # Upload the native .node addons so the publish job can combine them | |
| - name: Upload native addons | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: native-addons-${{ matrix.build }} | |
| path: | | |
| deps/js-host-api/js-host-api.*.node | |
| src/code-validator/guest/host/hyperlight-analysis.*.node | |
| src/code-validator/guest/hyperlight-analysis.*.node | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Assemble the final npm package tarball on a self-hosted Linux runner | |
| # (needs `just setup` for the hyperlight toolchain to build the binary). | |
| # The resulting tarball is uploaded as an artifact, then published from a | |
| # github-hosted runner — npm sigstore provenance *requires* github-hosted. | |
| pack-npm: | |
| name: Pack npm tarball | |
| needs: [build-native] | |
| permissions: | |
| contents: read | |
| runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd","JobId=hyperagent-pack-npm-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}"] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - uses: hyperlight-dev/ci-setup-workflow@v1.9.0 | |
| with: | |
| rust-toolchain: "1.89" | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup | |
| run: just setup | |
| # Download AFTER setup so artifacts land in the symlink/junction target | |
| # that build-hyperlight creates (deps/js-host-api → Cargo checkout). | |
| # Downloading before setup would be clobbered when setup re-creates the link. | |
| - name: Download all native addons | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: native-addons-* | |
| merge-multiple: true | |
| - name: Build binary (with all platform addons present) | |
| run: VERSION="${{ github.event.release.tag_name || inputs.version }}" node scripts/build-binary.js --release | |
| - name: Set version from release tag | |
| if: github.event_name == 'release' | |
| run: npm version ${{ github.event.release.tag_name }} --no-git-tag-version --allow-same-version | |
| - name: Set version from input | |
| if: github.event_name == 'workflow_dispatch' | |
| run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version | |
| - name: Pack npm tarball | |
| run: npm pack | |
| - name: Check npm package size | |
| run: | | |
| TARBALL=$(ls *.tgz) | |
| UNPACKED=$(npm pack --dry-run --json | node -e 'let data="";process.stdin.on("data",c=>data+=c);process.stdin.on("end",()=>console.log(JSON.parse(data)[0].unpackedSize))') | |
| MAX=$((300 * 1024 * 1024)) | |
| echo "Packed tarball: $TARBALL" | |
| echo "Unpacked size: $UNPACKED bytes (limit: $MAX)" | |
| if [ "$UNPACKED" -gt "$MAX" ]; then | |
| echo "❌ npm package is too large" | |
| exit 1 | |
| fi | |
| - name: Upload npm tarball | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: npm-tarball | |
| path: "*.tgz" | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Publish the prebuilt tarball from a github-hosted runner. | |
| # npm sigstore provenance (--provenance) only accepts github-hosted runners; | |
| # self-hosted is rejected with: | |
| # E422 Unsupported GitHub Actions runner environment: "self-hosted" | |
| # This job does no building — it just takes the tarball and pushes it. | |
| publish-npm: | |
| name: Publish to npmjs.org | |
| needs: [pack-npm] | |
| # id-token: write is required for npm OIDC trusted publishing. | |
| # Scoped to this job only (least privilege). | |
| permissions: | |
| id-token: write | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| registry-url: "https://registry.npmjs.org" | |
| # Trusted publishing requires npm >=11.5.1 for OIDC token exchange. | |
| # Pin to ^11.5.1 so we don't silently get an older 11.x that lacks OIDC. | |
| # | |
| # Bootstrap via `npx` rather than `npm install -g npm@...` — the latter | |
| # hits a long-standing npm self-upgrade bug (reproduces on github-hosted | |
| # runners too) where mid-reify npm unlinks its own `promise-retry` dep | |
| # and dies with MODULE_NOT_FOUND. Using a fresh npx-fetched npm to | |
| # install itself globally sidesteps the half-upgraded state entirely. | |
| - name: Upgrade npm for trusted publishing | |
| run: | | |
| npx --yes npm@^11.5.1 install -g --force npm@^11.5.1 | |
| npm --version | |
| - name: Download npm tarball | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: npm-tarball | |
| # OIDC trusted publishing for release events; NPM_TOKEN fallback for workflow_dispatch | |
| - name: Set publish flags | |
| id: publish-flags | |
| run: | | |
| if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then | |
| echo "provenance=--provenance" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "provenance=" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish to npmjs.org | |
| run: npm publish *.tgz --access public ${{ steps.publish-flags.outputs.provenance }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ github.event_name == 'workflow_dispatch' && secrets.NPM_TOKEN || '' }} | |
| # Post-publish smoke test: install the *just-published* tarball from the npm | |
| # registry into a clean container and verify the bundled launcher resolves | |
| # the correct platform binary. | |
| # | |
| # This is the missing gate that catches: | |
| # - tarball missing native binaries (the Friday-2026-04-23 false-alarm) | |
| # - launcher musl-vs-glibc detection broken | |
| # - file: deps in `dependencies` actually breaking npm install | |
| # - registry replication / CDN propagation lag | |
| # - shasum mismatch between published metadata and tarball blob | |
| # | |
| # We test linux-x64-gnu (debian-slim) and linux-x64-musl (alpine) — the two | |
| # Linux triples we ship. Windows smoke is left out for now (the launcher's | |
| # win32-x64-msvc path is exercised via `just test` pre-publish). | |
| smoke-test: | |
| name: Smoke test (${{ matrix.label }}) | |
| needs: [publish-npm] | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - label: linux-x64-gnu | |
| image: node:22-bookworm-slim | |
| - label: linux-x64-musl | |
| image: node:22-alpine | |
| steps: | |
| - name: Resolve published version | |
| id: ver | |
| run: | | |
| # Release tags arrive as `v0.2.3`; npm strips the leading `v` at | |
| # publish time. Match that here so `npm view <pkg>@<ver>` resolves. | |
| RAW="${{ github.event.release.tag_name || inputs.version }}" | |
| VERSION="${RAW#v}" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Smoke-testing @hyperlight-dev/hyperagent@$VERSION" | |
| - name: Wait for registry propagation | |
| env: | |
| VERSION: ${{ steps.ver.outputs.version }} | |
| run: | | |
| # Poll registry.npmjs.org for up to 5 minutes for the version metadata | |
| # AND tarball blob to become fetchable. npm can expose dist metadata | |
| # before the tarball URL has propagated through the CDN, so metadata | |
| # visibility alone is not enough for installability. | |
| for i in $(seq 1 30); do | |
| ACTUAL=$(npm view "@hyperlight-dev/hyperagent@$VERSION" version 2>/dev/null || true) | |
| if [ "$ACTUAL" = "$VERSION" ]; then | |
| TARBALL=$(npm view "@hyperlight-dev/hyperagent@$VERSION" dist.tarball 2>/dev/null || true) | |
| EXPECTED=$(npm view "@hyperlight-dev/hyperagent@$VERSION" dist.shasum 2>/dev/null || true) | |
| TMP=$(mktemp) | |
| if [ -n "$TARBALL" ] && [ -n "$EXPECTED" ] && curl -fsSL "$TARBALL" -o "$TMP"; then | |
| FOUND=$(sha1sum "$TMP" | awk '{print $1}') | |
| rm -f "$TMP" | |
| if [ "$FOUND" = "$EXPECTED" ]; then | |
| echo "✅ metadata and tarball visible after $((i * 10))s ($EXPECTED)" | |
| exit 0 | |
| fi | |
| echo "⏳ tarball shasum mismatch on attempt $i — expected $EXPECTED got $FOUND" | |
| else | |
| rm -f "$TMP" | |
| echo "⏳ metadata visible, tarball not fetchable yet (attempt $i)" | |
| fi | |
| fi | |
| sleep 10 | |
| done | |
| echo "❌ @hyperlight-dev/hyperagent@$VERSION metadata/tarball not installable after 300s" | |
| exit 1 | |
| - name: Install + run in ${{ matrix.image }} | |
| env: | |
| VERSION: ${{ steps.ver.outputs.version }} | |
| run: | | |
| docker run --rm \ | |
| -e VERSION="$VERSION" \ | |
| ${{ matrix.image }} \ | |
| sh -c ' | |
| set -e | |
| echo "=== platform ===" | |
| uname -a | |
| cat /etc/os-release 2>/dev/null | head -3 || true | |
| echo "=== install ===" | |
| npm install -g --no-audit --no-fund "@hyperlight-dev/hyperagent@${VERSION}" | |
| echo "=== version check ===" | |
| ACTUAL=$(hyperagent --version 2>&1) | |
| echo "got: $ACTUAL" | |
| echo "$ACTUAL" | grep -q "${VERSION}" \ | |
| || { echo "❌ --version did not match ${VERSION}"; exit 1; } | |
| echo "=== help check ===" | |
| hyperagent --help 2>&1 | grep -q "Usage:" \ | |
| || { echo "❌ --help did not produce expected output"; exit 1; } | |
| echo "✅ smoke passed on ${VERSION}" | |
| ' | |
| # Build and publish Docker image (after tests pass) | |
| publish-docker: | |
| name: Publish to GitHub Container Registry | |
| needs: [build-native] | |
| # packages: write for pushing the image to GHCR. | |
| # Scoped to this job only (least privilege). | |
| permissions: | |
| contents: read | |
| packages: write | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - uses: hyperlight-dev/ci-setup-workflow@v1.9.0 | |
| with: | |
| rust-toolchain: "1.89" | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup | |
| run: just setup | |
| - name: Resolve symlinks for Docker context | |
| run: | | |
| if [ -L deps/js-host-api ]; then | |
| target=$(readlink -f deps/js-host-api) | |
| rm deps/js-host-api | |
| cp -r "$target" deps/js-host-api | |
| fi | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=semver,pattern={{major}} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| VERSION=${{ github.event.release.tag_name || inputs.version }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max |