Repository navigation
Release #140
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: Release | |
| on: | |
| workflow_call: | |
| inputs: | |
| mode: | |
| type: string | |
| required: true | |
| stable_action: | |
| type: string | |
| required: true | |
| source_ref: | |
| type: string | |
| required: false | |
| default: main | |
| qualification_run_id: | |
| type: string | |
| required: false | |
| default: "" | |
| dry_run: | |
| type: boolean | |
| required: false | |
| default: false | |
| outputs: | |
| version: | |
| value: ${{ jobs.prepare.outputs.version }} | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: Publication channel | |
| type: choice | |
| default: stable | |
| options: [stable, dev] | |
| stable_action: | |
| description: Prepare a version PR | |
| type: choice | |
| default: prepare | |
| options: [prepare] | |
| release_branch: | |
| description: Linear-derived branch name for the version PR (prepare only) | |
| type: string | |
| default: "" | |
| source_ref: | |
| description: Exact 40-character source commit for dev npm packages | |
| type: string | |
| default: main | |
| dry_run: | |
| description: Validate without publishing | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| env: | |
| # Fall back to local compilation when the shared sccache backend returns a | |
| # transient error (e.g. webdav 403) instead of failing the whole build. | |
| SCCACHE_IGNORE_SERVER_IO_ERROR: "1" | |
| jobs: | |
| publish-npm-dev: | |
| if: inputs.mode == 'dev' | |
| uses: ./.github/workflows/publish-npm-dev.yml | |
| with: | |
| source_ref: ${{ inputs.source_ref }} | |
| dry_run: ${{ inputs.dry_run }} | |
| secrets: inherit | |
| prepare: | |
| if: inputs.mode == 'stable' | |
| runs-on: depot-ubuntu-24.04-arm | |
| timeout-minutes: 10 | |
| outputs: | |
| version: ${{ steps.bump.outputs.version || steps.qualification_version.outputs.version || steps.merged_version.outputs.version }} | |
| ready: ${{ steps.release_state.outputs.ready }} | |
| source_ref: ${{ steps.release_source.outputs.ref }} | |
| qualification_run_id: ${{ steps.qualification.outputs.run_id }} | |
| qualification_head_sha: ${{ steps.qualification.outputs.head_sha }} | |
| release_commit: ${{ steps.merged_version.outputs.commit }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| fetch-depth: 0 | |
| - name: Compute next version | |
| id: bump | |
| if: ${{ inputs.stable_action == 'prepare' || inputs.dry_run }} | |
| run: | | |
| LATEST_TAG=$(git tag --list "v[0-9]*" --sort=-v:refname | head -n1) | |
| if [ -z "$LATEST_TAG" ]; then | |
| LATEST_TAG="v0.0.0" | |
| fi | |
| CURRENT_VERSION=${LATEST_TAG#v} | |
| echo "Latest tag: $LATEST_TAG" | |
| WORKSPACE_VERSION=$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml) | |
| if [ "$WORKSPACE_VERSION" != "$CURRENT_VERSION" ]; then | |
| echo "::error::main already contains unreleased version v${WORKSPACE_VERSION}; run the stable publish action instead" | |
| exit 1 | |
| fi | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" | |
| VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" | |
| echo "Bump type: patch" | |
| echo "New version: v${VERSION}" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Read qualification version | |
| id: qualification_version | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| run: | | |
| VERSION=$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml) | |
| LATEST_TAG=$(git tag --list "v[0-9]*" --sort=-v:refname | head -n1) | |
| if [ "v${VERSION}" = "$LATEST_TAG" ]; then | |
| echo "::error::Qualification requires an unreleased version commit" | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Validate qualification package versions | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| env: | |
| VERSION: ${{ steps.qualification_version.outputs.version }} | |
| run: | | |
| cargo metadata --format-version 1 --locked >/dev/null | |
| for pkg in packages/core packages/commands packages/sdk packages/testing \ | |
| client-sdks/platform/typescript client-sdks/manager/typescript \ | |
| packages/bindings crates/alien-bindings-node \ | |
| packages/bindings/npm/darwin-arm64 packages/bindings/npm/darwin-x64 \ | |
| packages/bindings/npm/linux-x64-gnu packages/bindings/npm/linux-arm64-gnu; do | |
| ACTUAL=$(node -p "require('./${pkg}/package.json').version") | |
| if [ "$ACTUAL" != "$VERSION" ]; then | |
| echo "::error::${pkg}/package.json has version ${ACTUAL}, expected ${VERSION}" | |
| exit 1 | |
| fi | |
| done | |
| - name: Read merged release version | |
| id: merged_version | |
| if: ${{ inputs.stable_action == 'publish' && !inputs.dry_run }} | |
| run: | | |
| if [ "${GITHUB_REF}" != "refs/heads/main" ]; then | |
| echo "::error::Stable publishing must run from main" | |
| exit 1 | |
| fi | |
| VERSION=$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml) | |
| VERSION_TAG="v${VERSION}" | |
| if git rev-parse --verify --quiet "refs/tags/${VERSION_TAG}" >/dev/null; then | |
| RELEASE_COMMIT=$(git rev-list -n1 "$VERSION_TAG") | |
| else | |
| LATEST_TAG=$(git tag --list "v[0-9]*" --sort=-v:refname | head -n1) | |
| RELEASE_COMMIT="" | |
| while read -r COMMIT; do | |
| COMMIT_VERSION=$(git show "${COMMIT}:Cargo.toml" | sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p') | |
| if [ "$COMMIT_VERSION" = "$VERSION" ]; then | |
| RELEASE_COMMIT="$COMMIT" | |
| break | |
| fi | |
| done < <(git rev-list --reverse "${LATEST_TAG}..HEAD") | |
| fi | |
| if [ -z "$RELEASE_COMMIT" ]; then | |
| echo "::error::Could not find the merged commit that introduced v${VERSION}" | |
| exit 1 | |
| fi | |
| COMMIT_VERSION=$(git show "${RELEASE_COMMIT}:Cargo.toml" | | |
| sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p') | |
| if [ "$COMMIT_VERSION" != "$VERSION" ]; then | |
| echo "::error::${VERSION_TAG} does not point to a v${VERSION} source tree" | |
| exit 1 | |
| fi | |
| echo "Publishing merged release v${VERSION} from ${RELEASE_COMMIT}" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "commit=$RELEASE_COMMIT" >> "$GITHUB_OUTPUT" | |
| - name: Set workspace Cargo version | |
| if: ${{ inputs.stable_action == 'prepare' || inputs.dry_run }} | |
| run: | | |
| python3 - <<EOF | |
| import re, os | |
| v = os.environ['VERSION'] | |
| with open('Cargo.toml', 'r') as f: | |
| content = f.read() | |
| content = re.sub(r'(\[workspace\.package\]\nversion = )"[^"]+"', rf'\1"{v}"', content) | |
| content = re.sub(r'(path = "[^"]+", version = )"[^"]+"', rf'\1"{v}"', content) | |
| with open('Cargo.toml', 'w') as f: | |
| f.write(content) | |
| EOF | |
| env: | |
| VERSION: ${{ steps.bump.outputs.version }} | |
| - name: Set JavaScript package versions | |
| if: ${{ inputs.stable_action == 'prepare' || inputs.dry_run }} | |
| run: | | |
| VERSION="${{ steps.bump.outputs.version }}" | |
| # The bindings addon ships as a wrapper + per-platform prebuild packages | |
| # (crate manifest, wrapper, and each npm/<triple> skeleton) that must all | |
| # carry the release version BEFORE the native build on the runners, so the | |
| # loader embeds it and publish injects matching optionalDependencies. | |
| for pkg in packages/core packages/commands packages/sdk packages/testing \ | |
| client-sdks/platform/typescript client-sdks/manager/typescript \ | |
| packages/bindings crates/alien-bindings-node \ | |
| packages/bindings/npm/darwin-arm64 packages/bindings/npm/darwin-x64 \ | |
| packages/bindings/npm/linux-x64-gnu packages/bindings/npm/linux-arm64-gnu \ | |
| packages/ai-gateway; do | |
| node -e " | |
| const fs = require('fs'); | |
| const path = '${pkg}/package.json'; | |
| const content = fs.readFileSync(path, 'utf8'); | |
| fs.writeFileSync(path, content.replace(/\"version\": \"[^\"]*\"/, '\"version\": \"${VERSION}\"')); | |
| " | |
| done | |
| # Both generated SDKs publish a JSR manifest alongside package.json. | |
| # Keep the two registries on the release version. | |
| node -e " | |
| const fs = require('fs'); | |
| for (const path of [ | |
| 'client-sdks/platform/typescript/jsr.json', | |
| 'client-sdks/manager/typescript/jsr.json', | |
| ]) { | |
| const content = fs.readFileSync(path, 'utf8'); | |
| fs.writeFileSync(path, content.replace(/\"version\": \"[^\"]*\"/, '\"version\": \"${VERSION}\"')); | |
| } | |
| " | |
| # Stamp package metadata in generated npm locks when present. | |
| node -e " | |
| const fs = require('fs'); | |
| for (const sdk of ['platform', 'manager']) { | |
| const lockPath = 'client-sdks/' + sdk + '/typescript/package-lock.json'; | |
| if (!fs.existsSync(lockPath)) continue; | |
| const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); | |
| lock.version = '${VERSION}'; | |
| lock.packages[''].version = '${VERSION}'; | |
| fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\\n'); | |
| } | |
| " | |
| # Both checked-in generation workflows use the version from gen.yaml. | |
| # Advance them with the release so later regeneration cannot reset manifests. | |
| node -e " | |
| const fs = require('fs'); | |
| for (const sdk of ['platform', 'manager']) { | |
| const root = 'client-sdks/' + sdk + '/typescript/.speakeasy/'; | |
| const configPath = root + 'gen.yaml'; | |
| const config = fs.readFileSync(configPath, 'utf8'); | |
| const configVersionPattern = /(typescript:\\n version: )[^\\n]+/; | |
| if (!configVersionPattern.test(config)) { | |
| throw new Error(sdk + ' SDK version not found in gen.yaml'); | |
| } | |
| const updatedConfig = config.replace( | |
| configVersionPattern, | |
| (_, prefix) => prefix + '${VERSION}', | |
| ); | |
| fs.writeFileSync(configPath, updatedConfig); | |
| const lockPath = root + 'gen.lock'; | |
| const lock = fs.readFileSync(lockPath, 'utf8'); | |
| const lockVersionPattern = /( releaseVersion: )[^\\n]+/; | |
| if (!lockVersionPattern.test(lock)) { | |
| throw new Error(sdk + ' SDK releaseVersion not found in gen.lock'); | |
| } | |
| const updatedLock = lock.replace( | |
| lockVersionPattern, | |
| (_, prefix) => prefix + '${VERSION}', | |
| ); | |
| fs.writeFileSync(lockPath, updatedLock); | |
| } | |
| " | |
| - name: Regenerate Cargo lockfile | |
| if: ${{ inputs.stable_action == 'prepare' || inputs.dry_run }} | |
| run: cargo metadata --format-version 1 >/dev/null | |
| # Dry-run build jobs start from the untagged commit, so carry the exact | |
| # versioned manifests that a real release would commit into those fresh | |
| # checkouts. This keeps the addon binary, wrapper, and platform packages | |
| # on one computed version without mutating git or publishing anything. | |
| - name: Upload versioned manifests for dry run | |
| if: ${{ inputs.dry_run }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: release-version-manifests | |
| retention-days: 30 | |
| path: | | |
| Cargo.toml | |
| Cargo.lock | |
| packages/core/package.json | |
| packages/commands/package.json | |
| packages/sdk/package.json | |
| packages/testing/package.json | |
| client-sdks/platform/typescript/package.json | |
| client-sdks/platform/typescript/jsr.json | |
| client-sdks/platform/typescript/package-lock.json | |
| client-sdks/platform/typescript/.speakeasy/gen.yaml | |
| client-sdks/platform/typescript/.speakeasy/gen.lock | |
| client-sdks/manager/typescript/package.json | |
| client-sdks/manager/typescript/jsr.json | |
| client-sdks/manager/typescript/package-lock.json | |
| client-sdks/manager/typescript/examples/package-lock.json | |
| client-sdks/manager/typescript/.speakeasy/gen.yaml | |
| client-sdks/manager/typescript/.speakeasy/gen.lock | |
| packages/bindings/package.json | |
| crates/alien-bindings-node/package.json | |
| packages/bindings/npm/darwin-arm64/package.json | |
| packages/bindings/npm/darwin-x64/package.json | |
| packages/bindings/npm/linux-x64-gnu/package.json | |
| packages/bindings/npm/linux-arm64-gnu/package.json | |
| packages/ai-gateway/package.json | |
| - name: Open release pull request | |
| if: ${{ inputs.stable_action == 'prepare' && !inputs.dry_run }} | |
| env: | |
| GH_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| RELEASE_BRANCH: ${{ inputs.release_branch }} | |
| run: | | |
| VERSION="${{ steps.bump.outputs.version }}" | |
| if ! grep -qE '^[a-z0-9][a-z0-9._-]*/alien-[0-9]+-[a-z0-9]+(-[a-z0-9]+)*$' <<< "$RELEASE_BRANCH"; then | |
| echo "::error::release_branch must be the Linear-derived branch for the release issue" | |
| exit 1 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add --update | |
| git commit -m "chore: release v${VERSION}" | |
| BRANCH="$RELEASE_BRANCH" | |
| if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then | |
| PR_URL=$(gh pr list --head "$BRANCH" --state open --json url --jq '.[0].url') | |
| if [ -z "$PR_URL" ]; then | |
| echo "::error::${BRANCH} exists without an open release PR" | |
| exit 1 | |
| fi | |
| echo "Release PR already exists: ${PR_URL}" | |
| exit 0 | |
| fi | |
| git switch -c "$BRANCH" | |
| git push --set-upstream origin "$BRANCH" | |
| gh pr create \ | |
| --base main \ | |
| --head "$BRANCH" \ | |
| --title "chore: release v${VERSION}" \ | |
| --body "Version-only release PR generated by the stable release workflow. The **Release qualification** check builds and records the exact artifact set. After every required check passes and this merges, run the stable **publish** action from \`main\` to promote those qualified artifacts." | |
| - name: Validate merged package versions | |
| if: ${{ inputs.stable_action == 'publish' && !inputs.dry_run }} | |
| env: | |
| VERSION: ${{ steps.merged_version.outputs.version }} | |
| run: | | |
| git checkout --detach "${{ steps.merged_version.outputs.commit }}" | |
| cargo metadata --format-version 1 --locked >/dev/null | |
| for pkg in packages/core packages/commands packages/sdk packages/testing \ | |
| client-sdks/platform/typescript client-sdks/manager/typescript \ | |
| packages/bindings crates/alien-bindings-node \ | |
| packages/bindings/npm/darwin-arm64 packages/bindings/npm/darwin-x64 \ | |
| packages/bindings/npm/linux-x64-gnu packages/bindings/npm/linux-arm64-gnu; do | |
| ACTUAL=$(node -p "require('./${pkg}/package.json').version") | |
| if [ "$ACTUAL" != "$VERSION" ]; then | |
| echo "::error::${pkg}/package.json has version ${ACTUAL}, expected ${VERSION}" | |
| exit 1 | |
| fi | |
| done | |
| - name: Select same-run qualification artifacts | |
| id: qualification | |
| if: ${{ inputs.stable_action == 'publish' && !inputs.dry_run }} | |
| env: | |
| RELEASE_COMMIT: ${{ steps.merged_version.outputs.commit }} | |
| QUALIFICATION_RUN_ID: ${{ inputs.qualification_run_id }} | |
| run: | | |
| if ! grep -qE '^[0-9]+$' <<< "$QUALIFICATION_RUN_ID"; then | |
| echo "::error::Stable publication requires its orchestrator run ID" | |
| exit 1 | |
| fi | |
| echo "run_id=$QUALIFICATION_RUN_ID" >> "$GITHUB_OUTPUT" | |
| echo "head_sha=$RELEASE_COMMIT" >> "$GITHUB_OUTPUT" | |
| - name: Select release source | |
| id: release_source | |
| run: | | |
| if [ "${{ inputs.stable_action }}" = "qualification" ]; then | |
| echo "ref=${{ inputs.source_ref }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "ref=${{ steps.merged_version.outputs.commit }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Select downstream release state | |
| id: release_state | |
| run: | | |
| if [ "${{ inputs.stable_action }}" = "qualification" ] || \ | |
| [ "${{ inputs.stable_action }}" = "publish" ]; then | |
| echo "ready=true" >> "$GITHUB_OUTPUT" | |
| else | |
| 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@v5 | |
| 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 | |
| args: --latest --strip header | |
| env: | |
| OUTPUT: CHANGELOG-release.md | |
| - name: Upload changelog | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: changelog | |
| retention-days: 30 | |
| path: CHANGELOG-release.md | |
| # ─── Registry publishing (crates.io + npm) ────────────────────────── | |
| publish-crates: | |
| needs: [prepare] | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' }} | |
| runs-on: depot-ubuntu-24.04-arm-4 | |
| timeout-minutes: 60 | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - uses: dtolnay/rust-toolchain@nightly | |
| - name: Configure git credentials | |
| run: git config --global url."https://x-access-token:${{ secrets.REPO_ACCESS_TOKEN }}@github.com/".insteadOf "https://github.com/" | |
| - uses: depot/setup-action@v1 | |
| - uses: mozilla-actions/sccache-action@v0.0.10 | |
| continue-on-error: true | |
| with: | |
| version: v0.16.0 | |
| - name: Install protoc | |
| uses: arduino/setup-protoc@v3 | |
| with: | |
| version: "27.x" | |
| repo-token: ${{ github.token }} | |
| - name: Download qualified crates | |
| if: ${{ inputs.stable_action == 'publish' }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ needs.prepare.outputs.qualification_run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| name: qualified-crates | |
| path: qualified/crates | |
| - name: Publish crates | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| mkdir -p qualified/crates | |
| crates_io_user_agent="Alien release workflow (https://github.com/alienplatform/alien)" | |
| wait_for_crates_io_index() { | |
| local crate=$1 expected_checksum=$2 index_path published_checksum | |
| local index_response index_status | |
| case "${#crate}" in | |
| 1) index_path="1/${crate}" ;; | |
| 2) index_path="2/${crate}" ;; | |
| 3) index_path="3/${crate:0:1}/${crate}" ;; | |
| *) index_path="${crate:0:2}/${crate:2:2}/${crate}" ;; | |
| esac | |
| index_response="crates-io-index-${crate}.json" | |
| for attempt in $(seq 1 30); do | |
| if ! index_status=$(curl \ | |
| --user-agent "$crates_io_user_agent" \ | |
| --silent --show-error \ | |
| --output "$index_response" \ | |
| --write-out '%{http_code}' \ | |
| "https://index.crates.io/${index_path}"); then | |
| echo "crates.io index request failed for ${crate}; retrying" | |
| sleep 10 | |
| continue | |
| fi | |
| if [ "$index_status" = "200" ]; then | |
| if ! published_checksum=$(jq -s -e -r \ | |
| --arg version "$VERSION" \ | |
| 'map(select(.vers == $version)) | last | .cksum // ""' \ | |
| "$index_response"); then | |
| echo "crates.io index returned malformed data for ${crate}; retrying" | |
| sleep 10 | |
| continue | |
| fi | |
| elif [ "$index_status" = "404" ]; then | |
| published_checksum="" | |
| else | |
| echo "crates.io index returned HTTP ${index_status} for ${crate}; retrying" | |
| sleep 10 | |
| continue | |
| fi | |
| if [ "$published_checksum" = "$expected_checksum" ]; then | |
| echo "${crate}@${VERSION} is available in the crates.io index" | |
| return 0 | |
| fi | |
| if [ -n "$published_checksum" ]; then | |
| echo "::error::${crate}@${VERSION} reached the index with a different checksum" | |
| return 1 | |
| fi | |
| echo "Waiting for ${crate}@${VERSION} index propagation (${attempt}/30)..." | |
| sleep 10 | |
| done | |
| echo "::error::Timed out waiting for ${crate}@${VERSION} in the crates.io index" | |
| return 1 | |
| } | |
| for crate in \ | |
| alien-error-derive \ | |
| alien-macros \ | |
| alien-error \ | |
| alien-core \ | |
| alien-client-core \ | |
| alien-aws-clients \ | |
| alien-gcp-clients \ | |
| alien-azure-clients \ | |
| alien-k8s-clients \ | |
| alien-platform-api \ | |
| alien-manager-api \ | |
| alien-client-config \ | |
| alien-permissions \ | |
| alien-bindings \ | |
| alien-worker-protocol \ | |
| alien-sdk \ | |
| alien-commands \ | |
| alien-commands-client; do | |
| if [ "${{ inputs.stable_action }}" = "qualification" ]; then | |
| echo "Recording package inputs for $crate..." | |
| depot cargo package -p "$crate" --list | | |
| LC_ALL=C sort > "qualified/crates/${crate}-${VERSION}.files" | |
| continue | |
| fi | |
| depot cargo package -p "$crate" --list | | |
| LC_ALL=C sort > "${crate}-${VERSION}.files" | |
| if ! cmp -s \ | |
| "${crate}-${VERSION}.files" \ | |
| "qualified/crates/${crate}-${VERSION}.files"; then | |
| echo "::error::${crate}@${VERSION} package inputs differ from qualification" | |
| diff -u \ | |
| "qualified/crates/${crate}-${VERSION}.files" \ | |
| "${crate}-${VERSION}.files" || true | |
| exit 1 | |
| fi | |
| # Cargo validates normalized registry dependencies while assembling | |
| # the archive. A dependent crate cannot be packaged until the | |
| # qualified prerequisite version exists in crates.io, so final | |
| # assembly happens here in dependency order. | |
| echo "Packaging $crate..." | |
| depot cargo package -p "$crate" --no-verify | |
| package="target/package/${crate}-${VERSION}.crate" | |
| checksum=$(sha256sum "$package" | awk '{print $1}') | |
| metadata_response="crates-io-metadata-${crate}.json" | |
| if ! metadata_status=$(curl \ | |
| --user-agent "$crates_io_user_agent" \ | |
| --silent --show-error \ | |
| --output "$metadata_response" \ | |
| --write-out '%{http_code}' \ | |
| "https://crates.io/api/v1/crates/${crate}/${VERSION}"); then | |
| echo "::error::Unable to query crates.io for ${crate}@${VERSION}" | |
| exit 1 | |
| fi | |
| if [ "$metadata_status" = "200" ]; then | |
| if ! published_checksum=$(jq -e -r '.version.checksum' \ | |
| "$metadata_response"); then | |
| echo "::error::crates.io returned malformed metadata for ${crate}@${VERSION}" | |
| exit 1 | |
| fi | |
| elif [ "$metadata_status" = "404" ]; then | |
| published_checksum="" | |
| else | |
| echo "::error::crates.io returned HTTP ${metadata_status} for ${crate}@${VERSION}" | |
| exit 1 | |
| fi | |
| if [ -n "$published_checksum" ]; then | |
| if [ "$published_checksum" != "$checksum" ]; then | |
| echo "::error::${crate}@${VERSION} exists with a different checksum" | |
| exit 1 | |
| fi | |
| echo "${crate}@${VERSION} already matches the qualified package" | |
| wait_for_crates_io_index "$crate" "$checksum" | |
| continue | |
| fi | |
| echo "Publishing $crate..." | |
| depot cargo publish -p "$crate" --no-verify | |
| wait_for_crates_io_index "$crate" "$checksum" | |
| done | |
| - name: Upload qualified crates | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: qualified-crates | |
| retention-days: 30 | |
| if-no-files-found: error | |
| path: qualified/crates/ | |
| publish-npm: | |
| needs: [prepare] | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' }} | |
| runs-on: depot-ubuntu-24.04-arm-4 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10.11.0 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| registry-url: https://registry.npmjs.org | |
| - uses: oven-sh/setup-bun@v2 | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| with: | |
| bun-version: "1.3.14" | |
| - name: Validate npm auth | |
| if: ${{ inputs.stable_action == 'publish' }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: npm whoami | |
| - name: Install dependencies | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| run: pnpm install --frozen-lockfile | |
| - name: Build | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| run: pnpm build | |
| env: | |
| NODE_OPTIONS: "--max-old-space-size=8192" | |
| - name: Download qualified npm packages | |
| if: ${{ inputs.stable_action == 'publish' }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ needs.prepare.outputs.qualification_run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| name: qualified-npm | |
| path: qualified/npm | |
| - name: Publish npm packages | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| if [ "${{ inputs.stable_action }}" = "qualification" ]; then | |
| mkdir -p qualified/npm | |
| for package in core commands sdk testing; do | |
| pnpm --filter "@alienplatform/${package}" pack --pack-destination qualified/npm | |
| done | |
| exit 0 | |
| fi | |
| publish_tarball() { | |
| local tarball="$1" package="$2" | |
| local integrity | |
| integrity="sha512-$(openssl dgst -sha512 -binary "$tarball" | openssl base64 -A)" | |
| local published | |
| published=$(npm view "${package}@${{ needs.prepare.outputs.version }}" dist.integrity 2>/dev/null || true) | |
| if [ -n "$published" ]; then | |
| if [ "$published" != "$integrity" ]; then | |
| echo "::error::${package}@${{ needs.prepare.outputs.version }} exists with different contents" | |
| exit 1 | |
| fi | |
| echo "${package}@${{ needs.prepare.outputs.version }} already matches the qualified package" | |
| else | |
| npm publish "$tarball" --access public | |
| fi | |
| } | |
| publish_tarball qualified/npm/alienplatform-core-${{ needs.prepare.outputs.version }}.tgz @alienplatform/core | |
| publish_tarball qualified/npm/alienplatform-commands-${{ needs.prepare.outputs.version }}.tgz @alienplatform/commands | |
| publish_tarball qualified/npm/alienplatform-sdk-${{ needs.prepare.outputs.version }}.tgz @alienplatform/sdk | |
| publish_tarball qualified/npm/alienplatform-testing-${{ needs.prepare.outputs.version }}.tgz @alienplatform/testing | |
| - name: Upload qualified npm packages | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: qualified-npm | |
| retention-days: 30 | |
| if-no-files-found: error | |
| path: qualified/npm/ | |
| publish-client-sdks: | |
| needs: [prepare] | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' }} | |
| runs-on: depot-ubuntu-24.04-arm-4 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| registry-url: https://registry.npmjs.org | |
| package-manager-cache: false | |
| - name: Validate npm auth | |
| if: ${{ inputs.stable_action == 'publish' }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: npm whoami | |
| - name: Build platform API SDK | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| working-directory: client-sdks/platform/typescript | |
| env: | |
| NODE_OPTIONS: "--max-old-space-size=8192" | |
| run: | | |
| npm install | |
| npm run build | |
| - name: Build manager API SDK | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| working-directory: client-sdks/manager/typescript | |
| run: | | |
| npm install | |
| npm run build | |
| - name: Pack qualified client SDKs | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| run: | | |
| mkdir -p qualified/client-sdks | |
| destination="$PWD/qualified/client-sdks" | |
| (cd client-sdks/platform/typescript && npm pack --pack-destination "$destination") | |
| (cd client-sdks/manager/typescript && npm pack --pack-destination "$destination") | |
| - name: Upload qualified client SDKs | |
| if: ${{ inputs.stable_action == 'qualification' }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: qualified-client-sdks | |
| retention-days: 30 | |
| if-no-files-found: error | |
| path: qualified/client-sdks/ | |
| - name: Download qualified client SDKs | |
| if: ${{ inputs.stable_action == 'publish' }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ needs.prepare.outputs.qualification_run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| name: qualified-client-sdks | |
| path: qualified/client-sdks | |
| - name: Publish qualified client SDKs | |
| if: ${{ inputs.stable_action == 'publish' }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| publish_tarball() { | |
| local tarball="$1" package="$2" | |
| local integrity | |
| integrity="sha512-$(openssl dgst -sha512 -binary "$tarball" | openssl base64 -A)" | |
| local published | |
| published=$(npm view "${package}@${VERSION}" dist.integrity 2>/dev/null || true) | |
| if [ -n "$published" ]; then | |
| if [ "$published" != "$integrity" ]; then | |
| echo "::error::${package}@${VERSION} exists with different contents" | |
| exit 1 | |
| fi | |
| echo "${package}@${VERSION} already matches the qualified package" | |
| else | |
| npm publish "$tarball" --access public | |
| fi | |
| } | |
| publish_tarball "qualified/client-sdks/alienplatform-platform-api-${VERSION}.tgz" "@alienplatform/platform-api" | |
| publish_tarball "qualified/client-sdks/alienplatform-manager-api-${VERSION}.tgz" "@alienplatform/manager-api" | |
| # ─── Native addon prebuilds (@alienplatform/bindings) ────────────── | |
| # | |
| # The bindings addon publishes as a thin wrapper plus per-platform prebuild | |
| # packages (`@alienplatform/bindings-<triple>`) that carry a compiled `.node`, | |
| # so `npm install @alienplatform/bindings` needs no Rust toolchain. Each triple | |
| # builds on its native runner (darwin-x64 cross-builds on the arm mac — same | |
| # toolchain via `--target`). Qualification records the four native artifacts, | |
| # runs the consumer smoke tests, then packs the exact npm tarballs that the | |
| # publish action later promotes. | |
| build-addon: | |
| needs: prepare | |
| if: inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # gateway_target/gateway_artifact drive the alien-ai-gateway launcher | |
| # binary built alongside the addon: darwin legs share the addon's | |
| # target; linux legs build it statically against musl, so one binary | |
| # (per arch) serves both the glibc and Alpine prebuild packages. | |
| - runner: depot-macos-15 | |
| os: macos | |
| target: aarch64-apple-darwin | |
| triple: darwin-arm64 | |
| expect_arch: arm64 | |
| gateway_target: aarch64-apple-darwin | |
| gateway_artifact: darwin-arm64 | |
| - runner: depot-macos-15 | |
| os: macos | |
| target: x86_64-apple-darwin | |
| triple: darwin-x64 | |
| expect_arch: x86_64 | |
| gateway_target: x86_64-apple-darwin | |
| gateway_artifact: darwin-x64 | |
| - runner: depot-ubuntu-24.04-16 | |
| os: linux | |
| target: x86_64-unknown-linux-gnu | |
| triple: linux-x64-gnu | |
| expect_arch: x86-64 | |
| gateway_target: x86_64-unknown-linux-musl | |
| gateway_artifact: linux-x64 | |
| - runner: depot-ubuntu-24.04-arm-16 | |
| os: linux | |
| target: aarch64-unknown-linux-gnu | |
| triple: linux-arm64-gnu | |
| expect_arch: aarch64 | |
| gateway_target: aarch64-unknown-linux-musl | |
| gateway_artifact: linux-arm64 | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 60 | |
| env: | |
| CARGO_INCREMENTAL: "0" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Apply computed version for dry run | |
| if: ${{ inputs.dry_run }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: release-version-manifests | |
| path: . | |
| - name: Validate bindings release version lockstep | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node packages/bindings/scripts/validate-release-version.mjs "$VERSION" | |
| - name: Validate ai-gateway release version lockstep | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node packages/ai-gateway/scripts/validate-release-version.mjs "$VERSION" | |
| # Rust toolchain: Linux via the shared composite; macOS uses dtolnay + | |
| # brew protoc directly (plain `napi build`, not `depot cargo`, so no | |
| # Depot/sccache setup here). The addon depends on alien-bindings, whose | |
| # build.rs needs protoc. | |
| - name: Setup Rust (linux) | |
| if: matrix.os == 'linux' | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| depot-project-id: ${{ vars.DEPOT_PROJECT_ID }} | |
| repo-access-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| install-protoc: "true" | |
| - uses: dtolnay/rust-toolchain@nightly | |
| if: matrix.os == 'macos' | |
| - name: Configure git credentials for cargo (macos) | |
| if: matrix.os == 'macos' | |
| run: git config --global url."https://x-access-token:${{ secrets.REPO_ACCESS_TOKEN }}@github.com/".insteadOf "https://github.com/" | |
| - name: Install protoc (macos) | |
| if: matrix.os == 'macos' | |
| run: brew install protobuf | |
| - name: Add cross target (darwin-x64) | |
| if: matrix.target == 'x86_64-apple-darwin' | |
| run: rustup target add x86_64-apple-darwin | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10.11.0 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build addon (${{ matrix.triple }}) | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| run: | | |
| pnpm --filter @alienplatform/bindings exec napi build \ | |
| --platform --release --target ${{ matrix.target }} \ | |
| --cwd "$GITHUB_WORKSPACE/crates/alien-bindings-node" | |
| - name: Stage addon into platform package | |
| run: | | |
| cp "crates/alien-bindings-node/alien-bindings-node.${{ matrix.triple }}.node" \ | |
| "packages/bindings/npm/${{ matrix.triple }}/alien-bindings-node.${{ matrix.triple }}.node" | |
| # Closes the wrong-arch hole on the two legs the smoke step doesn't cover. | |
| - name: Assert staged binary architecture | |
| run: file "packages/bindings/npm/${{ matrix.triple }}/alien-bindings-node.${{ matrix.triple }}.node" | grep -q "${{ matrix.expect_arch }}" | |
| - name: Upload addon artifact | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: addon-${{ matrix.triple }} | |
| retention-days: 30 | |
| path: packages/bindings/npm/${{ matrix.triple }}/alien-bindings-node.${{ matrix.triple }}.node | |
| # The AI gateway ships as a standalone launcher binary, not an addon. It | |
| # reuses this matrix (same runners/toolchains; it depends on | |
| # alien-bindings' credential resolver, so protoc is already set up). | |
| - name: Add gateway build target (${{ matrix.gateway_target }}) | |
| run: rustup target add ${{ matrix.gateway_target }} | |
| - name: Build ai-gateway binary (${{ matrix.gateway_artifact }}) | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| run: | | |
| cargo build --release -p alien-ai-gateway --bin alien-ai-gateway \ | |
| --target ${{ matrix.gateway_target }} | |
| - name: Assert ai-gateway binary architecture | |
| run: file "target/${{ matrix.gateway_target }}/release/alien-ai-gateway" | grep -q "${{ matrix.expect_arch }}" | |
| - name: Upload ai-gateway binary artifact | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ai-gateway-bin-${{ matrix.gateway_artifact }} | |
| retention-days: 30 | |
| path: target/${{ matrix.gateway_target }}/release/alien-ai-gateway | |
| # This job deliberately has no Rust compiler or protoc setup. It | |
| # starts from the already-built artifacts and proves that a consumer whose | |
| # only direct dependency is @alienplatform/bindings gets the right optional | |
| # platform package. Native runners additionally exercise npm + Node and Bun | |
| # installs with a real local-provider KV operation. | |
| smoke-addon: | |
| needs: [prepare, build-addon] | |
| if: inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runner: depot-macos-15 | |
| triple: darwin-arm64 | |
| execute: true | |
| - runner: depot-macos-15 | |
| triple: darwin-x64 | |
| execute: false | |
| - runner: depot-ubuntu-24.04-16 | |
| triple: linux-x64-gnu | |
| execute: true | |
| - runner: depot-ubuntu-24.04-arm-16 | |
| triple: linux-arm64-gnu | |
| execute: true | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Apply computed version for dry run | |
| if: ${{ inputs.dry_run }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: release-version-manifests | |
| path: . | |
| - name: Validate bindings release version lockstep | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node packages/bindings/scripts/validate-release-version.mjs "$VERSION" | |
| - name: Validate ai-gateway release version lockstep | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node packages/ai-gateway/scripts/validate-release-version.mjs "$VERSION" | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10.11.0 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - uses: oven-sh/setup-bun@v2 | |
| if: matrix.execute | |
| with: | |
| bun-version: "1.3.14" | |
| - name: Install JavaScript dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build packages used by addon smoke test | |
| run: | | |
| pnpm --filter @alienplatform/core build | |
| pnpm --filter @alienplatform/bindings build | |
| - name: Download staged addon (${{ matrix.triple }}) | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: addon-${{ matrix.triple }} | |
| path: packages/bindings/npm/${{ matrix.triple }} | |
| - name: Verify wrapper-only prebuild install (${{ matrix.triple }}) | |
| env: | |
| EXECUTE_NATIVE: ${{ matrix.execute }} | |
| run: | | |
| args=("${{ matrix.triple }}") | |
| if [ "$EXECUTE_NATIVE" = "true" ]; then args+=(--execute); fi | |
| bash scripts/smoke-addon-prebuild.sh "${args[@]}" | |
| qualify-bindings: | |
| needs: [prepare, smoke-addon, publish-npm] | |
| # The wrapper pins @alienplatform/core, so qualify it only after the core | |
| # package has been packed successfully and every native smoke test passes. | |
| if: inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' && always() && needs.smoke-addon.result == 'success' && needs.publish-npm.result == 'success' | |
| runs-on: depot-ubuntu-24.04-arm-4 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Apply computed version for dry run | |
| if: ${{ inputs.dry_run }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: release-version-manifests | |
| path: . | |
| - name: Validate bindings release version lockstep | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node packages/bindings/scripts/validate-release-version.mjs "$VERSION" | |
| - name: Validate ai-gateway release version lockstep | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node packages/ai-gateway/scripts/validate-release-version.mjs "$VERSION" | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10.11.0 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| registry-url: https://registry.npmjs.org | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Download qualification addon artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: addon-* | |
| path: ./addons | |
| merge-multiple: false | |
| - name: Stage addons into platform packages | |
| run: | | |
| for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu; do | |
| cp "./addons/addon-${triple}/alien-bindings-node.${triple}.node" \ | |
| "packages/bindings/npm/${triple}/alien-bindings-node.${triple}.node" | |
| done | |
| - name: Build wrapper | |
| run: | | |
| pnpm --filter @alienplatform/core build | |
| pnpm --filter @alienplatform/bindings build | |
| - name: Pack platform prebuild packages | |
| run: | | |
| mkdir -p qualified/bindings | |
| for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu; do | |
| npm pack "packages/bindings/npm/${triple}" --pack-destination "$PWD/qualified/bindings" | |
| done | |
| # Inject exact-version optionalDependencies explicitly (see the script) — | |
| # more deterministic and reviewable than `napi prepublish`. | |
| - name: Inject optionalDependencies into the wrapper | |
| run: node packages/bindings/scripts/inject-optional-deps.mjs | |
| # pnpm, not npm: only pnpm rewrites the `workspace:` range on | |
| # `@alienplatform/core` into the released version, and npm rejects a | |
| # published package that still carries the protocol. | |
| - name: Pack bindings wrapper | |
| run: pnpm --filter @alienplatform/bindings pack --pack-destination "$PWD/qualified/bindings" | |
| - name: Upload qualified bindings packages | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: qualified-bindings | |
| retention-days: 30 | |
| if-no-files-found: error | |
| path: qualified/bindings/ | |
| publish-bindings: | |
| needs: [prepare, publish-npm] | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' }} | |
| runs-on: depot-ubuntu-24.04-arm | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| registry-url: https://registry.npmjs.org | |
| - name: Validate npm auth | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: npm whoami | |
| - name: Download qualified bindings packages | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ needs.prepare.outputs.qualification_run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| name: qualified-bindings | |
| path: qualified/bindings | |
| - name: Publish qualified bindings packages | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| publish_tarball() { | |
| local tarball="$1" package="$2" | |
| local integrity | |
| integrity="sha512-$(openssl dgst -sha512 -binary "$tarball" | openssl base64 -A)" | |
| local published | |
| published=$(npm view "${package}@${VERSION}" dist.integrity 2>/dev/null || true) | |
| if [ -n "$published" ]; then | |
| if [ "$published" != "$integrity" ]; then | |
| echo "::error::${package}@${VERSION} exists with different contents" | |
| exit 1 | |
| fi | |
| echo "${package}@${VERSION} already matches the qualified package" | |
| else | |
| npm publish "$tarball" --access public | |
| fi | |
| } | |
| for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu; do | |
| publish_tarball \ | |
| "qualified/bindings/alienplatform-bindings-${triple}-${VERSION}.tgz" \ | |
| "@alienplatform/bindings-${triple}" | |
| done | |
| publish_tarball \ | |
| "qualified/bindings/alienplatform-bindings-${VERSION}.tgz" \ | |
| "@alienplatform/bindings" | |
| qualify-ai-gateway: | |
| needs: [prepare, build-addon, publish-npm] | |
| # Same ordering rules as qualify-bindings: the wrapper pins | |
| # `@alienplatform/core: ^<release>`, so it is qualified only after core packs. | |
| if: inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' && always() && needs.build-addon.result == 'success' && needs.publish-npm.result == 'success' | |
| runs-on: depot-ubuntu-24.04-arm-4 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Apply computed version for dry run | |
| if: ${{ inputs.dry_run }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: release-version-manifests | |
| path: . | |
| - name: Validate ai-gateway release version lockstep | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: node packages/ai-gateway/scripts/validate-release-version.mjs "$VERSION" | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10.11.0 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| registry-url: https://registry.npmjs.org | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Download ai-gateway binaries | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: ai-gateway-bin-* | |
| path: ./gateway-bins | |
| merge-multiple: false | |
| - name: Generate prebuild manifests | |
| run: node packages/ai-gateway/scripts/generate-prebuilds.mjs | |
| # The linux binaries are static musl builds, so a triple's gnu and musl | |
| # packages ship the same executable. Artifact download drops the execute | |
| # bit; restore it so npm packs the binary runnable. | |
| - name: Stage binaries into prebuild packages | |
| run: | | |
| for pair in darwin-arm64:darwin-arm64 darwin-x64:darwin-x64 \ | |
| linux-x64-gnu:linux-x64 linux-x64-musl:linux-x64 \ | |
| linux-arm64-gnu:linux-arm64 linux-arm64-musl:linux-arm64; do | |
| triple="${pair%%:*}" | |
| source="${pair##*:}" | |
| cp "./gateway-bins/ai-gateway-bin-${source}/alien-ai-gateway" \ | |
| "packages/ai-gateway/npm/${triple}/alien-ai-gateway" | |
| chmod 755 "packages/ai-gateway/npm/${triple}/alien-ai-gateway" | |
| done | |
| # Install no longer builds workspace packages, so the wrapper's tsc needs | |
| # its core dependency built explicitly first. | |
| - name: Build wrapper | |
| run: | | |
| pnpm --filter @alienplatform/core build | |
| pnpm --filter @alienplatform/ai-gateway build | |
| - name: Pack platform prebuild packages | |
| run: | | |
| mkdir -p qualified/ai-gateway | |
| for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-x64-musl linux-arm64-gnu linux-arm64-musl; do | |
| npm pack "packages/ai-gateway/npm/${triple}" --pack-destination "$PWD/qualified/ai-gateway" | |
| done | |
| # Pinned before the wrapper is packed, so the qualified tarball already | |
| # carries the exact-version optionalDependencies it will publish with. | |
| - name: Inject optionalDependencies into the wrapper | |
| run: node packages/ai-gateway/scripts/inject-optional-deps.mjs | |
| # pnpm, not npm: only pnpm rewrites the `workspace:` range on | |
| # `@alienplatform/core` into the released version, and npm rejects a | |
| # published package that still carries the protocol. | |
| - name: Pack ai-gateway wrapper | |
| run: pnpm --filter @alienplatform/ai-gateway pack --pack-destination "$PWD/qualified/ai-gateway" | |
| - name: Upload qualified ai-gateway packages | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: qualified-ai-gateway | |
| retention-days: 30 | |
| if-no-files-found: error | |
| path: qualified/ai-gateway/ | |
| publish-ai-gateway: | |
| needs: [prepare, publish-npm] | |
| # Skipped on a dry run: publishing is permanent and there is no qualification run to download from. | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' && !inputs.dry_run }} | |
| runs-on: depot-ubuntu-24.04-arm | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| registry-url: https://registry.npmjs.org | |
| - name: Validate npm auth | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: npm whoami | |
| - name: Download qualified ai-gateway packages | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ needs.prepare.outputs.qualification_run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| name: qualified-ai-gateway | |
| path: qualified/ai-gateway | |
| # Prebuild packages publish FIRST so an install of the wrapper can never | |
| # race ahead of the optionalDependencies it pins. | |
| - name: Publish qualified ai-gateway packages | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| publish_tarball() { | |
| local tarball="$1" package="$2" | |
| local integrity | |
| integrity="sha512-$(openssl dgst -sha512 -binary "$tarball" | openssl base64 -A)" | |
| local published | |
| published=$(npm view "${package}@${VERSION}" dist.integrity 2>/dev/null || true) | |
| if [ -n "$published" ]; then | |
| if [ "$published" != "$integrity" ]; then | |
| echo "::error::${package}@${VERSION} exists with different contents" | |
| exit 1 | |
| fi | |
| echo "${package}@${VERSION} already matches the qualified package" | |
| else | |
| npm publish "$tarball" --access public | |
| fi | |
| } | |
| for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-x64-musl linux-arm64-gnu linux-arm64-musl; do | |
| publish_tarball \ | |
| "qualified/ai-gateway/alienplatform-ai-gateway-${triple}-${VERSION}.tgz" \ | |
| "@alienplatform/ai-gateway-${triple}" | |
| done | |
| publish_tarball \ | |
| "qualified/ai-gateway/alienplatform-ai-gateway-${VERSION}.tgz" \ | |
| "@alienplatform/ai-gateway" | |
| # ─── Binary builds (4 targets, all parallel) ─────────────────────── | |
| build-binaries-linux-x86_64: | |
| needs: prepare | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' }} | |
| runs-on: depot-ubuntu-24.04-16 | |
| timeout-minutes: 60 | |
| env: | |
| CARGO_INCREMENTAL: "0" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Restore binaries from cache | |
| id: binary-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| target/x86_64-unknown-linux-musl/release/alien | |
| target/x86_64-unknown-linux-musl/release/alien-deploy | |
| target/x86_64-unknown-linux-musl/release/alien-operator | |
| target/x86_64-unknown-linux-musl/release/alien-worker-runtime | |
| target/x86_64-unknown-linux-musl/release/alien-manager | |
| key: release-linux-x86_64-v${{ needs.prepare.outputs.version }} | |
| - uses: ./.github/actions/setup-rust | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| with: | |
| depot-project-id: ${{ vars.DEPOT_PROJECT_ID }} | |
| repo-access-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| targets: x86_64-unknown-linux-musl | |
| install-protoc: "true" | |
| - name: Build x86_64 linux binaries | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| run: | | |
| depot cargo build --release \ | |
| -p alien-cli -p alien-deploy-cli -p alien-operator -p alien-worker-runtime -p alien-manager \ | |
| --target x86_64-unknown-linux-musl | |
| - name: Stage binaries for upload | |
| run: | | |
| mkdir -p staged/x86_64-unknown-linux-musl | |
| for binary in alien alien-deploy alien-operator alien-worker-runtime alien-manager; do | |
| cp "target/x86_64-unknown-linux-musl/release/${binary}" "staged/x86_64-unknown-linux-musl/${binary}" | |
| done | |
| - name: Upload x86_64 linux binaries | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: binaries-x86_64-unknown-linux-musl | |
| retention-days: 30 | |
| path: staged/x86_64-unknown-linux-musl/ | |
| build-binaries-linux-aarch64: | |
| needs: prepare | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' }} | |
| runs-on: depot-ubuntu-24.04-arm-16 | |
| timeout-minutes: 60 | |
| env: | |
| CARGO_INCREMENTAL: "0" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Restore binaries from cache | |
| id: binary-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| target/aarch64-unknown-linux-musl/release/alien | |
| target/aarch64-unknown-linux-musl/release/alien-deploy | |
| target/aarch64-unknown-linux-musl/release/alien-operator | |
| target/aarch64-unknown-linux-musl/release/alien-worker-runtime | |
| target/aarch64-unknown-linux-musl/release/alien-manager | |
| key: release-linux-aarch64-v${{ needs.prepare.outputs.version }} | |
| - uses: ./.github/actions/setup-rust | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| with: | |
| depot-project-id: ${{ vars.DEPOT_PROJECT_ID }} | |
| repo-access-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| targets: aarch64-unknown-linux-musl | |
| install-protoc: "true" | |
| - name: Build aarch64 linux binaries | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| run: | | |
| depot cargo build --release \ | |
| -p alien-cli -p alien-deploy-cli -p alien-operator -p alien-worker-runtime -p alien-manager \ | |
| --target aarch64-unknown-linux-musl | |
| - name: Stage binaries for upload | |
| run: | | |
| mkdir -p staged/aarch64-unknown-linux-musl | |
| for binary in alien alien-deploy alien-operator alien-worker-runtime alien-manager; do | |
| cp "target/aarch64-unknown-linux-musl/release/${binary}" "staged/aarch64-unknown-linux-musl/${binary}" | |
| done | |
| - name: Upload aarch64 linux binaries | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: binaries-aarch64-unknown-linux-musl | |
| retention-days: 30 | |
| path: staged/aarch64-unknown-linux-musl/ | |
| # ─── macOS build (aarch64 only, native) ──────────────────────────── | |
| build-binaries-darwin: | |
| needs: prepare | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' }} | |
| runs-on: depot-macos-15 | |
| timeout-minutes: 60 | |
| env: | |
| CARGO_INCREMENTAL: "0" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Restore binaries from cache | |
| id: binary-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| target/aarch64-apple-darwin/release/alien | |
| target/aarch64-apple-darwin/release/alien-deploy | |
| target/aarch64-apple-darwin/release/alien-operator | |
| key: release-darwin-aarch64-v${{ needs.prepare.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@nightly | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| - name: Configure git credentials for cargo | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| run: | | |
| git config --global url."https://x-access-token:${{ secrets.REPO_ACCESS_TOKEN }}@github.com/".insteadOf "https://github.com/" | |
| - name: Setup Depot | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| uses: depot/setup-action@v1 | |
| - name: Install sccache | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| continue-on-error: true | |
| uses: mozilla-actions/sccache-action@v0.0.10 | |
| with: | |
| version: v0.16.0 | |
| - name: Install protoc | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| run: brew install protobuf | |
| - name: Build aarch64 darwin binaries | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| run: | | |
| depot cargo build --release \ | |
| -p alien-cli -p alien-deploy-cli -p alien-operator \ | |
| --target aarch64-apple-darwin | |
| - name: Stage binaries for upload | |
| run: | | |
| mkdir -p staged/aarch64-apple-darwin | |
| for binary in alien alien-deploy alien-operator; do | |
| cp "target/aarch64-apple-darwin/release/${binary}" "staged/aarch64-apple-darwin/${binary}" | |
| done | |
| - name: Upload aarch64 darwin binaries | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: binaries-aarch64-apple-darwin | |
| retention-days: 30 | |
| path: staged/aarch64-apple-darwin/ | |
| # ─── Windows build (x86_64, native MSVC) ────────────────────────── | |
| build-binaries-windows: | |
| needs: prepare | |
| if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification' }} | |
| runs-on: depot-windows-2025-16 | |
| timeout-minutes: 60 | |
| env: | |
| CARGO_INCREMENTAL: "0" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Restore binaries from cache | |
| id: binary-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| target/x86_64-pc-windows-msvc/release/alien.exe | |
| target/x86_64-pc-windows-msvc/release/alien-deploy.exe | |
| target/x86_64-pc-windows-msvc/release/alien-operator.exe | |
| key: release-windows-x86_64-v${{ needs.prepare.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@nightly | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| - name: Configure git credentials for cargo | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: | | |
| git config --global url."https://x-access-token:${{ secrets.REPO_ACCESS_TOKEN }}@github.com/".insteadOf "https://github.com/" | |
| # depot cargo is Linux/macOS only — use sccache directly on Windows | |
| - name: Install sccache | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| continue-on-error: true | |
| uses: mozilla-actions/sccache-action@v0.0.10 | |
| with: | |
| version: v0.16.0 | |
| - name: Install build tools (protoc, nasm, cmake) | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: choco install protoc nasm cmake -y | |
| - name: Build windows binaries | |
| if: steps.binary-cache.outputs.cache-hit != 'true' | |
| shell: bash | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| AWS_LC_SYS_CMAKE_BUILDER: "1" | |
| run: | | |
| export PATH="/c/Program Files/NASM:$PATH" | |
| cargo build --release \ | |
| -p alien-cli -p alien-deploy-cli -p alien-operator \ | |
| --target x86_64-pc-windows-msvc | |
| - name: Stage binaries for upload | |
| shell: bash | |
| run: | | |
| mkdir -p staged/x86_64-pc-windows-msvc | |
| cp target/x86_64-pc-windows-msvc/release/alien.exe 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/ | |
| - name: Upload windows binaries | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: binaries-x86_64-pc-windows-msvc | |
| retention-days: 30 | |
| path: staged/x86_64-pc-windows-msvc/ | |
| # ─── Upload binaries to S3 ────────────────────────────────────────── | |
| upload-binaries: | |
| 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: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ inputs.stable_action == 'publish' && needs.prepare.outputs.qualification_run_id || github.run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| pattern: binaries-* | |
| path: ./artifacts | |
| merge-multiple: false | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: arn:aws:iam::216938125589:role/alien-releases-upload | |
| aws-region: us-east-1 | |
| - name: Upload to S3 (versioned + latest) | |
| run: | | |
| VERSION="${{ needs.prepare.outputs.version }}" | |
| # Artifacts are downloaded as: ./artifacts/binaries-<target>/<binary> | |
| upload() { | |
| local binary=$1 target=$2 os=$3 arch=$4 | |
| local src="./artifacts/binaries-${target}/${binary}" | |
| local versioned="s3://alien-releases-prod/${binary}/v${VERSION}/${os}-${arch}/${binary}" | |
| local checksum | |
| checksum=$(sha256sum "$src" | awk '{print $1}') | |
| local published | |
| published=$(aws s3api head-object \ | |
| --bucket alien-releases-prod \ | |
| --key "${binary}/v${VERSION}/${os}-${arch}/${binary}" \ | |
| --query 'Metadata.sha256' --output text 2>/dev/null || true) | |
| if [ -n "$published" ] && [ "$published" != "None" ] && [ "$published" != "$checksum" ]; then | |
| echo "::error::${versioned} exists with different contents" | |
| exit 1 | |
| fi | |
| if [ "$published" != "$checksum" ]; then | |
| aws s3 cp "$src" "$versioned" --metadata "sha256=${checksum}" | |
| fi | |
| } | |
| for binary in alien 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 | |
| upload "$binary" aarch64-apple-darwin darwin aarch64 | |
| done | |
| upload_exe() { | |
| local binary=$1 target=$2 os=$3 arch=$4 | |
| local src="./artifacts/binaries-${target}/${binary}.exe" | |
| local versioned="s3://alien-releases-prod/${binary}/v${VERSION}/${os}-${arch}/${binary}.exe" | |
| local checksum | |
| checksum=$(sha256sum "$src" | awk '{print $1}') | |
| local published | |
| published=$(aws s3api head-object \ | |
| --bucket alien-releases-prod \ | |
| --key "${binary}/v${VERSION}/${os}-${arch}/${binary}.exe" \ | |
| --query 'Metadata.sha256' --output text 2>/dev/null || true) | |
| if [ -n "$published" ] && [ "$published" != "None" ] && [ "$published" != "$checksum" ]; then | |
| echo "::error::${versioned} exists with different contents" | |
| exit 1 | |
| fi | |
| if [ "$published" != "$checksum" ]; then | |
| aws s3 cp "$src" "$versioned" --metadata "sha256=${checksum}" | |
| fi | |
| } | |
| for binary in alien 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@v7 | |
| with: | |
| pattern: binaries-* | |
| path: ./artifacts | |
| merge-multiple: false | |
| - name: Download changelog | |
| uses: actions/download-artifact@v7 | |
| 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@v6 | |
| with: | |
| name: qualified-github-release | |
| retention-days: 30 | |
| if-no-files-found: error | |
| path: | | |
| CHANGELOG-release.md | |
| dist/ | |
| # ─── Docker images + Helm chart ───────────────────────────────────── | |
| qualify-images: | |
| needs: [prepare, build-addon, 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-16 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Download linux binaries | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ inputs.stable_action == 'publish' && needs.prepare.outputs.qualification_run_id || github.run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| pattern: binaries-*-unknown-linux-musl | |
| path: ./linux-bins | |
| merge-multiple: false | |
| - name: Place binaries in target layout for Dockerfiles | |
| run: | | |
| for target in x86_64-unknown-linux-musl aarch64-unknown-linux-musl; do | |
| mkdir -p "target/${target}/release" | |
| for binary in alien-operator alien-worker-runtime alien-manager; do | |
| cp "./linux-bins/binaries-${target}/${binary}" "target/${target}/release/${binary}" | |
| chmod +x "target/${target}/release/${binary}" | |
| done | |
| done | |
| - name: Download Linux AI gateway binaries | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: ai-gateway-bin-linux-* | |
| path: ./gateway-bins | |
| merge-multiple: false | |
| - name: Place AI gateway binaries in target layout for Dockerfiles | |
| run: | | |
| cp ./gateway-bins/ai-gateway-bin-linux-x64/alien-ai-gateway \ | |
| target/x86_64-unknown-linux-musl/release/alien-ai-gateway | |
| cp ./gateway-bins/ai-gateway-bin-linux-arm64/alien-ai-gateway \ | |
| target/aarch64-unknown-linux-musl/release/alien-ai-gateway | |
| chmod +x \ | |
| target/x86_64-unknown-linux-musl/release/alien-ai-gateway \ | |
| target/aarch64-unknown-linux-musl/release/alien-ai-gateway | |
| - name: Download Linux Worker addons | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ inputs.stable_action == 'publish' && needs.prepare.outputs.qualification_run_id || github.run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| pattern: addon-linux-*-gnu | |
| path: ./linux-addons | |
| merge-multiple: true | |
| - name: Place Worker addons in Docker build context | |
| run: | | |
| cp ./linux-addons/alien-bindings-node.linux-x64-gnu.node \ | |
| crates/alien-bindings-node/alien-bindings-node.linux-x64-gnu.node | |
| cp ./linux-addons/alien-bindings-node.linux-arm64-gnu.node \ | |
| crates/alien-bindings-node/alien-bindings-node.linux-arm64-gnu.node | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Depot | |
| uses: depot/setup-action@v1 | |
| - name: Metadata (alien-base) | |
| id: meta_base | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ghcr.io/alienplatform/alien-base | |
| tags: | | |
| type=raw,value=qualification-${{ inputs.source_ref }} | |
| - name: Publish alien-base | |
| id: publish_base | |
| uses: depot/build-push-action@v1 | |
| with: | |
| project: ${{ vars.DEPOT_PROJECT_ID }} | |
| context: . | |
| file: docker/Dockerfile.alien-base | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta_base.outputs.tags }} | |
| labels: ${{ steps.meta_base.outputs.labels }} | |
| provenance: true | |
| sbom: true | |
| - name: Metadata (alien-worker-typescript) | |
| id: meta_worker_typescript | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ghcr.io/alienplatform/alien-worker-typescript | |
| tags: | | |
| type=raw,value=qualification-${{ inputs.source_ref }} | |
| - name: Publish alien-worker-typescript | |
| id: publish_worker_typescript | |
| uses: depot/build-push-action@v1 | |
| with: | |
| project: ${{ vars.DEPOT_PROJECT_ID }} | |
| context: . | |
| file: docker/Dockerfile.alien-worker-typescript | |
| build-args: | | |
| ALIEN_WORKER_BASE=ghcr.io/alienplatform/alien-base@${{ steps.publish_base.outputs.digest }} | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta_worker_typescript.outputs.tags }} | |
| labels: ${{ steps.meta_worker_typescript.outputs.labels }} | |
| provenance: true | |
| sbom: true | |
| - name: Smoke-test Worker base images | |
| env: | |
| GENERIC_BASE: ghcr.io/alienplatform/alien-base@${{ steps.publish_base.outputs.digest }} | |
| TYPESCRIPT_BASE: ghcr.io/alienplatform/alien-worker-typescript@${{ steps.publish_worker_typescript.outputs.digest }} | |
| run: | | |
| docker run --rm --platform linux/arm64 --entrypoint /bin/sh "$GENERIC_BASE" \ | |
| -c 'test ! -e /usr/local/bin/bun && test -x /app/alien-worker-runtime' | |
| docker run --rm --platform linux/arm64 --entrypoint /usr/local/bin/bun "$TYPESCRIPT_BASE" \ | |
| -e 'const a=require("/app/alien-bindings.node"); if (!a.version()) throw new Error("native version() returned empty")' | |
| - name: Metadata (alien-builder) | |
| id: meta_builder | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ghcr.io/alienplatform/alien-builder | |
| tags: | | |
| type=raw,value=qualification-${{ inputs.source_ref }} | |
| - name: Publish alien-builder | |
| uses: depot/build-push-action@v1 | |
| with: | |
| project: ${{ vars.DEPOT_PROJECT_ID }} | |
| context: . | |
| file: docker/Dockerfile.alien-builder | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta_builder.outputs.tags }} | |
| labels: ${{ steps.meta_builder.outputs.labels }} | |
| provenance: true | |
| sbom: true | |
| - name: Metadata (alien-operator) | |
| id: meta_agent | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ghcr.io/alienplatform/alien-operator | |
| tags: | | |
| type=raw,value=qualification-${{ inputs.source_ref }} | |
| - name: Publish alien-operator | |
| uses: depot/build-push-action@v1 | |
| with: | |
| project: ${{ vars.DEPOT_PROJECT_ID }} | |
| context: . | |
| file: docker/Dockerfile.alien-operator | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta_agent.outputs.tags }} | |
| labels: ${{ steps.meta_agent.outputs.labels }} | |
| provenance: true | |
| sbom: true | |
| - name: Metadata (alien-manager) | |
| id: meta_manager | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ghcr.io/alienplatform/alien-manager | |
| tags: | | |
| type=raw,value=qualification-${{ inputs.source_ref }} | |
| - name: Publish alien-manager | |
| uses: depot/build-push-action@v1 | |
| with: | |
| project: ${{ vars.DEPOT_PROJECT_ID }} | |
| context: . | |
| file: docker/Dockerfile.alien-manager | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta_manager.outputs.tags }} | |
| labels: ${{ steps.meta_manager.outputs.labels }} | |
| provenance: true | |
| sbom: true | |
| - name: Record qualified image digests | |
| env: | |
| RELEASE_COMMIT: ${{ inputs.source_ref }} | |
| run: | | |
| DIGESTS='{}' | |
| for name in alien-base alien-worker-typescript alien-builder alien-operator alien-manager; do | |
| qualified="ghcr.io/alienplatform/${name}:qualification-${RELEASE_COMMIT}" | |
| digest=$(docker buildx imagetools inspect "$qualified" | | |
| sed -n 's/^Digest:[[:space:]]*//p' | head -n1) | |
| if [ -z "$digest" ]; then | |
| echo "::error::Qualified image ${qualified} has no manifest digest" | |
| exit 1 | |
| fi | |
| DIGESTS=$(jq --arg name "$name" --arg digest "$digest" \ | |
| '. + {($name): $digest}' <<< "$DIGESTS") | |
| done | |
| jq -n \ | |
| --arg releaseCommit "$RELEASE_COMMIT" \ | |
| --argjson images "$DIGESTS" \ | |
| '{releaseCommit: $releaseCommit, images: $images}' \ | |
| > qualified-image-digests.json | |
| - name: Upload qualified image digests | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: qualified-image-digests | |
| retention-days: 30 | |
| if-no-files-found: error | |
| path: qualified-image-digests.json | |
| publish-images: | |
| 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: | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download qualified image digests | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ needs.prepare.outputs.qualification_run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| name: qualified-image-digests | |
| path: qualification | |
| - name: Promote qualified image manifests | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| RELEASE_COMMIT: ${{ needs.prepare.outputs.qualification_head_sha }} | |
| run: | | |
| recorded_commit=$(jq -r '.releaseCommit' qualification/qualified-image-digests.json) | |
| if [ "$recorded_commit" != "$RELEASE_COMMIT" ]; then | |
| echo "::error::Qualified images belong to ${recorded_commit}, expected ${RELEASE_COMMIT}" | |
| exit 1 | |
| fi | |
| jq -r '.images | to_entries[] | [.key, .value] | @tsv' \ | |
| qualification/qualified-image-digests.json | | |
| while IFS=$'\t' read -r name source_digest; do | |
| image="ghcr.io/alienplatform/${name}" | |
| if ! docker buildx imagetools inspect "${image}@${source_digest}" >/dev/null; then | |
| echo "::error::Qualified image ${image}@${source_digest} is unavailable" | |
| exit 1 | |
| fi | |
| versioned="${image}:v${VERSION}" | |
| published_digest=$(docker buildx imagetools inspect "$versioned" 2>/dev/null | | |
| sed -n 's/^Digest:[[:space:]]*//p' | head -n1 || true) | |
| if [ -n "$published_digest" ] && [ "$published_digest" != "$source_digest" ]; then | |
| echo "::error::${versioned} exists with a different manifest" | |
| exit 1 | |
| fi | |
| docker buildx imagetools create \ | |
| --tag "$versioned" \ | |
| --tag "${image}:latest" \ | |
| "${image}@${source_digest}" | |
| done | |
| # 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. | |
| create-github-release: | |
| needs: | |
| - prepare | |
| - publish-crates | |
| - publish-npm | |
| - publish-client-sdks | |
| - publish-bindings | |
| - 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 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_commit }} | |
| fetch-depth: 0 | |
| - name: Download qualified GitHub release payload | |
| uses: actions/download-artifact@v7 | |
| with: | |
| run-id: ${{ needs.prepare.outputs.qualification_run_id }} | |
| github-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| name: qualified-github-release | |
| path: . | |
| - name: Require immutable GitHub release protections | |
| env: | |
| GH_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| run: | | |
| enabled=$(gh api \ | |
| -H "X-GitHub-Api-Version: 2026-03-10" \ | |
| "/repos/${GITHUB_REPOSITORY}/immutable-releases" \ | |
| --jq '.enabled') | |
| if [ "$enabled" != "true" ]; then | |
| echo "::error::Immutable GitHub releases must be enabled" | |
| exit 1 | |
| fi | |
| ruleset_id=$(gh api --paginate \ | |
| -H "X-GitHub-Api-Version: 2026-03-10" \ | |
| "/repos/${GITHUB_REPOSITORY}/rulesets" \ | |
| --jq '.[] | select( | |
| .name == "Immutable release tags" and | |
| .target == "tag" and | |
| .enforcement == "active" | |
| ) | .id' | head -n 1) | |
| if [ -z "$ruleset_id" ]; then | |
| echo "::error::The active Immutable release tags ruleset is required" | |
| exit 1 | |
| fi | |
| ruleset=$(gh api \ | |
| -H "X-GitHub-Api-Version: 2026-03-10" \ | |
| "/repos/${GITHUB_REPOSITORY}/rulesets/${ruleset_id}") | |
| if ! jq -e ' | |
| .conditions.ref_name.include == ["refs/tags/v*"] and | |
| .conditions.ref_name.exclude == [] and | |
| ([.rules[].type] | index("deletion")) != null and | |
| ([.rules[].type] | index("update")) != null and | |
| (.bypass_actors | length) == 0 | |
| ' <<< "$ruleset" >/dev/null; then | |
| echo "::error::Release tags must prohibit updates and deletion without bypasses" | |
| exit 1 | |
| fi | |
| - name: Verify existing release tag | |
| id: verify_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)]') | |
| 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 assets | |
| 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 | |
| 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 | |
| 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 | |
| files: | | |
| dist/alien-v*-*.tar.gz | |
| dist/alien-v*-*.zip | |
| dist/alien-v*-checksums.txt | |
| 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 | |
| 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} points to ${TAG_COMMIT}, expected ${COMMIT}" | |
| 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@v4 | |
| 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@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| registry-url: https://registry.npmjs.org | |
| package-manager-cache: false | |
| - name: Download all binary artifacts | |
| uses: actions/download-artifact@v7 | |
| 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@v6 | |
| 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@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_commit }} | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| 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@v7 | |
| 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 |