name: Release # Unified release for the entire monorepo. # All @elaraai/* npm packages, all 5 east-py PyPI packages, the VSCode extension, # and the native east-c binaries ship under one root-package.json version. # # VS Marketplace doesn't accept semver pre-release labels (e.g. 1.0.0-beta.0), # so for `pre*` release_types the VSIX gets a plain patch bump on its own track # and is published with --pre-release. See scripts/set-vsix-version.mjs. on: workflow_dispatch: inputs: release_type: description: "Release type" required: true default: "prerelease" type: choice options: - prerelease - prepatch - preminor - premajor - patch - minor - major dry_run: description: "Dry run (build everything, skip all publishes + tag/push)" required: false default: true type: boolean # On PRs to main, exercise ONLY the verdaccio release dry-run (validate-release). # Every other job needs `prepare`, which is gated to workflow_dispatch below, so # they all skip on PRs — no publish, no tag, no push, no secrets. `pull_request` # (never pull_request_target) means fork PRs get no secrets to leak. pull_request: branches: [main] paths: - "libs/**" - "scripts/**" - "package.json" - ".github/workflows/release.yml" concurrency: # Real releases share one global lock (never cancel a release in flight); PR # validations are per-ref and cancel stale runs for fast iteration. group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || 'release' }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: # ──────────────────────────────────────────────────────────────────────── # 1. prepare: bump root version, propagate to all manifests, upload as artifact. # Downstream jobs download this artifact so every job sees identical manifests # without re-running the bump (which would be non-deterministic across jobs). # ──────────────────────────────────────────────────────────────────────── prepare: # Only on manual dispatch. On PRs this job (and everything that needs it — # i.e. all the publish/finalize jobs) skips, leaving just validate-release. if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest outputs: version: ${{ steps.bump.outputs.version }} pep440: ${{ steps.bump.outputs.pep440 }} vsix_version: ${{ steps.bump.outputs.vsix_version }} npm_tag: ${{ steps.bump.outputs.npm_tag }} is_prerelease: ${{ steps.bump.outputs.is_prerelease }} steps: # Real releases (non-dry-run) MUST run from main — they tag, push, and # publish to registries. Dry-runs make no state changes (no publish, no # tag, no push), so they're allowed from any branch — useful for the # one-time east-c npm bootstrap and for testing release-pipeline changes # before merging them. - name: Enforce release from main (non-dry-run only) if: ${{ !inputs.dry_run }} run: | if [ "${{ github.ref }}" != "refs/heads/main" ]; then echo "::error::Real releases may only be run from main (got ${{ github.ref }})." exit 1 fi - name: Note dry-run on non-main branch if: ${{ inputs.dry_run && github.ref != 'refs/heads/main' }} run: echo "::warning::Dry-run on ${{ github.ref }} — artifacts will reflect this branch, not main." - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 # `Compute target version` bumps the Python pyproject versions and then # re-locks libs/east-py/uv.lock via `uv lock` (scripts/set-python-version.mjs), # so the `uv` binary must be on PATH in this job too — not just the build jobs. - uses: astral-sh/setup-uv@v3 - name: Preflight — VSCE PAT valid if: ${{ !inputs.dry_run }} env: VSCE_PAT: ${{ secrets.VSCE_PAT }} run: | if [[ -z "$VSCE_PAT" ]]; then echo "::error::VSCE_PAT secret is empty. Set it in repo secrets." exit 1 fi npx --yes @vscode/vsce@latest verify-pat ElaraAI - name: Preflight — npm auth (OIDC reachable) if: ${{ !inputs.dry_run }} run: | # OIDC token exchange happens at publish time; nothing to verify here except # that the registry is reachable. Catches DNS/network outages before wasting a build. curl -sSf https://registry.npmjs.org/-/ping > /dev/null - name: Preflight — PyPI registry reachable if: ${{ !inputs.dry_run }} run: | curl -sSf https://upload.pypi.org/ > /dev/null || true curl -sSf https://pypi.org/ > /dev/null - name: Compute target version id: bump run: | if [[ "${{ inputs.release_type }}" == pre* ]]; then npm version "${{ inputs.release_type }}" --preid=beta --no-git-tag-version else npm version "${{ inputs.release_type }}" --no-git-tag-version fi NEW_VERSION=$(node -p "require('./package.json').version") if [[ "$NEW_VERSION" == *"-"* ]]; then echo "npm_tag=beta" >> $GITHUB_OUTPUT echo "is_prerelease=true" >> $GITHUB_OUTPUT else echo "npm_tag=latest" >> $GITHUB_OUTPUT echo "is_prerelease=false" >> $GITHUB_OUTPUT fi echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT make set-version VERSION="${NEW_VERSION}" PEP440=$(node -e "import('./scripts/set-python-version.mjs').then(m => process.stdout.write(m.semverToPep440('${NEW_VERSION}')))") echo "pep440=${PEP440}" >> $GITHUB_OUTPUT echo "vsix_version=$(node -p "require('./libs/east-ui/packages/east-ui-extension/package.json').version")" >> $GITHUB_OUTPUT - name: Upload bumped manifests uses: actions/upload-artifact@v4 with: name: version-bump path: | package.json libs/**/package.json libs/east-py/packages/*/pyproject.toml libs/east-py/uv.lock libs/east-c/VERSION .claude-plugin/marketplace.json libs/east-claude-plugin/.claude-plugin/plugin.json # uv.lock is re-locked by `Compute target version`; it MUST ride along # in this artifact or `finalize`'s drift check sees the stale lock and # the release fails after everything has already published (v1.0.21). # The .claude-plugin/ dirs are hidden; without this the uploader # silently drops plugin.json and the release commits version drift # (exactly what happened at v1.0.4 and v1.0.5). include-hidden-files: true retention-days: 7 # ──────────────────────────────────────────────────────────────────────── # validate-release: publish-dry-run guard. On each OS, spins a throwaway # verdaccio, publishes every @elaraai/* package + the east-c-cli launcher + # the host-platform binary exactly as the real jobs do — minus provenance, # which is npm-OIDC-only — then installs them into a fresh consumer and # smoke-tests. Matrixed over the 3 publish OSes so the per-platform npm # install contract (launcher → matching @elaraai/east-c-cli-) is # validated where it actually runs. Runs in BOTH dry and real so a real # release is proven installable BEFORE anything hits npm; the real npm # publish jobs gate on it. No secrets / no OIDC. (Locally, `act` covers the # ubuntu leg; macOS/Windows legs are CI-only.) # ──────────────────────────────────────────────────────────────────────── validate-release: strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-14, windows-latest] runs-on: ${{ matrix.os }} defaults: run: # Git Bash on Windows so the bash dry-run script + make builds run everywhere. shell: bash env: # east-c's Makefile picks Ninja on Windows; ensure cmake honours it too. CMAKE_GENERATOR: Ninja steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" - name: Install build deps (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y cmake build-essential libcurl4-openssl-dev - name: Install build deps (macOS) if: runner.os == 'macOS' run: brew install cmake || true - name: Install make (Windows) if: runner.os == 'Windows' run: choco install make --no-progress -y - name: Set up MSVC (Windows) if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - name: Install run: pnpm install - name: Release dry-run against verdaccio run: bash scripts/test-release-verdaccio.sh # ──────────────────────────────────────────────────────────────────────── # Tests are NOT re-run here: main is protected and only reachable via PRs that # pass the per-lib test workflows, so the release trusts main and only builds # + publishes. # # publish-npm: the public @elaraai/* packages via scripts/publish-npm.mjs. # ──────────────────────────────────────────────────────────────────────── publish-npm: needs: [prepare, validate-release] runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: { name: version-bump } - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" registry-url: "https://registry.npmjs.org" - name: Install run: pnpm install - name: Upgrade npm for trusted publishing run: npm install -g npm@latest - name: Build east working-directory: libs/east run: make build - name: Build east-node working-directory: libs/east-node run: make build - name: Build e3 working-directory: libs/e3 run: make build - name: Build east-ui working-directory: libs/east-ui run: make build env: NODE_OPTIONS: --max-old-space-size=4096 - name: Build east-py-datascience TypeScript working-directory: libs/east-py/packages/east-py-datascience run: pnpm run build - name: Build east-diagnostics working-directory: libs/east-diagnostics run: make build - name: Build eslint-plugin-east working-directory: libs/eslint-plugin-east run: make build - name: Build tsserver-plugin-east working-directory: libs/tsserver-plugin-east run: make build - name: Build create packages run: pnpm --filter @elaraai/scaffold-core --filter @elaraai/create-e3 --filter @elaraai/create-east run build - name: Publish (skip already-published) run: | node scripts/publish-npm.mjs \ "${{ needs.prepare.outputs.npm_tag }}" \ ${{ inputs.dry_run && '--dry-run' || '' }} # ──────────────────────────────────────────────────────────────────────── # 3. publish-vsix: build + (optionally) publish the VSCode extension. # ──────────────────────────────────────────────────────────────────────── publish-vsix: needs: prepare runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: { name: version-bump } - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" - name: Install run: pnpm install - name: Build east working-directory: libs/east run: make build - name: Build east-node working-directory: libs/east-node run: make build - name: Build e3 working-directory: libs/e3 run: make build - name: Build east-ui working-directory: libs/east-ui run: make build env: NODE_OPTIONS: --max-old-space-size=4096 - name: Build east-diagnostics + tsserver-plugin-east run: pnpm --filter @elaraai/east-diagnostics --filter @elaraai/tsserver-plugin-east run build - name: Build extension + webview run: pnpm --filter=east-ui-preview --filter=east-ui-preview-webview run build - name: Package VSIX working-directory: libs/east-ui/packages/east-ui-extension # The package script wraps vsce and injects @elaraai/tsserver-plugin-east # into the vsix (vsce --no-dependencies excludes node_modules). run: pnpm run package - name: Publish to VS Marketplace if: ${{ !inputs.dry_run }} working-directory: libs/east-ui/packages/east-ui-extension env: VSCE_PAT: ${{ secrets.VSCE_PAT }} run: | OUT=$(pnpm exec vsce publish --no-dependencies --packagePath *.vsix \ ${{ needs.prepare.outputs.is_prerelease == 'true' && '--pre-release' || '' }} 2>&1) || { if echo "$OUT" | grep -q "already exists"; then echo "Version already published, skipping." else echo "$OUT" && exit 1 fi } echo "$OUT" - name: Upload VSIX artifact uses: actions/upload-artifact@v4 with: name: vsix path: libs/east-ui/packages/east-ui-extension/*.vsix # ──────────────────────────────────────────────────────────────────────── # 4a. publish-pypi-pure: pure-Python wheels (east-py-std, east-py-io, # east-py-cli) via uv build. # ──────────────────────────────────────────────────────────────────────── publish-pypi-pure: needs: prepare runs-on: ubuntu-latest strategy: fail-fast: false matrix: pkg: [east-py-std, east-py-io, east-py-cli] steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: { name: version-bump } - uses: astral-sh/setup-uv@v3 - name: Set up Python 3.11 run: uv python install 3.11 - name: Build sdist + wheel working-directory: libs/east-py/packages/${{ matrix.pkg }} # --out-dir dist: uv otherwise writes to the uv workspace root # (libs/east-py/dist), where the upload step below wouldn't find it. run: uv build --sdist --wheel --out-dir dist - uses: actions/upload-artifact@v4 with: name: pypi-${{ matrix.pkg }} path: libs/east-py/packages/${{ matrix.pkg }}/dist/* # ──────────────────────────────────────────────────────────────────────── # 4b. publish-pypi-cython-wheels: cibuildwheel for the Cython packages # (east-py, east-py-datascience). Both link east-c and compile .pyx via # their scikit-build-core CMakeLists, so both need per-platform wheels. # ──────────────────────────────────────────────────────────────────────── publish-pypi-cython-wheels: needs: prepare strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-14, windows-latest] pkg: - { name: east-py, dir: libs/east-py/packages/east-py } - { name: east-py-datascience, dir: libs/east-py/packages/east-py-datascience } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: { name: version-bump } # On Linux cibuildwheel builds inside a manylinux container that only # sees the package dir — stage east-c (+ east-py .pxd) into _vendor/ so # the CMakeLists fallback finds them. macOS builds natively in the full # checkout where the monorepo is already on disk. - name: Vendor native deps if: runner.os == 'Linux' run: >- python3 libs/east-py/scripts/vendor-native.py --package ${{ matrix.pkg.dir }} ${{ matrix.pkg.name == 'east-py-datascience' && '--with-east-py-pxd' || '' }} - uses: pypa/cibuildwheel@v3.4.1 with: package-dir: ${{ matrix.pkg.dir }} env: CIBW_BUILD: "cp311-* cp312-* cp313-*" CIBW_SKIP: "*-musllinux_*" # macos-14 is Apple Silicon (arm64); also cross-compile Intel wheels. CIBW_ARCHS_MACOS: "arm64 x86_64" # The single shared east-c (libeast-c.so / .dylib / east_c_shared.dll) is # a payload of the core `east` package. Exclude it from wheel repair on # BOTH rows: in the datascience wheel a grafted copy would recreate the # multi-slab duplication; in the core wheel, relocating/renaming it would # break the cross-package resolution (datascience's $ORIGIN/../../east # RUNPATH on Unix, os.add_dll_directory(east/) on Windows). The runtime # resolves the one co-installed copy in east/. CIBW_REPAIR_WHEEL_COMMAND_LINUX: >- auditwheel repair --exclude libeast-c.so -w {dest_dir} {wheel} # delocate resolves the FULL dependency tree before --exclude applies, so # the datascience wheel — which deliberately ships without libeast-c (the # core wheel carries the one copy) — hits an unresolvable # @rpath/libeast-c.dylib and needs --ignore-missing-dependencies. Core # keeps strict resolution: its libeast-c is in-wheel, so a missing dep # there is a real packaging bug. CIBW_REPAIR_WHEEL_COMMAND_MACOS: ${{ matrix.pkg.name == 'east-py' && 'delocate-wheel --exclude libeast-c --require-archs {delocate_archs} -w {dest_dir} {wheel}' || 'delocate-wheel --exclude libeast-c --ignore-missing-dependencies --require-archs {delocate_archs} -w {dest_dir} {wheel}' }} # cibuildwheel doesn't bundle delvewheel; install it for the repair step. CIBW_BEFORE_BUILD_WINDOWS: pip install delvewheel # Core SHIPS east_c_shared.dll inside east/ — keep it un-vendored and its # name verbatim (--ignore-existing --no-mangle) so datascience resolves it # by its embedded import name. Datascience EXCLUDES it (core ships the one # copy). delvewheel still vendors other deps (e.g. the MSVC runtime). CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: ${{ matrix.pkg.name == 'east-py' && 'delvewheel repair --ignore-existing --no-mangle east_c_shared.dll -w {dest_dir} {wheel}' || 'delvewheel repair --exclude east_c_shared.dll -w {dest_dir} {wheel}' }} - uses: actions/upload-artifact@v4 with: name: pypi-${{ matrix.pkg.name }}-${{ matrix.os }} path: ./wheelhouse/*.whl # ──────────────────────────────────────────────────────────────────────── # 4c. publish-pypi-cython-sdist: sdists for the Cython packages # (east-py, east-py-datascience). # ──────────────────────────────────────────────────────────────────────── publish-pypi-cython-sdist: needs: prepare runs-on: ubuntu-latest strategy: fail-fast: false matrix: pkg: - { name: east-py, dir: libs/east-py/packages/east-py } - { name: east-py-datascience, dir: libs/east-py/packages/east-py-datascience } steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: { name: version-bump } - uses: astral-sh/setup-uv@v3 - name: Set up Python 3.11 run: uv python install 3.11 # Stage east-c (+ east-py .pxd) into _vendor/ so the sdist is # self-contained and installable without the monorepo. - name: Vendor native deps run: >- python3 libs/east-py/scripts/vendor-native.py --package ${{ matrix.pkg.dir }} ${{ matrix.pkg.name == 'east-py-datascience' && '--with-east-py-pxd' || '' }} - name: Build sdist working-directory: ${{ matrix.pkg.dir }} # --out-dir dist: uv otherwise writes to the uv workspace root # (libs/east-py/dist), where the upload step below wouldn't find it. run: uv build --sdist --out-dir dist - uses: actions/upload-artifact@v4 with: name: pypi-${{ matrix.pkg.name }}-sdist path: ${{ matrix.pkg.dir }}/dist/*.tar.gz # ──────────────────────────────────────────────────────────────────────── # 4d. publish-pypi-upload: gather all dists and upload via the official PyPA # action. skip-existing makes re-runs safe; the action handles upload retries # itself (no hand-rolled backoff). # ──────────────────────────────────────────────────────────────────────── publish-pypi-upload: needs: [publish-pypi-pure, publish-pypi-cython-wheels, publish-pypi-cython-sdist] if: ${{ !inputs.dry_run }} runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: path: dist-staging pattern: pypi-* merge-multiple: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist-staging skip-existing: true password: ${{ secrets.PYPI_TOKEN }} # ──────────────────────────────────────────────────────────────────────── # 5. publish-c-native: linux-x64 + linux-arm64 + macos-arm64 + macos-x64 binary tarballs. # ──────────────────────────────────────────────────────────────────────── publish-c-native: needs: prepare strategy: fail-fast: false matrix: include: - { os: ubuntu-latest, target: linux-x64 } - { os: ubuntu-24.04-arm, target: linux-arm64 } - { os: macos-14, target: macos-arm64 } - { os: macos-14, target: macos-x64, cmake_arch: x86_64 } - { os: windows-latest, target: win32-x64 } runs-on: ${{ matrix.os }} defaults: run: # Git Bash on Windows so the mkdir/cp/tar build + stage steps run on every OS. shell: bash steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: { name: version-bump } - name: Install build deps (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y cmake build-essential libcurl4-openssl-dev - name: Install build deps (macOS) if: runner.os == 'macOS' run: brew install cmake || true - name: Set up MSVC (Windows) if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - name: Build working-directory: libs/east-c run: | mkdir -p build && cd build if [ "$RUNNER_OS" = "Windows" ]; then # Ninja (single-config) so binaries land at build/packages/.../ # as on Unix; needs the MSVC env set up above. cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build . else ARCH_FLAG="${{ matrix.cmake_arch && format('-DCMAKE_OSX_ARCHITECTURES={0}', matrix.cmake_arch) || '' }}" cmake .. $ARCH_FLAG cmake --build . -j$(nproc 2>/dev/null || sysctl -n hw.logicalcpu) fi - name: Stage tarball run: | VERSION="${{ needs.prepare.outputs.version }}" TARGET="${{ matrix.target }}" STAGE="east-c-${VERSION}-${TARGET}" mkdir -p "$STAGE"/{bin,lib,include} if [ "$RUNNER_OS" = "Windows" ]; then cp libs/east-c/build/packages/east-c-cli/east-c.exe "$STAGE/bin/" cp libs/east-c/build/packages/east-c/east-c.lib "$STAGE/lib/" 2>/dev/null || true cp libs/east-c/build/packages/east-c-std/east-c-std.lib "$STAGE/lib/" 2>/dev/null || true else cp libs/east-c/build/packages/east-c-cli/east-c "$STAGE/bin/" cp libs/east-c/build/packages/east-c/libeast-c.a "$STAGE/lib/" 2>/dev/null || true cp libs/east-c/build/packages/east-c-std/libeast-c-std.a "$STAGE/lib/" 2>/dev/null || true fi cp -r libs/east-c/packages/east-c/include/east "$STAGE/include/" cp libs/east-c/LICENSE "$STAGE/" 2>/dev/null || cp LICENSE "$STAGE/" 2>/dev/null || true cp libs/east-c/README.md "$STAGE/" 2>/dev/null || true tar -czf "${STAGE}.tar.gz" "$STAGE" - uses: actions/upload-artifact@v4 with: name: c-native-${{ matrix.target }} path: east-c-*.tar.gz # Per-platform npm package — just the east-c binary, a generated # package.json (`os` / `cpu` gating + `files: ["east-c*",...]`), a # generated minimal README, and the BSL LICENSE. Packed with # `npm pack` so publish-c-npm can publish the .tgz directly. - name: Set up Node (per-platform npm pack) uses: actions/setup-node@v4 with: node-version: 22 - name: Stage per-platform npm package run: | VERSION="${{ needs.prepare.outputs.version }}" # The publish-c-native matrix uses `macos-*` for the tarball names # (the historical convention). The npm per-platform package uses # `darwin-*` so the launcher's `require.resolve` finds it via # `process.platform-process.arch` on a real Mac. Map here. case "${{ matrix.target }}" in macos-arm64) NPM_TARGET=darwin-arm64 ;; macos-x64) NPM_TARGET=darwin-x64 ;; *) NPM_TARGET="${{ matrix.target }}" ;; esac STAGE_NPM="east-c-cli-${NPM_TARGET}-${VERSION}" mkdir -p "$STAGE_NPM" if [ "$RUNNER_OS" = "Windows" ]; then cp libs/east-c/build/packages/east-c-cli/east-c.exe "$STAGE_NPM/" else cp libs/east-c/build/packages/east-c-cli/east-c "$STAGE_NPM/" fi cp libs/east-c/packages/east-c-cli/LICENSE.md "$STAGE_NPM/" node scripts/emit-east-c-platform-manifest.mjs \ --target "$NPM_TARGET" --version "$VERSION" \ --out "$STAGE_NPM/package.json" node scripts/emit-east-c-platform-readme.mjs \ --target "$NPM_TARGET" --version "$VERSION" \ --out "$STAGE_NPM/README.md" ( cd "$STAGE_NPM" && npm pack --pack-destination .. ) - uses: actions/upload-artifact@v4 with: name: c-npm-${{ matrix.target }} # npm pack folds the scope into the filename: # @elaraai/east-c-cli- → elaraai-east-c-cli--.tgz path: elaraai-east-c-cli-*.tgz if-no-files-found: error # ──────────────────────────────────────────────────────────────────────── # 5b. publish-c-npm: publishes the @elaraai/east-c-cli launcher + the 5 # per-platform packages (@elaraai/east-c-cli-) to npm with # provenance. The 5 .tgz tarballs are downloaded as artifacts from # publish-c-native; the launcher is pnpm-published from source. # # Trusted publishing must be configured per package name in the npm # UI before this job's first non-dry run — see scripts/bootstrap- # east-c-npm.mjs and libs/east-c/docs/npm-runner-distribution.md. # ──────────────────────────────────────────────────────────────────────── publish-c-npm: needs: [prepare, publish-c-native, validate-release] runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: { name: version-bump } - uses: actions/download-artifact@v4 with: path: c-npm pattern: c-npm-* merge-multiple: true - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" registry-url: "https://registry.npmjs.org" - name: Upgrade npm for trusted publishing run: npm install -g npm@latest - name: Publish per-platform packages run: | VERSION="${{ needs.prepare.outputs.version }}" TAG="${{ needs.prepare.outputs.npm_tag }}" DRY=${{ inputs.dry_run && '--dry-run' || '' }} for target in linux-x64 linux-arm64 darwin-arm64 darwin-x64 win32-x64; do # npm pack folds the scope into the filename (elaraai- prefix). # Leading ./ is required: without it npm parses the single-slash # path as a GitHub owner/repo shorthand and tries git ls-remote. TGZ="./c-npm/elaraai-east-c-cli-${target}-${VERSION}.tgz" if [ ! -f "$TGZ" ]; then echo "::error::Missing artifact $TGZ" exit 1 fi NAME="@elaraai/east-c-cli-${target}" # Skip if already published — registry rejects re-publishing the # same version, and a partial-failure retry must be safe. if [ -z "$DRY" ] && npm view "${NAME}@${VERSION}" version >/dev/null 2>&1; then echo " skip: ${NAME}@${VERSION} already on npm" continue fi echo " publish: ${NAME}@${VERSION} ← ${TGZ}" npm publish "$TGZ" --access public --provenance --tag "$TAG" $DRY done - name: Inject launcher optionalDependencies (not committed; see scripts/inject-east-c-platform-deps.mjs) run: | node scripts/inject-east-c-platform-deps.mjs \ --version "${{ needs.prepare.outputs.version }}" \ --file libs/east-c/packages/east-c-cli/package.json - name: Publish launcher working-directory: libs/east-c/packages/east-c-cli run: | VERSION="${{ needs.prepare.outputs.version }}" TAG="${{ needs.prepare.outputs.npm_tag }}" DRY=${{ inputs.dry_run && '--dry-run' || '' }} if [ -z "$DRY" ] && npm view "@elaraai/east-c-cli@${VERSION}" version >/dev/null 2>&1; then echo " skip: @elaraai/east-c-cli@${VERSION} already on npm" exit 0 fi pnpm publish --access public --provenance --tag "$TAG" --no-git-checks $DRY # ──────────────────────────────────────────────────────────────────────── # 6. finalize: on success, commit + tag + push, create GitHub Release with # VSIX + C binaries attached. Skipped entirely on dry run. # ──────────────────────────────────────────────────────────────────────── finalize: needs: [prepare, publish-npm, publish-vsix, publish-pypi-upload, publish-c-native, publish-c-npm] if: >- ${{ !inputs.dry_run && needs.prepare.result == 'success' && needs.publish-npm.result == 'success' && needs.publish-vsix.result == 'success' && needs.publish-pypi-upload.result == 'success' && needs.publish-c-native.result == 'success' && needs.publish-c-npm.result == 'success' }} runs-on: ubuntu-latest permissions: contents: write steps: - name: Generate app token uses: actions/create-github-app-token@v1 id: app-token with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@v4 with: token: ${{ steps.app-token.outputs.token }} - uses: actions/download-artifact@v4 with: { name: version-bump } - name: Configure git run: | git config user.name "elara-ci[bot]" git config user.email "elara-ci[bot]@users.noreply.github.com" # Guard: the bump ran in `prepare`; this job only sees what survived the # artifact round-trip. Re-check alignment here so a dropped manifest # fails the release instead of committing drift. - uses: actions/setup-node@v4 with: node-version: 22 - name: Verify manifests aligned run: node scripts/check-version-drift.mjs - name: Commit + tag + push run: | VERSION="${{ needs.prepare.outputs.version }}" git add -A git diff --staged --quiet || git commit -m "chore: release v${VERSION}" git tag "v${VERSION}" 2>/dev/null || echo "Tag v${VERSION} already exists, skipping." git push origin HEAD --tags - name: Download release assets uses: actions/download-artifact@v4 with: path: release-assets pattern: "{vsix,c-native-*}" merge-multiple: true - name: Create GitHub Release env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | VERSION="${{ needs.prepare.outputs.version }}" PRE_FLAG="" if [[ "${{ needs.prepare.outputs.is_prerelease }}" == "true" ]]; then PRE_FLAG="--prerelease" fi if gh release view "v${VERSION}" >/dev/null 2>&1; then echo "Release v${VERSION} already exists, skipping creation." else gh release create "v${VERSION}" \ --title "v${VERSION}" \ --generate-notes \ $PRE_FLAG \ release-assets/* fi