fix: resolve signed-out model slot references to env slots, add regression test coverage #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish ZCode CLI release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| publish: | |
| description: Create the matching Git tag and GitHub release | |
| required: true | |
| default: true | |
| type: boolean | |
| pull_request: | |
| branches: | |
| - main | |
| types: | |
| - closed | |
| permissions: {} | |
| concurrency: | |
| group: zcode-cli-publish | |
| cancel-in-progress: false | |
| jobs: | |
| validate: | |
| if: >- | |
| ( | |
| github.event_name == 'workflow_dispatch' && | |
| github.ref_name == github.event.repository.default_branch | |
| ) || | |
| ( | |
| github.event_name == 'pull_request' && | |
| github.event.pull_request.merged == true && | |
| github.event.pull_request.head.repo.full_name == github.repository && | |
| ( | |
| github.event.pull_request.head.ref == 'release/zcode-cli' || | |
| github.event.pull_request.head.ref == 'release/zcode-upstream' | |
| ) | |
| ) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| outputs: | |
| artifact_name: ${{ steps.package.outputs.artifact_name }} | |
| package_name: ${{ steps.expected.outputs.package_name }} | |
| package_version: ${{ steps.expected.outputs.package_version }} | |
| expected_commit: ${{ steps.expected.outputs.commit }} | |
| tarball: ${{ steps.package.outputs.tarball }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| persist-credentials: false | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.sha }} | |
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | |
| with: | |
| node-version: 24 | |
| package-manager-cache: false | |
| - name: Prepare reproducible npm toolchain | |
| run: | | |
| npm install --global npm@12.0.1 --ignore-scripts | |
| node --version | |
| npm --version | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.12 | |
| - name: Install extraction tools | |
| run: sudo apt-get update && sudo apt-get install -y p7zip-full | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Capture committed release | |
| id: expected | |
| run: | | |
| git ls-files --error-unmatch zcode-runtime.lock.json >/dev/null | |
| PACKAGE_NAME=$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name") | |
| PACKAGE_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version") | |
| echo "package_name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT" | |
| echo "package_version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Build committed release | |
| run: bun run release:build | |
| - name: Verify release did not drift | |
| env: | |
| EXPECTED_VERSION: ${{ steps.expected.outputs.package_version }} | |
| run: | | |
| ACTUAL_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version") | |
| if [[ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]]; then | |
| echo "The locked release changed while rebuilding: expected ${EXPECTED_VERSION}, resolved ${ACTUAL_VERSION}." >&2 | |
| exit 1 | |
| fi | |
| git diff --exit-code -- package.json zcode-runtime.lock.json | |
| - name: Pack and install-test release | |
| id: package | |
| env: | |
| ARTIFACT_NAME: validated-release-${{ github.run_id }} | |
| run: | | |
| bun run release:pack | |
| TARBALL=$(node -p "JSON.parse(require('fs').readFileSync('.release/release.json', 'utf8')).tarball") | |
| [[ -f "$TARBALL" ]] || { echo "Validated tarball is missing: $TARBALL" >&2; exit 1; } | |
| rm -rf release-artifact | |
| mkdir release-artifact | |
| cp .release/release.json "$TARBALL" release-artifact/ | |
| echo "artifact_name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" | |
| - name: Upload validated release | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: validated-release-${{ github.run_id }} | |
| path: release-artifact | |
| if-no-files-found: error | |
| retention-days: 7 | |
| publish: | |
| needs: validate | |
| if: needs.validate.result == 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| persist-credentials: false | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.sha }} | |
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | |
| with: | |
| node-version: 24 | |
| package-manager-cache: false | |
| - name: Download validated release | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: ${{ needs.validate.outputs.artifact_name }} | |
| path: .release | |
| - name: Verify validated tarball | |
| env: | |
| EXPECTED_NAME: ${{ needs.validate.outputs.package_name }} | |
| EXPECTED_VERSION: ${{ needs.validate.outputs.package_version }} | |
| EXPECTED_COMMIT: ${{ needs.validate.outputs.expected_commit }} | |
| VALIDATED_TARBALL: ${{ needs.validate.outputs.tarball }} | |
| run: | | |
| PACKAGE_NAME=$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name") | |
| PACKAGE_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version") | |
| COMMIT=$(git rev-parse HEAD) | |
| [[ "$PACKAGE_NAME" == "$EXPECTED_NAME" ]] || { echo "Package name changed while transferring the release." >&2; exit 1; } | |
| [[ "$PACKAGE_VERSION" == "$EXPECTED_VERSION" ]] || { echo "Package version changed while transferring the release." >&2; exit 1; } | |
| [[ "$COMMIT" == "$EXPECTED_COMMIT" ]] || { echo "Release commit changed while transferring the release." >&2; exit 1; } | |
| git ls-files --error-unmatch zcode-runtime.lock.json >/dev/null | |
| [[ -f "$VALIDATED_TARBALL" ]] || { echo "Validated tarball is missing: $VALIDATED_TARBALL" >&2; exit 1; } | |
| node - "$VALIDATED_TARBALL" "$EXPECTED_NAME" "$EXPECTED_VERSION" <<'NODE' | |
| const [tarball, expectedName, expectedVersion] = process.argv.slice(2); | |
| const release = JSON.parse(require("fs").readFileSync(".release/release.json", "utf8")); | |
| if (release.name !== expectedName || release.version !== expectedVersion) { | |
| throw new Error("Validated release metadata does not match the checked-out package."); | |
| } | |
| if (release.tarball !== tarball) { | |
| throw new Error(`Validated tarball path differs: ${release.tarball} != ${tarball}`); | |
| } | |
| NODE | |
| - name: Rebuild tarball without project scripts | |
| env: | |
| VALIDATED_TARBALL: ${{ needs.validate.outputs.tarball }} | |
| run: | | |
| rm -rf .release/publish .release/rebuilt | |
| mkdir -p .release/publish .release/rebuilt | |
| tar -xzf "$VALIDATED_TARBALL" -C .release/publish | |
| [[ -f .release/publish/package/package.json ]] || { echo "Validated tarball has no package root." >&2; exit 1; } | |
| npm pack ./.release/publish/package \ | |
| --ignore-scripts \ | |
| --json \ | |
| --pack-destination .release/rebuilt > .release/rebuilt.json | |
| REBUILT_TARBALL=$(find .release/rebuilt -maxdepth 1 -type f -name '*.tgz' -print -quit) | |
| [[ -n "$REBUILT_TARBALL" ]] || { echo "Rebuilding the release tarball produced no archive." >&2; exit 1; } | |
| # npm pack names the archive after the package; the validated asset | |
| # carries the same project name. Compare the contents under the | |
| # rebuilt name, then publish the renamed asset. | |
| mv "$REBUILT_TARBALL" ".release/rebuilt/$(basename "$VALIDATED_TARBALL")" | |
| REBUILT_TARBALL=".release/rebuilt/$(basename "$VALIDATED_TARBALL")" | |
| if ! cmp -- "$VALIDATED_TARBALL" "$REBUILT_TARBALL"; then | |
| echo "The privileged publish job produced a different tarball than the validated job." >&2 | |
| exit 1 | |
| fi | |
| - name: Inspect release state | |
| id: release | |
| shell: bash | |
| env: | |
| EXPECTED_COMMIT: ${{ needs.validate.outputs.expected_commit }} | |
| PACKAGE_NAME: ${{ needs.validate.outputs.package_name }} | |
| PACKAGE_VERSION: ${{ needs.validate.outputs.package_version }} | |
| PUBLISH_REQUESTED: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="v${PACKAGE_VERSION}" | |
| git fetch --tags --force origin | |
| TAG_COMMIT=$(git rev-list -n 1 "$TAG" 2>/dev/null || true) | |
| if [[ -n "$TAG_COMMIT" && "$TAG_COMMIT" != "$EXPECTED_COMMIT" ]]; then | |
| echo "${TAG} already points to ${TAG_COMMIT}, not ${EXPECTED_COMMIT}." >&2 | |
| exit 1 | |
| fi | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| RELEASE_EXISTS=true | |
| else | |
| RELEASE_EXISTS=false | |
| fi | |
| echo "enabled=$PUBLISH_REQUESTED" >> "$GITHUB_OUTPUT" | |
| echo "create_tag=$([[ -n "$TAG_COMMIT" ]] && echo false || echo true)" >> "$GITHUB_OUTPUT" | |
| echo "create_release=$([[ "$RELEASE_EXISTS" == "true" ]] && echo false || echo true)" >> "$GITHUB_OUTPUT" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Create immutable Git tag | |
| if: steps.release.outputs.enabled == 'true' && steps.release.outputs.create_tag == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PACKAGE_NAME: ${{ needs.validate.outputs.package_name }} | |
| PACKAGE_VERSION: ${{ needs.validate.outputs.package_version }} | |
| EXPECTED_COMMIT: ${{ needs.validate.outputs.expected_commit }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| run: | | |
| TAG_OBJECT=$(gh api --method POST "repos/${GITHUB_REPOSITORY}/git/tags" \ | |
| -f tag="$TAG" \ | |
| -f message="${PACKAGE_NAME}@${PACKAGE_VERSION}" \ | |
| -f object="$EXPECTED_COMMIT" \ | |
| -f type=commit \ | |
| --jq .sha) | |
| gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ | |
| -f ref="refs/tags/${TAG}" \ | |
| -f sha="$TAG_OBJECT" >/dev/null | |
| - name: Create GitHub Release | |
| if: steps.release.outputs.enabled == 'true' && steps.release.outputs.create_release == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PACKAGE_NAME: ${{ needs.validate.outputs.package_name }} | |
| PACKAGE_VERSION: ${{ needs.validate.outputs.package_version }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| run: | | |
| gh release create "$TAG" --verify-tag --generate-notes --title "${PACKAGE_NAME}@${PACKAGE_VERSION}" | |
| gh release upload "$TAG" ".release/zcode-cli-${PACKAGE_VERSION}.tgz" --clobber | |
| - name: Attach tarball to an existing GitHub Release | |
| if: steps.release.outputs.enabled == 'true' && steps.release.outputs.create_release == 'false' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PACKAGE_NAME: ${{ needs.validate.outputs.package_name }} | |
| PACKAGE_VERSION: ${{ needs.validate.outputs.package_version }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| run: gh release upload "$TAG" ".release/zcode-cli-${PACKAGE_VERSION}.tgz" --clobber | |
| - name: Summarize release | |
| if: always() | |
| env: | |
| ENABLED: ${{ steps.release.outputs.enabled }} | |
| PACKAGE_NAME: ${{ needs.validate.outputs.package_name }} | |
| PACKAGE_VERSION: ${{ needs.validate.outputs.package_version }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| run: | | |
| echo "Release: ${PACKAGE_NAME}@${PACKAGE_VERSION}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Mutations enabled: ${ENABLED:-unknown}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Git tag: ${TAG:-unknown}" >> "$GITHUB_STEP_SUMMARY" |