From 2b893fc60f9a3ee8165da5881d90eb32009a713f Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Mon, 5 Oct 2026 13:55:58 +0300 Subject: [PATCH 1/2] ci: stop publishing the alien CLI distribution from this repo The stable release built the `alien` executable from a separately pinned external composition. That pin went stale, and it compiled a combination of sources that no other CI job tested. This repository now publishes only what it builds from its own source: crates, npm libraries, native addons, the AI gateway, client SDKs, Python wheels, container images, the OSS binaries in the release bucket, and a GitHub release carrying the changelog. The `alien` executable, `@alienplatform/cli`, the Homebrew formula and the `channels/stable` pointer are published by the official CLI distribution, from this repository's release tag. Co-Authored-By: Claude Opus 5.5 --- .github/RELEASING.md | 12 +- .github/official-cli-platform-revision | 1 - .github/workflows/release-stable.yml | 6 +- .github/workflows/release.yml | 435 +------------------------ packages/alien-cli-npm/bin/alien.js | 79 ----- packages/alien-cli-npm/package.json | 27 -- pnpm-lock.yaml | 2 - scripts/README.md | 1 - scripts/build-npm-packages.sh | 182 ----------- scripts/release-routing.test.mjs | 49 --- 10 files changed, 24 insertions(+), 770 deletions(-) delete mode 100644 .github/official-cli-platform-revision delete mode 100644 packages/alien-cli-npm/bin/alien.js delete mode 100644 packages/alien-cli-npm/package.json delete mode 100755 scripts/build-npm-packages.sh diff --git a/.github/RELEASING.md b/.github/RELEASING.md index ab5e119994..b47adbf7ab 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -17,7 +17,7 @@ The **Release qualification** check then: - builds and smoke-tests every native addon, binary, and container image; - packs every Cargo and npm package; -- creates the changelog, archives, and checksums; +- creates the changelog; - records the source Git tree plus every artifact checksum and image digest. Qualification artifacts are retained for 30 days. If they expire, rerun @@ -53,8 +53,8 @@ action. Never prepare another version to recover a partial release. The immutable Git tag and GitHub release are created only after package registries, images, and versioned binary objects have accepted the qualified -artifacts. Homebrew is updated last because its formula references the GitHub -release assets. After every destination succeeds, the workflow atomically -updates `channels/stable`; clients resolve that one pointer before downloading -immutable versioned binaries. Stable releases never update a collection of -independent `latest` objects. +artifacts. The GitHub release carries the changelog. + +This workflow does not publish the `alien` CLI, its npm package, the Homebrew +formula, or the `channels/stable` pointer. Those are published by the official +CLI distribution after this release, from the same release tag. diff --git a/.github/official-cli-platform-revision b/.github/official-cli-platform-revision deleted file mode 100644 index 051ffdb441..0000000000 --- a/.github/official-cli-platform-revision +++ /dev/null @@ -1 +0,0 @@ -6b196b8d747f0ba42794394890839bd0fb35a2e9 diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 9dcf8c36c8..ae2f7fa607 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -78,7 +78,7 @@ jobs: if ! jq -e ' (length > 0) and (any(.[]; .name == "qualify / prepare" and .conclusion == "success")) and - (any(.[]; .name == "qualify / qualify-github-release" and .conclusion == "success")) and + (any(.[]; .name == "qualify / qualify-images" and .conclusion == "success")) and (all(.[]; .conclusion == "success" or .conclusion == "skipped")) ' <<< "$qualified_jobs" >/dev/null; then echo "::error::Prior qualification jobs did not all pass" @@ -95,8 +95,8 @@ jobs: binaries-aarch64-apple-darwin binaries-aarch64-unknown-linux-musl \ binaries-x86_64-pc-windows-msvc binaries-x86_64-unknown-linux-musl \ changelog qualified-ai-gateway qualified-bindings \ - qualified-client-sdks qualified-crates qualified-github-release \ - qualified-image-digests qualified-npm qualified-npm-cli \ + qualified-client-sdks qualified-crates \ + qualified-image-digests qualified-npm \ qualified-python-linux-aarch64 qualified-python-linux-x86_64 \ qualified-python-macos-aarch64 qualified-python-macos-x86_64; do if ! jq -e --arg name "$required" 'index($name) != null' \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e947dad9f..c436fd05f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,7 +79,6 @@ jobs: qualification_run_id: ${{ steps.qualification.outputs.run_id }} qualification_head_sha: ${{ steps.qualification.outputs.head_sha }} release_commit: ${{ steps.merged_version.outputs.commit }} - platform_ref: ${{ steps.platform_revision.outputs.ref }} steps: - uses: actions/checkout@v7 with: @@ -104,16 +103,6 @@ jobs: exit 1 fi - - name: Read official CLI Platform revision - id: platform_revision - run: | - ref=$(tr -d '[:space:]' < .github/official-cli-platform-revision) - if ! grep -qE '^[0-9a-f]{40}$' <<< "$ref"; then - echo "::error::.github/official-cli-platform-revision must contain one exact commit" - exit 1 - fi - echo "ref=$ref" >> "$GITHUB_OUTPUT" - - name: Test release metadata and routing run: node --test scripts/sdk-release-metadata.test.mjs scripts/release-routing.test.mjs @@ -1725,17 +1714,6 @@ jobs: path: .ci-tool-versions persist-credentials: false - - uses: actions/checkout@v7 - with: - repository: alienplatform/platform - ref: ${{ needs.prepare.outputs.platform_ref }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} - path: platform - - - uses: ./platform/.github/actions/setup-composed-cli-dependencies - with: - repo-access-token: ${{ secrets.REPO_ACCESS_TOKEN }} - - name: Restore binaries from cache id: binary-cache uses: actions/cache@v6 @@ -1746,9 +1724,8 @@ jobs: target/x86_64-unknown-linux-musl/release/alien-worker-runtime target/x86_64-unknown-linux-musl/release/alien-manager target/x86_64-unknown-linux-musl/release/alien-sandbox-agent - platform/crates/alien-clix/target/x86_64-unknown-linux-musl/release/alien # Bump the -bN suffix whenever the set of cached binaries changes. - key: release-linux-x86_64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-clix-${{ needs.prepare.outputs.platform_ref }}-b1-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} + key: release-linux-x86_64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-b2-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} - uses: ./.ci-tool-versions/.github/actions/setup-rust if: steps.binary-cache.outputs.cache-hit != 'true' @@ -1766,9 +1743,6 @@ jobs: depot cargo build --release \ -p alien-deploy-cli -p alien-operator -p alien-worker-runtime -p alien-manager -p alien-sandbox-agent \ --target x86_64-unknown-linux-musl - ln -sfn "$GITHUB_WORKSPACE" alien - depot cargo build --release --manifest-path platform/crates/alien-clix/Cargo.toml \ - --bin alien --target x86_64-unknown-linux-musl - name: Stage binaries for upload run: | @@ -1776,8 +1750,6 @@ jobs: for binary in alien-deploy alien-operator alien-worker-runtime alien-manager alien-sandbox-agent; do cp "target/x86_64-unknown-linux-musl/release/${binary}" "staged/x86_64-unknown-linux-musl/${binary}" done - cp platform/crates/alien-clix/target/x86_64-unknown-linux-musl/release/alien staged/x86_64-unknown-linux-musl/alien - staged/x86_64-unknown-linux-musl/alien --help >/dev/null - name: Upload x86_64 linux binaries uses: actions/upload-artifact@v7 @@ -1804,17 +1776,6 @@ jobs: path: .ci-tool-versions persist-credentials: false - - uses: actions/checkout@v7 - with: - repository: alienplatform/platform - ref: ${{ needs.prepare.outputs.platform_ref }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} - path: platform - - - uses: ./platform/.github/actions/setup-composed-cli-dependencies - with: - repo-access-token: ${{ secrets.REPO_ACCESS_TOKEN }} - - name: Restore binaries from cache id: binary-cache uses: actions/cache@v6 @@ -1825,9 +1786,8 @@ jobs: target/aarch64-unknown-linux-musl/release/alien-worker-runtime target/aarch64-unknown-linux-musl/release/alien-manager target/aarch64-unknown-linux-musl/release/alien-sandbox-agent - platform/crates/alien-clix/target/aarch64-unknown-linux-musl/release/alien # Bump the -bN suffix whenever the set of cached binaries changes. - key: release-linux-aarch64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-clix-${{ needs.prepare.outputs.platform_ref }}-b1-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} + key: release-linux-aarch64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-b2-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} - uses: ./.ci-tool-versions/.github/actions/setup-rust if: steps.binary-cache.outputs.cache-hit != 'true' @@ -1845,9 +1805,6 @@ jobs: depot cargo build --release \ -p alien-deploy-cli -p alien-operator -p alien-worker-runtime -p alien-manager -p alien-sandbox-agent \ --target aarch64-unknown-linux-musl - ln -sfn "$GITHUB_WORKSPACE" alien - depot cargo build --release --manifest-path platform/crates/alien-clix/Cargo.toml \ - --bin alien --target aarch64-unknown-linux-musl - name: Stage binaries for upload run: | @@ -1855,8 +1812,6 @@ jobs: for binary in alien-deploy alien-operator alien-worker-runtime alien-manager alien-sandbox-agent; do cp "target/aarch64-unknown-linux-musl/release/${binary}" "staged/aarch64-unknown-linux-musl/${binary}" done - cp platform/crates/alien-clix/target/aarch64-unknown-linux-musl/release/alien staged/aarch64-unknown-linux-musl/alien - staged/aarch64-unknown-linux-musl/alien --help >/dev/null - name: Upload aarch64 linux binaries uses: actions/upload-artifact@v7 @@ -1885,17 +1840,6 @@ jobs: path: .ci-tool-versions persist-credentials: false - - uses: actions/checkout@v7 - with: - repository: alienplatform/platform - ref: ${{ needs.prepare.outputs.platform_ref }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} - path: platform - - - uses: ./platform/.github/actions/setup-composed-cli-dependencies - with: - repo-access-token: ${{ secrets.REPO_ACCESS_TOKEN }} - - name: Restore binaries from cache id: binary-cache uses: actions/cache@v6 @@ -1903,8 +1847,7 @@ jobs: path: | target/aarch64-apple-darwin/release/alien-deploy target/aarch64-apple-darwin/release/alien-operator - platform/crates/alien-clix/target/aarch64-apple-darwin/release/alien - key: release-darwin-aarch64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-clix-${{ needs.prepare.outputs.platform_ref }}-b1-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} + key: release-darwin-aarch64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-b2-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} - if: steps.binary-cache.outputs.cache-hit != 'true' name: Install Rust toolchain @@ -1946,9 +1889,6 @@ jobs: depot cargo build --release \ -p alien-deploy-cli -p alien-operator \ --target aarch64-apple-darwin - ln -sfn "$GITHUB_WORKSPACE" alien - depot cargo build --release --manifest-path platform/crates/alien-clix/Cargo.toml \ - --bin alien --target aarch64-apple-darwin - name: Stage binaries for upload run: | @@ -1956,8 +1896,6 @@ jobs: for binary in alien-deploy alien-operator; do cp "target/aarch64-apple-darwin/release/${binary}" "staged/aarch64-apple-darwin/${binary}" done - cp platform/crates/alien-clix/target/aarch64-apple-darwin/release/alien staged/aarch64-apple-darwin/alien - staged/aarch64-apple-darwin/alien --help >/dev/null - name: Upload aarch64 darwin binaries uses: actions/upload-artifact@v7 @@ -1986,17 +1924,6 @@ jobs: path: .ci-tool-versions persist-credentials: false - - uses: actions/checkout@v7 - with: - repository: alienplatform/platform - ref: ${{ needs.prepare.outputs.platform_ref }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} - path: platform - - - uses: ./platform/.github/actions/setup-composed-cli-dependencies - with: - repo-access-token: ${{ secrets.REPO_ACCESS_TOKEN }} - - name: Restore binaries from cache id: binary-cache uses: actions/cache@v6 @@ -2004,8 +1931,7 @@ jobs: path: | target/x86_64-pc-windows-msvc/release/alien-deploy.exe target/x86_64-pc-windows-msvc/release/alien-operator.exe - platform/crates/alien-clix/target/x86_64-pc-windows-msvc/release/alien.exe - key: release-windows-x86_64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-clix-${{ needs.prepare.outputs.platform_ref }}-b1-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} + key: release-windows-x86_64-v${{ needs.prepare.outputs.version }}-${{ needs.prepare.outputs.source_ref }}-b2-${{ hashFiles('.ci-tool-versions/rust-toolchain.toml', '.ci-tool-versions/mise*.toml') }} - if: steps.binary-cache.outputs.cache-hit != 'true' name: Install Rust toolchain @@ -2049,9 +1975,6 @@ jobs: cargo build --release \ -p alien-deploy-cli -p alien-operator \ --target x86_64-pc-windows-msvc - cmd //c "mklink /J alien ." - cargo build --release --manifest-path platform/crates/alien-clix/Cargo.toml \ - --bin alien --target x86_64-pc-windows-msvc - name: Stage binaries for upload shell: bash @@ -2059,8 +1982,6 @@ jobs: mkdir -p staged/x86_64-pc-windows-msvc cp target/x86_64-pc-windows-msvc/release/alien-deploy.exe staged/x86_64-pc-windows-msvc/ cp target/x86_64-pc-windows-msvc/release/alien-operator.exe staged/x86_64-pc-windows-msvc/ - cp platform/crates/alien-clix/target/x86_64-pc-windows-msvc/release/alien.exe staged/x86_64-pc-windows-msvc/ - staged/x86_64-pc-windows-msvc/alien.exe --help >/dev/null - name: Upload windows binaries uses: actions/upload-artifact@v7 @@ -2117,12 +2038,12 @@ jobs: fi } - for binary in alien alien-deploy alien-operator alien-worker-runtime alien-manager; do + for binary in alien-deploy alien-operator alien-worker-runtime alien-manager; do upload "$binary" x86_64-unknown-linux-musl linux x86_64 upload "$binary" aarch64-unknown-linux-musl linux aarch64 done - for binary in alien alien-deploy alien-operator; do + for binary in alien-deploy alien-operator; do upload "$binary" aarch64-apple-darwin darwin aarch64 done @@ -2146,72 +2067,10 @@ jobs: fi } - for binary in alien alien-deploy alien-operator; do + for binary in alien-deploy alien-operator; do upload_exe "$binary" x86_64-pc-windows-msvc windows x86_64 done - # ─── GitHub Release ──────────────────────────────────────────────── - - qualify-github-release: - needs: [prepare, generate-changelog, build-binaries-linux-x86_64, build-binaries-linux-aarch64, build-binaries-darwin, build-binaries-windows] - if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' }} - runs-on: depot-ubuntu-24.04-arm - timeout-minutes: 10 - steps: - - name: Download all binary artifacts - uses: actions/download-artifact@v8 - with: - pattern: binaries-* - path: ./artifacts - merge-multiple: false - - - name: Download changelog - uses: actions/download-artifact@v8 - with: - name: changelog - path: . - - - name: Create release archives - run: | - VERSION="${{ needs.prepare.outputs.version }}" - mkdir -p dist - - # Linux x86_64 tarball - tar -czf "dist/alien-v${VERSION}-x86_64-unknown-linux-musl.tar.gz" \ - -C artifacts/binaries-x86_64-unknown-linux-musl \ - alien alien-deploy alien-operator alien-worker-runtime alien-manager - - # Linux aarch64 tarball - tar -czf "dist/alien-v${VERSION}-aarch64-unknown-linux-musl.tar.gz" \ - -C artifacts/binaries-aarch64-unknown-linux-musl \ - alien alien-deploy alien-operator alien-worker-runtime alien-manager - - # macOS aarch64 tarball - tar -czf "dist/alien-v${VERSION}-aarch64-apple-darwin.tar.gz" \ - -C artifacts/binaries-aarch64-apple-darwin \ - alien alien-deploy alien-operator - - # Windows x86_64 zip - (cd artifacts/binaries-x86_64-pc-windows-msvc && \ - zip "../../dist/alien-v${VERSION}-x86_64-pc-windows-msvc.zip" \ - alien.exe alien-deploy.exe alien-operator.exe) - - - name: Generate checksums - run: | - VERSION="${{ needs.prepare.outputs.version }}" - cd dist - sha256sum alien-v${VERSION}-*.tar.gz alien-v${VERSION}-*.zip > "alien-v${VERSION}-checksums.txt" - - - name: Upload qualified GitHub release payload - uses: actions/upload-artifact@v7 - with: - name: qualified-github-release - retention-days: 30 - if-no-files-found: error - path: | - CHANGELOG-release.md - dist/ - # ─── Docker images + Helm chart ───────────────────────────────────── qualify-images: @@ -2572,8 +2431,7 @@ jobs: helm push "dist/alien-manager-${VERSION}.tgz" oci://ghcr.io/alienplatform/charts # Publish the GitHub release only after the registries and binary store have - # accepted their qualified artifacts. Homebrew follows because its formula - # references these release assets. + # accepted their qualified artifacts. create-github-release: needs: - prepare @@ -2584,7 +2442,6 @@ jobs: - publish-ai-gateway - upload-binaries - publish-images - - publish-npm-cli-wrapper if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' }} runs-on: depot-ubuntu-24.04-arm timeout-minutes: 10 @@ -2594,12 +2451,12 @@ jobs: ref: ${{ needs.prepare.outputs.release_commit }} fetch-depth: 0 - - name: Download qualified GitHub release payload + - name: Download qualified changelog uses: actions/download-artifact@v8 with: run-id: ${{ needs.prepare.outputs.qualification_run_id }} github-token: ${{ secrets.REPO_ACCESS_TOKEN }} - name: qualified-github-release + name: changelog path: . - name: Require immutable GitHub release protections @@ -2685,7 +2542,7 @@ jobs: echo "exists=true" >> "$GITHUB_OUTPUT" echo "${TAG} points to the qualified release commit" - - name: Verify existing GitHub release assets + - name: Verify existing GitHub release id: verify_release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -2700,29 +2557,10 @@ jobs: exit 1 fi - if [ "$release_count" = "1" ]; then - release=$(jq -c '.[0]' <<< "$releases") - else - release='{"assets":[],"draft":true}' - fi - - complete=true - for file in dist/*; do - name=$(basename "$file") - published=$(jq -r --arg name "$name" \ - '.assets[] | select(.name == $name) | .digest // empty' <<< "$release") - if [ -z "$published" ]; then - complete=false - continue - fi - checksum="sha256:$(sha256sum "$file" | awk '{print $1}')" - if [ "$published" != "$checksum" ]; then - echo "::error::GitHub release asset ${name} exists with different contents" - exit 1 - fi - done - if [ "$(jq -r '.draft' <<< "$release")" = "true" ]; then - complete=false + complete=false + if [ "$release_count" = "1" ] && + [ "$(jq -r '.[0].draft' <<< "$releases")" = "false" ]; then + complete=true fi echo "complete=${complete}" >> "$GITHUB_OUTPUT" @@ -2736,10 +2574,6 @@ jobs: target_commitish: ${{ needs.prepare.outputs.release_commit }} name: Alien v${{ needs.prepare.outputs.version }} body_path: CHANGELOG-release.md - files: | - dist/alien-v*-*.tar.gz - dist/alien-v*-*.zip - dist/alien-v*-checksums.txt draft: true prerelease: false @@ -2810,242 +2644,3 @@ jobs: exit 1 fi echo "${TAG} points to the qualified release commit" - - # ─── Homebrew tap ────────────────────────────────────────────────── - - publish-homebrew-tap: - needs: [prepare, create-github-release] - if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' }} - runs-on: depot-ubuntu-24.04-arm - timeout-minutes: 10 - steps: - - name: Download release checksums - run: | - VERSION="${{ needs.prepare.outputs.version }}" - gh release download "v${VERSION}" \ - --repo "${{ github.repository }}" \ - --pattern "alien-v${VERSION}-checksums.txt" \ - --dir . - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Parse checksums - id: checksums - run: | - VERSION="${{ needs.prepare.outputs.version }}" - FILE="alien-v${VERSION}-checksums.txt" - { - echo "linux_x64=$(grep 'x86_64-unknown-linux-musl' "$FILE" | awk '{print $1}')" - echo "linux_arm64=$(grep 'aarch64-unknown-linux-musl' "$FILE" | awk '{print $1}')" - echo "darwin_arm64=$(grep 'aarch64-apple-darwin' "$FILE" | awk '{print $1}')" - } >> "$GITHUB_OUTPUT" - - - name: Generate Homebrew formula - run: | - VERSION="${{ needs.prepare.outputs.version }}" - REPO="${{ github.repository }}" - cat > alien.rb << 'FORMULA_EOF' - class Alien < Formula - desc "Alien Developer Platform CLI" - homepage "https://alien.dev" - version "VERSION_PLACEHOLDER" - license "Apache-2.0" - - on_macos do - on_arm do - url "https://github.com/REPO_PLACEHOLDER/releases/download/vVERSION_PLACEHOLDER/alien-vVERSION_PLACEHOLDER-aarch64-apple-darwin.tar.gz" - sha256 "SHA_DARWIN_ARM64_PLACEHOLDER" - end - end - - on_linux do - on_intel do - url "https://github.com/REPO_PLACEHOLDER/releases/download/vVERSION_PLACEHOLDER/alien-vVERSION_PLACEHOLDER-x86_64-unknown-linux-musl.tar.gz" - sha256 "SHA_LINUX_X64_PLACEHOLDER" - end - on_arm do - url "https://github.com/REPO_PLACEHOLDER/releases/download/vVERSION_PLACEHOLDER/alien-vVERSION_PLACEHOLDER-aarch64-unknown-linux-musl.tar.gz" - sha256 "SHA_LINUX_ARM64_PLACEHOLDER" - end - end - - def install - bin.install "alien" - bin.install "alien-deploy" - end - - test do - assert_match version.to_s, shell_output("#{bin}/alien --version") - end - end - FORMULA_EOF - - sed -i "s|VERSION_PLACEHOLDER|${VERSION}|g" alien.rb - sed -i "s|REPO_PLACEHOLDER|${REPO}|g" alien.rb - sed -i "s|SHA_LINUX_X64_PLACEHOLDER|${{ steps.checksums.outputs.linux_x64 }}|g" alien.rb - sed -i "s|SHA_LINUX_ARM64_PLACEHOLDER|${{ steps.checksums.outputs.linux_arm64 }}|g" alien.rb - sed -i "s|SHA_DARWIN_ARM64_PLACEHOLDER|${{ steps.checksums.outputs.darwin_arm64 }}|g" alien.rb - - - name: Push formula to tap - run: | - git clone "https://x-access-token:${{ secrets.REPO_ACCESS_TOKEN }}@github.com/alienplatform/homebrew-tap.git" tap - mkdir -p tap/Formula - if [ -f tap/Formula/alien.rb ] && - grep -q "version \"${{ needs.prepare.outputs.version }}\"" tap/Formula/alien.rb; then - if ! cmp -s alien.rb tap/Formula/alien.rb; then - echo "::error::Homebrew already has v${{ needs.prepare.outputs.version }} with different contents" - exit 1 - fi - echo "Homebrew formula already matches v${{ needs.prepare.outputs.version }}" - exit 0 - fi - cp alien.rb tap/Formula/alien.rb - cd tap - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add Formula/alien.rb - git diff --staged --quiet || git commit -m "Update alien to v${{ needs.prepare.outputs.version }}" - git push - - # A release channel is one atomic pointer. Consumers resolve it before - # downloading immutable versioned binaries; independent latest object copies - # cannot provide an atomic release boundary. - publish-stable-channel: - needs: [prepare, publish-homebrew-tap] - if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' }} - runs-on: depot-ubuntu-24.04-arm - timeout-minutes: 5 - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: arn:aws:iam::216938125589:role/alien-releases-upload - aws-region: us-east-1 - - - name: Publish stable channel - run: | - printf 'v%s\n' '${{ needs.prepare.outputs.version }}' > stable - published=$(aws s3 cp \ - s3://alien-releases-prod/channels/stable - 2>/dev/null || true) - if [ "$published" = "$(cat stable)" ]; then - echo "Stable channel already points to ${published}" - exit 0 - fi - aws s3 cp stable s3://alien-releases-prod/channels/stable \ - --cache-control 'no-cache' - aws cloudfront create-invalidation \ - --distribution-id ${{ vars.RELEASES_CLOUDFRONT_DISTRIBUTION_ID }} \ - --paths '/channels/stable' - - # ─── npm CLI wrapper ─────────────────────────────────────────────── - - qualify-npm-cli-wrapper: - needs: [prepare, build-binaries-linux-x86_64, build-binaries-linux-aarch64, build-binaries-darwin, build-binaries-windows] - if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' }} - runs-on: depot-ubuntu-24.04-arm - timeout-minutes: 20 - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ needs.prepare.outputs.source_ref }} - - - uses: actions/checkout@v7 - with: - ref: ${{ github.ref }} - path: .ci-tool-versions - persist-credentials: false - - - name: Install development tools - uses: jdx/mise-action@v5 - env: - MISE_DATA_DIR: ${{ runner.temp }}/mise-tools - MISE_DISABLE_TOOLS: rust - with: - version: 2026.10.1 - add_shims_to_path: false - working_directory: .ci-tool-versions - install_args: node - cache_key: "{{default}}-${{ hashFiles('.ci-tool-versions/package.json') }}" - id: mise - - name: Configure npm registry - uses: actions/setup-node@v7 - with: - registry-url: https://registry.npmjs.org - package-manager-cache: false - - - name: Download all binary artifacts - uses: actions/download-artifact@v8 - with: - pattern: binaries-* - path: ./artifacts - merge-multiple: false - - - name: Build qualified npm CLI packages - env: - MODE: qualify - OUTPUT_DIR: ${{ github.workspace }}/qualified/cli - VERSION: ${{ needs.prepare.outputs.version }} - run: bash scripts/build-npm-packages.sh - - - name: Upload qualified npm CLI packages - uses: actions/upload-artifact@v7 - with: - name: qualified-npm-cli - retention-days: 30 - if-no-files-found: error - path: qualified/cli/ - - publish-npm-cli-wrapper: - needs: prepare - if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' }} - runs-on: depot-ubuntu-24.04-arm - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ needs.prepare.outputs.release_commit }} - - - uses: actions/checkout@v7 - with: - ref: ${{ github.ref }} - path: .ci-tool-versions - persist-credentials: false - - - name: Install development tools - uses: jdx/mise-action@v5 - env: - MISE_DATA_DIR: ${{ runner.temp }}/mise-tools - MISE_DISABLE_TOOLS: rust - with: - version: 2026.10.1 - add_shims_to_path: false - working_directory: .ci-tool-versions - install_args: node - cache_key: "{{default}}-${{ hashFiles('.ci-tool-versions/package.json') }}" - id: mise - - name: Configure npm registry - uses: actions/setup-node@v7 - with: - registry-url: https://registry.npmjs.org - package-manager-cache: false - - - name: Validate npm auth - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm whoami - - - name: Download qualified npm CLI packages - uses: actions/download-artifact@v8 - with: - run-id: ${{ needs.prepare.outputs.qualification_run_id }} - github-token: ${{ secrets.REPO_ACCESS_TOKEN }} - name: qualified-npm-cli - path: qualified/cli - - - name: Publish qualified npm CLI packages - env: - MODE: publish-existing - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - OUTPUT_DIR: ${{ github.workspace }}/qualified/cli - VERSION: ${{ needs.prepare.outputs.version }} - run: bash scripts/build-npm-packages.sh diff --git a/packages/alien-cli-npm/bin/alien.js b/packages/alien-cli-npm/bin/alien.js deleted file mode 100644 index ae7057f89f..0000000000 --- a/packages/alien-cli-npm/bin/alien.js +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env node - -import { spawn } from "node:child_process" -import { existsSync } from "node:fs" -import { createRequire } from "node:module" -import { dirname, join } from "node:path" - -// Map Node.js platform/arch to Rust target triples -const TARGET_MAP = { - "linux-x64": "x86_64-unknown-linux-musl", - "linux-arm64": "aarch64-unknown-linux-musl", - "darwin-arm64": "aarch64-apple-darwin", - "win32-x64": "x86_64-pc-windows-msvc", -} - -const platformKey = `${process.platform}-${process.arch}` -const target = TARGET_MAP[platformKey] - -if (!target) { - console.error(`Unsupported platform: ${platformKey}`) - process.exit(1) -} - -const binaryName = process.platform === "win32" ? "alien.exe" : "alien" - -// Try to find the binary from the platform-specific optional dependency -function findBinary() { - const require = createRequire(import.meta.url) - - // Strategy 1: Look for the platform-specific optional dependency - const platformPkg = `@alienplatform/cli-${platformKey}` - try { - const pkgPath = require.resolve(`${platformPkg}/package.json`) - const vendorDir = join(dirname(pkgPath), "vendor", target) - const binPath = join(vendorDir, binaryName) - if (existsSync(binPath)) return binPath - } catch { - // Not installed (expected on other platforms) - } - - // Strategy 2: Look in local vendor directory (for development / non-npm installs) - const localVendor = join(dirname(import.meta.url.replace("file://", "")), "..", "vendor", target) - const localBin = join(localVendor, binaryName) - if (existsSync(localBin)) return localBin - - return null -} - -const binPath = findBinary() - -if (!binPath) { - console.error( - `Could not find alien binary for ${platformKey}.\nTry reinstalling: npm install -g @alienplatform/cli`, - ) - process.exit(1) -} - -// Spawn the binary, forwarding all arguments and stdio -const child = spawn(binPath, process.argv.slice(2), { - stdio: "inherit", - env: { ...process.env, ALIEN_INSTALL_METHOD: "npm" }, -}) - -// Forward signals to the child process -for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { - process.on(signal, () => { - if (!child.killed) { - child.kill(signal) - } - }) -} - -child.on("close", (code, signal) => { - if (signal) { - process.kill(process.pid, signal) - } else { - process.exit(code ?? 1) - } -}) diff --git a/packages/alien-cli-npm/package.json b/packages/alien-cli-npm/package.json deleted file mode 100644 index a0683eabae..0000000000 --- a/packages/alien-cli-npm/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@alienplatform/cli", - "version": "0.0.0-dev", - "description": "Alien Developer Platform CLI", - "author": "Alien Software, Inc. ", - "license": "FSL-1.1-Apache-2.0", - "homepage": "https://alien.dev", - "bin": { - "alien": "bin/alien.js" - }, - "type": "module", - "engines": { - "node": ">=18" - }, - "files": [ - "bin", - "vendor" - ], - "repository": { - "type": "git", - "url": "https://github.com/alienplatform/alien.git", - "directory": "packages/alien-cli-npm" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81e78b256e..f1f47ab1fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,8 +112,6 @@ importers: specifier: ^4.0.0 version: 4.1.11(@types/node@24.13.6)(msw@2.12.7(@types/node@24.13.6)(typescript@5.9.3))(vite@7.3.0(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.31.1)(tsx@4.21.0)) - packages/alien-cli-npm: {} - packages/bindings: dependencies: '@alienplatform/core': diff --git a/scripts/README.md b/scripts/README.md index 0457aebe58..9e4235c45d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,7 +4,6 @@ Small operational scripts used by root `package.json` commands and GitHub Action ## Release -- **`build-npm-packages.sh`** — Used by `.github/workflows/release.yml` to package and publish `@alienplatform/cli` from downloaded binary artifacts. Requires `VERSION`, `NODE_AUTH_TOKEN`, and the release workflow artifact layout under `./artifacts/`. - **`smoke-sandbox-agent.sh`** — Used by `.github/workflows/release.yml` to qualify the published `alien-sandbox-agent` image on both `linux/amd64` and `linux/arm64`, and usable on any GCP sandbox image built from the same contract. Takes the image reference and needs a Docker daemon that can pull it. `--platforms` narrows the platforms probed, and `--tools docker/sandbox-default-tools.txt` also runs every tool of the default sandbox base as the image's user. `scripts/smoke-sandbox-agent.test.sh` runs every probe and every failure mode against a stub `docker`, so the script itself is testable without a daemon or an image. ## Test environment diff --git a/scripts/build-npm-packages.sh b/scripts/build-npm-packages.sh deleted file mode 100755 index eecf99f2e0..0000000000 --- a/scripts/build-npm-packages.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -# -# Build, qualify, or publish @alienplatform/cli npm packages. -# -# Uses the Codex-style single-package pattern: platform variants are published -# as @alienplatform/cli@{VERSION}-{platform} and the main package at -# @alienplatform/cli@{VERSION} has optionalDependencies using npm aliases. -# -# Required env vars: -# VERSION - Release version (e.g., 1.3.2) -# MODE - qualify, publish, or publish-existing (default: publish) -# OUTPUT_DIR - qualified tarball directory (required outside publish) -# NODE_AUTH_TOKEN - npm auth token (publish modes only) -# -# Expected artifacts layout (from GitHub Actions download-artifact): -# ./artifacts/binaries-x86_64-unknown-linux-musl/{alien,alien-deploy,...} -# ./artifacts/binaries-aarch64-unknown-linux-musl/{alien,alien-deploy,...} -# ./artifacts/binaries-aarch64-apple-darwin/{alien,alien-deploy,...} -# ./artifacts/binaries-x86_64-pc-windows-msvc/{alien.exe,alien-deploy.exe,...} - -set -euo pipefail - -: "${VERSION:?VERSION is required}" -MODE="${MODE:-publish}" -if [[ "$MODE" != "qualify" ]]; then - : "${NODE_AUTH_TOKEN:?NODE_AUTH_TOKEN is required}" -fi -if [[ "$MODE" != "publish" ]]; then - : "${OUTPUT_DIR:?OUTPUT_DIR is required}" - mkdir -p "$OUTPUT_DIR" - OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" -fi - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -WORK_DIR="$(mktemp -d)" -trap 'rm -rf "$WORK_DIR"' EXIT - -publish_if_missing() { - local package_spec="$1" - local tarball="$2" - local tag="$3" - - local integrity - integrity="sha512-$(openssl dgst -sha512 -binary "$tarball" | openssl base64 -A)" - local published - published="$(npm view "$package_spec" dist.integrity 2>/dev/null || true)" - if [[ -n "$published" ]]; then - if [[ "$published" != "$integrity" ]]; then - echo "error: $package_spec exists with different contents" >&2 - exit 1 - fi - echo "Skipping $package_spec; qualified contents are already published" - return - fi - - npm publish "$tarball" --tag "$tag" -} - -qualify_or_publish() { - local package_spec="$1" - local tarball="$2" - local tag="$3" - - if [[ "$MODE" == "qualify" ]]; then - cp "$tarball" "$OUTPUT_DIR/" - else - publish_if_missing "$package_spec" "$tarball" "$tag" - fi -} - -if [[ "$MODE" == "publish-existing" ]]; then - for npm_suffix in linux-x64 linux-arm64 darwin-arm64 win32-x64; do - qualify_or_publish \ - "@alienplatform/cli@${VERSION}-${npm_suffix}" \ - "${OUTPUT_DIR}/alienplatform-cli-${VERSION}-${npm_suffix}.tgz" \ - "$npm_suffix" - done - qualify_or_publish \ - "@alienplatform/cli@${VERSION}" \ - "${OUTPUT_DIR}/alienplatform-cli-${VERSION}.tgz" \ - latest - exit 0 -fi - -# Platform definitions: npm_suffix target_triple os cpu binary_ext -PLATFORMS=( - "linux-x64 x86_64-unknown-linux-musl linux x64 " - "linux-arm64 aarch64-unknown-linux-musl linux arm64 " - "darwin-arm64 aarch64-apple-darwin darwin arm64 " - "win32-x64 x86_64-pc-windows-msvc win32 x64 .exe" -) - -echo "==> Building npm packages for @alienplatform/cli v${VERSION}" - -# ── Step 1: Build platform-specific packages ───────────────────────── - -for platform_def in "${PLATFORMS[@]}"; do - read -r npm_suffix target os cpu ext <<< "$platform_def" - - echo "" - echo "--- Building @alienplatform/cli@${VERSION}-${npm_suffix} ---" - - pkg_dir="${WORK_DIR}/cli-${npm_suffix}" - vendor_dir="${pkg_dir}/vendor/${target}" - mkdir -p "$vendor_dir" - - # Copy binaries - src_dir="./artifacts/binaries-${target}" - for binary in alien alien-deploy; do - cp "${src_dir}/${binary}${ext}" "${vendor_dir}/${binary}${ext}" - chmod +x "${vendor_dir}/${binary}${ext}" - done - - # Generate package.json for platform package - cat > "${pkg_dir}/package.json" << EOF -{ - "name": "@alienplatform/cli", - "version": "${VERSION}-${npm_suffix}", - "description": "Alien CLI binary for ${os}-${cpu}", - "os": ["${os}"], - "cpu": ["${cpu}"], - "files": ["vendor"], - "publishConfig": { - "access": "public" - } -} -EOF - - # Pack and publish with platform-specific tag - (cd "$pkg_dir" && npm pack) - qualify_or_publish "@alienplatform/cli@${VERSION}-${npm_suffix}" "${pkg_dir}/"*.tgz "${npm_suffix}" -done - -# ── Step 2: Build the main package ─────────────────────────────────── - -echo "" -echo "--- Building @alienplatform/cli@${VERSION} (main) ---" - -main_dir="${WORK_DIR}/cli-main" -mkdir -p "${main_dir}/bin" - -# Copy JS shim -cp "${REPO_ROOT}/packages/alien-cli-npm/bin/alien.js" "${main_dir}/bin/alien.js" - -# Generate package.json with injected optionalDependencies -cat > "${main_dir}/package.json" << EOF -{ - "name": "@alienplatform/cli", - "version": "${VERSION}", - "description": "Alien Developer Platform CLI", - "license": "Apache-2.0", - "bin": { - "alien": "bin/alien.js" - }, - "type": "module", - "engines": { - "node": ">=18" - }, - "files": ["bin"], - "repository": { - "type": "git", - "url": "https://github.com/alienplatform/alien.git", - "directory": "packages/alien-cli-npm" - }, - "publishConfig": { - "access": "public" - }, - "optionalDependencies": { - "@alienplatform/cli-linux-x64": "npm:@alienplatform/cli@${VERSION}-linux-x64", - "@alienplatform/cli-linux-arm64": "npm:@alienplatform/cli@${VERSION}-linux-arm64", - "@alienplatform/cli-darwin-arm64": "npm:@alienplatform/cli@${VERSION}-darwin-arm64", - "@alienplatform/cli-win32-x64": "npm:@alienplatform/cli@${VERSION}-win32-x64" - } -} -EOF - -(cd "$main_dir" && npm pack) -qualify_or_publish "@alienplatform/cli@${VERSION}" "${main_dir}/"*.tgz latest - -echo "" -echo "==> Done! ${MODE} @alienplatform/cli@${VERSION}" diff --git a/scripts/release-routing.test.mjs b/scripts/release-routing.test.mjs index bc49ea610c..2fe6a196fc 100644 --- a/scripts/release-routing.test.mjs +++ b/scripts/release-routing.test.mjs @@ -32,14 +32,6 @@ function parseJobs(source) { return jobs } -function jobBlock(name) { - const start = workflow.indexOf(` ${name}:\n`) - assert.notEqual(start, -1, `release workflow has ${name}`) - const remainder = workflow.slice(start + 1) - const next = remainder.search(/^ {2}[a-z0-9_-]+:$/m) - return next === -1 ? workflow.slice(start) : workflow.slice(start, start + 1 + next) -} - test("stable remains the default release mode", () => { assert.match( workflow, @@ -83,44 +75,3 @@ test("dev mode can reach only the reusable npm dev workflow", () => { assert.match(job.if, /inputs\.mode == 'stable'/, `${name} must be unreachable in dev mode`) } }) - -test("stable binary releases publish the pinned Platform composition as alien", () => { - assert.match(workflow, /ref=\$\(tr -d '\[:space:\]' < \.github\/official-cli-platform-revision\)/) - const targets = new Map([ - ["build-binaries-linux-x86_64", ["x86_64-unknown-linux-musl", "alien"]], - ["build-binaries-linux-aarch64", ["aarch64-unknown-linux-musl", "alien"]], - ["build-binaries-darwin", ["aarch64-apple-darwin", "alien"]], - ["build-binaries-windows", ["x86_64-pc-windows-msvc", "alien.exe"]], - ]) - - for (const [job, [target, binary]] of targets) { - const block = jobBlock(job) - assert.match(block, /repository: alienplatform\/platform/) - assert.match(block, /ref: \$\{\{ needs\.prepare\.outputs\.platform_ref \}\}/) - assert.match( - block, - /repository: alienplatform\/platform[\s\S]*?- uses: \.\/platform\/\.github\/actions\/setup-composed-cli-dependencies\n {8}with:\n {10}repo-access-token: \$\{\{ secrets\.REPO_ACCESS_TOKEN \}\}/, - `${job} must prepare the pinned Platform checkout before building the composed CLI`, - ) - assert.match( - block, - /key: .*\$\{\{ needs\.prepare\.outputs\.source_ref \}\}.*\$\{\{ needs\.prepare\.outputs\.platform_ref \}\}/, - ) - assert.match( - block, - new RegExp( - `manifest-path platform/crates/alien-clix/Cargo\\.toml[\\s\\S]{0,160}--bin alien --target ${target}`, - ), - ) - assert.match( - block, - new RegExp(`cp platform/crates/alien-clix/target/${target}/release/${binary} `), - ) - assert.match( - block, - new RegExp(`staged/${target}/${binary.replace(".", "\\.")} --help >/dev/null`), - ) - } - - assert.doesNotMatch(workflow, /-p alien-cli -p alien-deploy-cli/) -}) From a366c5e09a317dc07311c3bb0b89ac522ace4c9f Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Mon, 5 Oct 2026 14:48:41 +0300 Subject: [PATCH 2/2] ci: create only the release tag; the CLI distribution creates the release The GitHub release keeps archives that include the alien CLI, and immutable releases cannot gain assets after publication, so the release is created by the distribution that builds the CLI, from this tag. The changelog is rendered there from cliff.toml. Co-Authored-By: Claude Opus 5.5 --- .github/RELEASING.md | 15 +- .github/workflows/release-stable.yml | 2 +- .github/workflows/release.yml | 224 +++++---------------------- 3 files changed, 47 insertions(+), 194 deletions(-) diff --git a/.github/RELEASING.md b/.github/RELEASING.md index b47adbf7ab..be3c28c7b6 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -17,7 +17,6 @@ The **Release qualification** check then: - builds and smoke-tests every native addon, binary, and container image; - packs every Cargo and npm package; -- creates the changelog; - records the source Git tree plus every artifact checksum and image digest. Qualification artifacts are retained for 30 days. If they expire, rerun @@ -51,10 +50,12 @@ existing version only when its crates.io checksum matches. If publication stops, fix the external failure and rerun the same publish action. Never prepare another version to recover a partial release. -The immutable Git tag and GitHub release are created only after package -registries, images, and versioned binary objects have accepted the qualified -artifacts. The GitHub release carries the changelog. +The immutable Git tag is created only after package registries, images, and +versioned binary objects have accepted the qualified artifacts. -This workflow does not publish the `alien` CLI, its npm package, the Homebrew -formula, or the `channels/stable` pointer. Those are published by the official -CLI distribution after this release, from the same release tag. +This workflow does not publish the `alien` CLI, its npm package, the GitHub +release, the Homebrew formula, or the `channels/stable` pointer. The official +CLI distribution publishes those from the release tag: it builds `alien`, +creates the immutable GitHub release with the changelog (rendered from +`cliff.toml`) and the archives of `alien` plus this release's binaries, then +updates Homebrew and, last, `channels/stable`. diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index ae2f7fa607..8f457b66b4 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -94,7 +94,7 @@ jobs: ai-gateway-bin-linux-arm64 ai-gateway-bin-linux-x64 \ binaries-aarch64-apple-darwin binaries-aarch64-unknown-linux-musl \ binaries-x86_64-pc-windows-msvc binaries-x86_64-unknown-linux-musl \ - changelog qualified-ai-gateway qualified-bindings \ + qualified-ai-gateway qualified-bindings \ qualified-client-sdks qualified-crates \ qualified-image-digests qualified-npm \ qualified-python-linux-aarch64 qualified-python-linux-x86_64 \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c436fd05f8..f105057078 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -461,37 +461,6 @@ jobs: echo "ready=false" >> "$GITHUB_OUTPUT" fi - # ─── Changelog generation ────────────────────────────────────────── - - generate-changelog: - needs: prepare - if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' }} - runs-on: depot-ubuntu-24.04-arm - timeout-minutes: 5 - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ needs.prepare.outputs.source_ref }} - fetch-depth: 0 - - - name: Generate changelog - id: changelog - uses: orhun/git-cliff-action@v4 - with: - config: cliff.toml - # The release tag does not exist yet: render the commits since the - # previous tag under the new version. - args: --unreleased --tag v${{ needs.prepare.outputs.version }} --strip header - env: - OUTPUT: CHANGELOG-release.md - - - name: Upload changelog - uses: actions/upload-artifact@v7 - with: - name: changelog - retention-days: 30 - path: CHANGELOG-release.md - # ─── Registry publishing (crates.io + npm) ────────────────────────── publish-crates: @@ -2430,9 +2399,11 @@ jobs: --version "$VERSION" --app-version "$VERSION" --destination dist helm push "dist/alien-manager-${VERSION}.tgz" oci://ghcr.io/alienplatform/charts - # Publish the GitHub release only after the registries and binary store have - # accepted their qualified artifacts. - create-github-release: + # Create the immutable release tag only after the registries and binary + # store have accepted their qualified artifacts. The GitHub release for the + # tag, with the archives of the full binary set, is created by the official + # CLI distribution, which needs this tag and these binaries first. + create-release-tag: needs: - prepare - publish-crates @@ -2444,22 +2415,9 @@ jobs: - publish-images if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' }} runs-on: depot-ubuntu-24.04-arm - timeout-minutes: 10 + timeout-minutes: 5 steps: - - uses: actions/checkout@v7 - with: - ref: ${{ needs.prepare.outputs.release_commit }} - fetch-depth: 0 - - - name: Download qualified changelog - uses: actions/download-artifact@v8 - with: - run-id: ${{ needs.prepare.outputs.qualification_run_id }} - github-token: ${{ secrets.REPO_ACCESS_TOKEN }} - name: changelog - path: . - - - name: Require immutable GitHub release protections + - name: Require immutable release protections env: GH_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} run: | @@ -2499,148 +2457,42 @@ jobs: exit 1 fi - - name: Verify existing release tag - id: verify_tag + - name: Create or verify the release tag env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: v${{ needs.prepare.outputs.version }} + COMMIT: ${{ needs.prepare.outputs.release_commit }} run: | - VERSION="${{ needs.prepare.outputs.version }}" - TAG="v${VERSION}" - COMMIT="${{ needs.prepare.outputs.release_commit }}" - FULL_REF="refs/tags/${TAG}" - - MATCHING_REFS=$(gh api --paginate --slurp \ - "/repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${TAG}" | - jq -c --arg ref "$FULL_REF" '[add[] | select(.ref == $ref)]') - MATCH_COUNT=$(jq 'length' <<< "$MATCHING_REFS") - if [ "$MATCH_COUNT" = "0" ]; then - echo "${TAG} does not exist; GitHub Release creation will create it" - echo "exists=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [ "$MATCH_COUNT" != "1" ]; then - echo "::error::Expected at most one exact ${FULL_REF}" - exit 1 - fi - - REF_JSON=$(jq -c '.[0]' <<< "$MATCHING_REFS") - TAG_TYPE=$(jq -r '.object.type' <<< "$REF_JSON") - TAG_COMMIT=$(jq -r '.object.sha' <<< "$REF_JSON") - if [ "$TAG_TYPE" = "tag" ]; then - TAG_COMMIT=$(gh api \ - "/repos/${GITHUB_REPOSITORY}/git/tags/${TAG_COMMIT}" \ - --jq '.object.sha') - elif [ "$TAG_TYPE" != "commit" ]; then - echo "::error::${TAG} points to unsupported Git object type ${TAG_TYPE}" - exit 1 - fi - - if [ "$TAG_COMMIT" != "$COMMIT" ]; then - echo "::error::${TAG} already points to ${TAG_COMMIT}, expected ${COMMIT}" - exit 1 - fi - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "${TAG} points to the qualified release commit" - - - name: Verify existing GitHub release - id: verify_release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ needs.prepare.outputs.version }} - run: | - releases=$(gh api --paginate --slurp \ - "/repos/${GITHUB_REPOSITORY}/releases" | - jq -c --arg tag "v${VERSION}" '[add[] | select(.tag_name == $tag)]') - release_count=$(jq 'length' <<< "$releases") - if [ "$release_count" -gt 1 ]; then - echo "::error::Multiple GitHub releases use v${VERSION}" - exit 1 - fi - - complete=false - if [ "$release_count" = "1" ] && - [ "$(jq -r '.[0].draft' <<< "$releases")" = "false" ]; then - complete=true - fi - echo "complete=${complete}" >> "$GITHUB_OUTPUT" - - - name: Stage GitHub Release - id: stage_release - if: ${{ steps.verify_release.outputs.complete != 'true' }} - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - tag_name: v${{ needs.prepare.outputs.version }} - target_commitish: ${{ needs.prepare.outputs.release_commit }} - name: Alien v${{ needs.prepare.outputs.version }} - body_path: CHANGELOG-release.md - draft: true - prerelease: false - - - name: Publish staged GitHub Release - if: ${{ steps.verify_release.outputs.complete != 'true' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${{ needs.prepare.outputs.version }}" - TAG="v${VERSION}" - COMMIT="${{ needs.prepare.outputs.release_commit }}" - - staged_release=$(gh api \ - "/repos/${GITHUB_REPOSITORY}/releases/${{ steps.stage_release.outputs.id }}") - if ! jq -e \ - --arg tag "$TAG" \ - --arg commit "$COMMIT" \ - '.draft == true and - .tag_name == $tag and - .target_commitish == $commit' \ - <<< "$staged_release" >/dev/null; then - echo "::error::Staged release does not target the qualified commit" - exit 1 - fi - - published_release=$(gh api \ - --method PATCH \ - "/repos/${GITHUB_REPOSITORY}/releases/${{ steps.stage_release.outputs.id }}" \ - --field draft=false \ - --raw-field make_latest=true) - if ! jq -e '.draft == false and .immutable == true' \ - <<< "$published_release" >/dev/null; then - echo "::error::GitHub did not publish an immutable release" - exit 1 - fi - - - name: Verify published release tag - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${{ needs.prepare.outputs.version }}" - TAG="v${VERSION}" - COMMIT="${{ needs.prepare.outputs.release_commit }}" - FULL_REF="refs/tags/${TAG}" - - MATCHING_REFS=$(gh api --paginate --slurp \ - "/repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${TAG}" | - jq -c --arg ref "$FULL_REF" '[add[] | select(.ref == $ref)]') - if [ "$(jq 'length' <<< "$MATCHING_REFS")" != "1" ]; then - echo "::error::Expected exactly one ${FULL_REF} after release publication" - exit 1 - fi + tag_commit() { + local refs ref_json tag_type sha + refs=$(gh api --paginate --slurp \ + "/repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${TAG}" | + jq -c --arg ref "refs/tags/${TAG}" '[add[] | select(.ref == $ref)]') + case "$(jq 'length' <<< "$refs")" in + 0) return 0 ;; + 1) ;; + *) echo "::error::Expected at most one refs/tags/${TAG}" >&2; exit 1 ;; + esac + ref_json=$(jq -c '.[0]' <<< "$refs") + tag_type=$(jq -r '.object.type' <<< "$ref_json") + sha=$(jq -r '.object.sha' <<< "$ref_json") + if [ "$tag_type" = "tag" ]; then + sha=$(gh api "/repos/${GITHUB_REPOSITORY}/git/tags/${sha}" --jq '.object.sha') + elif [ "$tag_type" != "commit" ]; then + echo "::error::${TAG} points to unsupported Git object type ${tag_type}" >&2 + exit 1 + fi + echo "$sha" + } - REF_JSON=$(jq -c '.[0]' <<< "$MATCHING_REFS") - TAG_TYPE=$(jq -r '.object.type' <<< "$REF_JSON") - TAG_COMMIT=$(jq -r '.object.sha' <<< "$REF_JSON") - if [ "$TAG_TYPE" = "tag" ]; then - TAG_COMMIT=$(gh api \ - "/repos/${GITHUB_REPOSITORY}/git/tags/${TAG_COMMIT}" \ - --jq '.object.sha') - elif [ "$TAG_TYPE" != "commit" ]; then - echo "::error::${TAG} points to unsupported Git object type ${TAG_TYPE}" - exit 1 + existing=$(tag_commit) + if [ -z "$existing" ]; then + gh api --method POST "/repos/${GITHUB_REPOSITORY}/git/refs" \ + -f ref="refs/tags/${TAG}" -f sha="$COMMIT" >/dev/null + existing=$(tag_commit) fi - - if [ "$TAG_COMMIT" != "$COMMIT" ]; then - echo "::error::${TAG} points to ${TAG_COMMIT}, expected ${COMMIT}" + if [ "$existing" != "$COMMIT" ]; then + echo "::error::${TAG} points to ${existing}, expected ${COMMIT}" exit 1 fi echo "${TAG} points to the qualified release commit"