Repository navigation
v0.1.0-beta.43 #125
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: "Existing stable release tag to recover" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.event.release.tag_name }} | |
| TRACEDECAY_SKIP_DASHBOARD_BUILD: "1" | |
| concurrency: | |
| group: release-${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.event.release.tag_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| validate-release: | |
| name: Resolve release source | |
| if: >- | |
| github.repository == 'ScriptedAlchemy/tracedecay' && | |
| ( | |
| (github.event_name == 'release' && !github.event.release.prerelease) || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| attestations: read | |
| outputs: | |
| source_sha: ${{ steps.release.outputs.source_sha }} | |
| source_epoch: ${{ steps.release.outputs.source_epoch }} | |
| tag: ${{ steps.release.outputs.tag }} | |
| version: ${{ steps.release.outputs.version }} | |
| prerelease: ${{ steps.release.outputs.prerelease }} | |
| matrix: ${{ steps.release.outputs.matrix }} | |
| build_required: ${{ steps.release.outputs.build_required }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ env.RELEASE_TAG }} | |
| persist-credentials: false | |
| - name: Validate manual stable recovery | |
| if: github.event_name == 'workflow_dispatch' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| test "$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft,isPrerelease --jq '[.isPrerelease, .isDraft] | @tsv')" = $'false\tfalse' | |
| - name: Resolve release identity | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| source_sha="$(git rev-parse HEAD)" | |
| source_epoch="$(git show -s --format=%ct "$source_sha")" | |
| version="$(cargo metadata --no-deps --locked --format-version 1 | jq -r '.packages[] | select(.name == "tracedecay") | .version')" | |
| test -n "$version" | |
| test "$version" != "null" | |
| [[ "$version" != *-* ]] | |
| tag="v${version}" | |
| test "$RELEASE_TAG" = "$tag" | |
| test "$GITHUB_REF" = "refs/tags/${tag}" | |
| test "$GITHUB_SHA" = "$source_sha" | |
| if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then | |
| git fetch --force origin "refs/tags/${tag}:refs/tags/${tag}" | |
| tag_sha="$(git rev-list -n 1 "$tag")" | |
| if [[ "$tag_sha" != "$source_sha" ]]; then | |
| echo "::error::${tag} targets ${tag_sha}, not release source ${source_sha}" | |
| exit 1 | |
| fi | |
| fi | |
| { | |
| echo "source_sha=${source_sha}" | |
| echo "source_epoch=${source_epoch}" | |
| echo "tag=${tag}" | |
| echo "version=${version}" | |
| echo "prerelease=false" | |
| } >> "$GITHUB_OUTPUT" | |
| gh api "repos/${{ github.repository }}/releases/tags/${tag}" \ | |
| --jq '.assets[].name' > remote-asset-names | |
| python3 scripts/plan-release-recovery.py \ | |
| --manifest .github/release-targets.json \ | |
| --tag "$tag" \ | |
| --profile stable \ | |
| --asset-names remote-asset-names \ | |
| --retained-output retained-asset-names \ | |
| --github-output "$GITHUB_OUTPUT" | |
| mkdir retained-assets | |
| scripts/verify-retained-release-assets.sh \ | |
| --tag "$tag" \ | |
| --repo "${{ github.repository }}" \ | |
| --signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \ | |
| --source-digest "$source_sha" \ | |
| --asset-names retained-asset-names \ | |
| --download-dir retained-assets | |
| dashboard-assets: | |
| name: Build dashboard artifact | |
| needs: validate-release | |
| if: needs.validate-release.outputs.build_required == 'true' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.validate-release.outputs.source_sha }} | |
| - name: Checkout release automation | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.validate-release.outputs.source_sha }} | |
| path: .release-automation | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: dashboard/package-lock.json | |
| - name: Install dashboard dependencies | |
| working-directory: dashboard | |
| run: npm ci | |
| - name: Build dashboard | |
| working-directory: dashboard | |
| run: npm run build | |
| - name: Validate dashboard bundle | |
| run: python3 .release-automation/scripts/check-dashboard-bundle.py dashboard/app-dist | |
| - name: Upload dashboard artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dashboard-app-dist | |
| path: dashboard/app-dist | |
| if-no-files-found: error | |
| include-hidden-files: true | |
| build: | |
| name: Build ${{ matrix.name }} | |
| needs: [validate-release, dashboard-assets] | |
| if: needs.validate-release.outputs.build_required == 'true' | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.validate-release.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.validate-release.outputs.source_sha }} | |
| - name: Download dashboard artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dashboard-app-dist | |
| path: dashboard/app-dist | |
| - name: Validate downloaded dashboard bundle | |
| run: python3 scripts/check-dashboard-bundle.py dashboard/app-dist | |
| # Skip-mode cargo builds require the expected digest of the validated | |
| # prebuilt bundle; the CLI build script refuses to skip without it. | |
| - name: Export dashboard bundle digest for skip-mode builds | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| digest="$(python3 scripts/check-dashboard-bundle.py dashboard/app-dist --print-digest)" | |
| echo "TRACEDECAY_DASHBOARD_BUNDLE_SHA256=${digest}" >> "$GITHUB_ENV" | |
| - name: Validate portable distribution harnesses | |
| run: | | |
| python3 scripts/test-check-packaged-lsp-bridge.py | |
| python3 scripts/test-build-mcpb.py | |
| python3 scripts/test-resolve-installed-binary.py | |
| python3 scripts/test-check-packaged-mcp-stdio.py | |
| python3 scripts/test-check-distribution-feature-wiring.py | |
| python3 scripts/test-resolve-release-source-profile.py | |
| python3 scripts/test-package-release-archive.py | |
| python3 scripts/test-plan-release-recovery.py | |
| python3 scripts/test-check-release-artifacts.py | |
| bash scripts/test-check-distribution-clean-source.sh | |
| bash scripts/test-check-distribution-snapshot.sh | |
| bash scripts/test-check-distribution-reuse.sh | |
| # rust-toolchain.toml is the only compiler this job may install. | |
| # Installing floating stable as well fragments rust-cache's environment | |
| # key whenever stable moves (measured on Release Beta 35271070760). | |
| - name: Resolve pinned toolchain | |
| id: pin | |
| shell: bash | |
| run: | | |
| python3 -c 'import pathlib, tomllib; print("toolchain=" + tomllib.loads(pathlib.Path("rust-toolchain.toml").read_text(encoding="utf-8"))["toolchain"]["channel"])' >> "$GITHUB_OUTPUT" | |
| - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # 1.97.1 pin | |
| with: | |
| toolchain: ${{ steps.pin.outputs.toolchain }} | |
| targets: ${{ matrix.target }} | |
| # Restore before the first cargo invocation (`cargo tree` in the | |
| # profile resolver). Tag runs can restore only same-ref or default-branch | |
| # caches; keep this shared-key stable so a later master writer can seed | |
| # them. cache-workspace-crates stays false so release blobs stay inside | |
| # the 10 GB GitHub cache budget. | |
| - name: Cache Rust build | |
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| shared-key: release-${{ matrix.target }} | |
| cache-workspace-crates: "false" | |
| cache-on-failure: true | |
| - name: Resolve release source feature profile | |
| id: release-profile | |
| shell: bash | |
| run: | | |
| python3 scripts/resolve-release-source-profile.py \ | |
| --source . \ | |
| --target "${{ matrix.target }}" \ | |
| --github-output "$GITHUB_OUTPUT" | |
| if [[ -f crates/tracedecay-cli/Cargo.toml ]]; then | |
| echo "binary_args=--package tracedecay-cli --bin tracedecay" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "binary_args=--bin tracedecay" >> "$GITHUB_OUTPUT" | |
| fi | |
| # make-default symlinks ld to mold, which works on this | |
| # runner's GCC 11 (unlike -fuse-ld=mold, which needs GCC 12+). | |
| - name: Install mold | |
| if: runner.os == 'Linux' | |
| uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 | |
| with: | |
| mold-version: 2.41.0 | |
| make-default: true | |
| - name: Verify mold is the default linker | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mold --version | |
| ld --version | head -n 1 | grep -i mold | |
| - name: Use lld-link | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| where.exe lld-link | |
| lld-link --version | |
| "RUSTFLAGS=-C linker=lld-link.exe" >> $env:GITHUB_ENV | |
| - name: Build release binary for packaging | |
| run: cargo build ${{ steps.release-profile.outputs.binary_args }} --release --target ${{ matrix.target }} ${{ steps.release-profile.outputs.cargo_args }} --locked | |
| - name: Smoke release binary | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| binary="target/${{ matrix.target }}/release/tracedecay" | |
| if [[ "$RUNNER_OS" == "Windows" ]]; then | |
| binary="${binary}.exe" | |
| fi | |
| test -f "$binary" | |
| "$binary" --version | |
| "$binary" --help >/dev/null | |
| - name: Get version | |
| id: version | |
| shell: bash | |
| run: | | |
| echo "tag=${{ needs.validate-release.outputs.tag }}" >> "$GITHUB_OUTPUT" | |
| echo "version=${{ needs.validate-release.outputs.version }}" >> "$GITHUB_OUTPUT" | |
| # --- MCP Registry bundle (all platforms) --- | |
| - name: Package MCPB | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| binary="target/${{ matrix.target }}/release/tracedecay" | |
| if [[ "$RUNNER_OS" == "Windows" ]]; then | |
| binary="${binary}.exe" | |
| fi | |
| bundle="tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.mcpb" | |
| python3 scripts/build-mcpb.py build \ | |
| --binary "$binary" \ | |
| --output "$bundle" \ | |
| --version "${{ steps.version.outputs.version }}" \ | |
| --platform "${{ matrix.name }}" | |
| test -s "$bundle" | |
| python3 scripts/build-mcpb.py verify \ | |
| --bundle "$bundle" \ | |
| --version "${{ steps.version.outputs.version }}" \ | |
| --platform "${{ matrix.name }}" | |
| - name: Verify MCPB runtime (unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bundle="tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.mcpb" | |
| rm -rf verify-mcpb | |
| # unzip applies the mode bits build-mcpb.py records in the archive, | |
| # which is what an MCPB host does. `python3 -m zipfile -e` drops | |
| # them, so `test -x` failed on every Unix target with no output. | |
| unzip -q "$bundle" -d verify-mcpb | |
| test -x verify-mcpb/server/tracedecay | |
| verify-mcpb/server/tracedecay --version | |
| verify-mcpb/server/tracedecay --help >/dev/null | |
| - name: Upload MCPB artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: mcpb-${{ matrix.name }} | |
| path: tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.mcpb | |
| if-no-files-found: error | |
| # --- Binary archive (all platforms) --- | |
| - name: Package binary archive | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| binary="target/${{ matrix.target }}/release/tracedecay" | |
| entry_name="tracedecay" | |
| if [[ "$RUNNER_OS" == "Windows" ]]; then | |
| binary="${binary}.exe" | |
| entry_name="${entry_name}.exe" | |
| fi | |
| python3 scripts/package-release-archive.py \ | |
| --binary "$binary" \ | |
| --output "tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.${{ matrix.archive }}" \ | |
| --format "${{ matrix.archive }}" \ | |
| --entry-name "$entry_name" \ | |
| --epoch "${{ needs.validate-release.outputs.source_epoch }}" | |
| - name: Verify binary archive (unix) | |
| if: matrix.archive == 'tar.gz' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| archive="tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.tar.gz" | |
| rm -rf verify-binary | |
| mkdir verify-binary | |
| tar xzf "$archive" -C verify-binary | |
| test -x verify-binary/tracedecay | |
| verify-binary/tracedecay --version | |
| verify-binary/tracedecay --help >/dev/null | |
| - name: Verify binary archive (windows) | |
| if: matrix.archive == 'zip' | |
| shell: pwsh | |
| run: | | |
| $archive = "tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.zip" | |
| Remove-Item verify-binary -Recurse -Force -ErrorAction SilentlyContinue | |
| Expand-Archive -Path $archive -DestinationPath verify-binary | |
| & "./verify-binary/tracedecay.exe" --version | |
| if ($LASTEXITCODE -ne 0) { throw "Packaged binary failed --version" } | |
| & "./verify-binary/tracedecay.exe" --help | Out-Null | |
| if ($LASTEXITCODE -ne 0) { throw "Packaged binary failed --help" } | |
| - name: Upload binary archive artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: binary-${{ matrix.name }} | |
| path: tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.${{ matrix.archive }} | |
| if-no-files-found: error | |
| verify-release: | |
| name: Verify immutable GitHub release | |
| needs: [validate-release, dashboard-assets, build] | |
| if: >- | |
| always() && | |
| needs.validate-release.result == 'success' && | |
| (needs.dashboard-assets.result == 'success' || needs.dashboard-assets.result == 'skipped') && | |
| needs.build.result != 'cancelled' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ needs.validate-release.outputs.source_sha }} | |
| persist-credentials: false | |
| - name: Download validated dashboard artifact | |
| if: needs.validate-release.outputs.build_required == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dashboard-app-dist | |
| path: dashboard/app-dist | |
| - name: Validate downloaded dashboard bundle | |
| if: needs.validate-release.outputs.build_required == 'true' | |
| run: python3 scripts/check-dashboard-bundle.py dashboard/app-dist | |
| - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable | |
| - name: Revalidate release tag target | |
| env: | |
| TAG: ${{ needs.validate-release.outputs.tag }} | |
| SOURCE_SHA: ${{ needs.validate-release.outputs.source_sha }} | |
| run: | | |
| set -euo pipefail | |
| if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then | |
| git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}" | |
| test "$(git rev-list -n 1 "$TAG")" = "$SOURCE_SHA" | |
| fi | |
| - name: Verify published tag and release target | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.validate-release.outputs.tag }} | |
| SOURCE_SHA: ${{ needs.validate-release.outputs.source_sha }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}" | |
| test "$(git rev-list -n 1 "$TAG")" = "$SOURCE_SHA" | |
| gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null | |
| publish-assets: | |
| name: Publish release assets | |
| needs: [validate-release, build, verify-release] | |
| # A failed build matrix still publishes every target that did build; the | |
| # recovery planner rebuilds only the missing ones. Only a cancelled build | |
| # withholds publication. Marking the release latest, refreshing | |
| # server.json, and publishing the SDK stay gated on a complete build. | |
| if: >- | |
| always() && | |
| needs.validate-release.result == 'success' && | |
| needs.verify-release.result == 'success' && | |
| needs.build.result != 'cancelled' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Checkout release automation | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.validate-release.outputs.source_sha }} | |
| path: .release-automation | |
| persist-credentials: false | |
| - name: Get release tag | |
| id: release | |
| run: | | |
| echo "tag=${{ needs.validate-release.outputs.tag }}" >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ steps.release.outputs.tag }} | |
| persist-credentials: false | |
| - name: Download newly built binary artifacts | |
| if: needs.validate-release.outputs.build_required == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: binary-* | |
| path: new-binaries | |
| merge-multiple: true | |
| - name: Download newly built MCPB artifacts | |
| if: needs.validate-release.outputs.build_required == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: mcpb-* | |
| path: new-mcpbs | |
| merge-multiple: true | |
| - name: Assemble exact retained and newly built artifacts | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ steps.release.outputs.tag }}" | |
| SOURCE_SHA="${{ needs.validate-release.outputs.source_sha }}" | |
| VERSION="${TAG#v}" | |
| mkdir binaries mcpbs retained-assets | |
| gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \ | |
| --jq '.assets[].name' > remote-asset-names | |
| python3 .release-automation/scripts/plan-release-recovery.py \ | |
| --manifest .release-automation/.github/release-targets.json \ | |
| --tag "$TAG" \ | |
| --profile stable \ | |
| --asset-names remote-asset-names \ | |
| --retained-output retained-asset-names \ | |
| --github-output planner-output | |
| .release-automation/scripts/verify-retained-release-assets.sh \ | |
| --tag "$TAG" \ | |
| --repo "${{ github.repository }}" \ | |
| --signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \ | |
| --source-digest "$SOURCE_SHA" \ | |
| --asset-names retained-asset-names \ | |
| --download-dir retained-assets | |
| while IFS= read -r asset; do | |
| [[ -z "$asset" ]] && continue | |
| if [[ "$asset" == *.mcpb ]]; then | |
| cp "retained-assets/$asset" "mcpbs/$asset" | |
| else | |
| cp "retained-assets/$asset" "binaries/$asset" | |
| fi | |
| done < retained-asset-names | |
| if [[ "${{ needs.validate-release.outputs.build_required }}" == "true" ]]; then | |
| shopt -s nullglob | |
| for artifact in new-binaries/tracedecay-*; do | |
| destination="binaries/$(basename "$artifact")" | |
| [[ -e "$destination" ]] || cp "$artifact" "$destination" | |
| done | |
| for artifact in new-mcpbs/*.mcpb; do | |
| destination="mcpbs/$(basename "$artifact")" | |
| [[ -e "$destination" ]] || cp "$artifact" "$destination" | |
| done | |
| shopt -u nullglob | |
| fi | |
| python3 .release-automation/scripts/check-release-artifacts.py \ | |
| --manifest .release-automation/.github/release-targets.json \ | |
| --tag "$TAG" \ | |
| --binaries binaries \ | |
| --mcpbs mcpbs \ | |
| --allow-missing-targets | |
| for bundle in mcpbs/*.mcpb; do | |
| platform="${bundle#mcpbs/tracedecay-"${TAG}"-}" | |
| platform="${platform%.mcpb}" | |
| python3 .release-automation/scripts/build-mcpb.py verify \ | |
| --bundle "$bundle" \ | |
| --version "$VERSION" \ | |
| --platform "$platform" | |
| done | |
| - name: Count newly built artifacts | |
| id: built | |
| run: | | |
| shopt -s nullglob | |
| archives=(new-binaries/tracedecay-*) | |
| bundles=(new-mcpbs/*.mcpb) | |
| echo "archives=${#archives[@]}" >>"$GITHUB_OUTPUT" | |
| echo "bundles=${#bundles[@]}" >>"$GITHUB_OUTPUT" | |
| - name: Attest newly built binary archives | |
| if: steps.built.outputs.archives != '0' | |
| uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4 | |
| with: | |
| subject-path: new-binaries/tracedecay-* | |
| - name: Attest newly built MCPBs | |
| if: steps.built.outputs.bundles != '0' | |
| uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4 | |
| with: | |
| subject-path: new-mcpbs/*.mcpb | |
| - name: Upload validated artifacts to release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| binaries=(binaries/tracedecay-*) | |
| bundles=(mcpbs/*.mcpb) | |
| assets=("${binaries[@]}" "${bundles[@]}") | |
| ( | |
| cd binaries | |
| sha256sum tracedecay-* | |
| ) > SHA256SUMS | |
| ( | |
| cd mcpbs | |
| sha256sum -- *.mcpb | |
| ) >> SHA256SUMS | |
| assets+=(SHA256SUMS install.sh) | |
| TAG="${{ steps.release.outputs.tag }}" | |
| SOURCE_SHA="${{ needs.validate-release.outputs.source_sha }}" | |
| git -C .release-automation fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}" | |
| test "$(git -C .release-automation rev-list -n 1 "$TAG")" = "$SOURCE_SHA" | |
| gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \ | |
| --jq '.assets[].name' > remote-asset-names | |
| while IFS= read -r remote_name; do | |
| [[ -z "$remote_name" ]] && continue | |
| found=false | |
| for asset in "${assets[@]}"; do | |
| if [[ "$(basename "$asset")" == "$remote_name" ]]; then | |
| found=true | |
| break | |
| fi | |
| done | |
| if [[ "$found" != true ]]; then | |
| echo "::error::unexpected existing release asset: ${remote_name}" | |
| exit 1 | |
| fi | |
| done < remote-asset-names | |
| for asset in "${assets[@]}"; do | |
| name="$(basename "$asset")" | |
| if grep -Fxq "$name" remote-asset-names; then | |
| if [[ "$name" == SHA256SUMS ]]; then | |
| # A recovery run adds targets, so the new manifest may be a | |
| # superset of the published one. Every published line must | |
| # survive unchanged; then the superset replaces it. | |
| mkdir -p retained-metadata | |
| gh release download "$TAG" --repo "${{ github.repository }}" \ | |
| --pattern "$name" --dir retained-metadata | |
| if ! cmp -s "$asset" "retained-metadata/$name"; then | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| grep -Fxq -- "$line" "$asset" || { | |
| echo "::error::published SHA256SUMS line is not in the new manifest: $line" | |
| exit 1 | |
| } | |
| done < "retained-metadata/$name" | |
| gh release upload "$TAG" "$asset" --clobber --repo "${{ github.repository }}" | |
| fi | |
| elif [[ "$name" != tracedecay-* ]]; then | |
| mkdir -p retained-metadata | |
| gh release download "$TAG" --repo "${{ github.repository }}" \ | |
| --pattern "$name" --dir retained-metadata | |
| if ! cmp -s "$asset" "retained-metadata/$name"; then | |
| echo "::error::existing release metadata differs: ${name}" | |
| exit 1 | |
| fi | |
| fi | |
| else | |
| gh release upload "$TAG" "$asset" --repo "${{ github.repository }}" | |
| fi | |
| done | |
| # Verify by name, not by glob: a partial publish may have no .zip | |
| # or no .tar.gz yet, and `gh release download --pattern` fails on | |
| # an empty match. | |
| mkdir -p remote-assets/{binaries,mcpbs} | |
| gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \ | |
| --jq '.assets[].name' > remote-asset-names | |
| while IFS= read -r name; do | |
| case "$name" in | |
| tracedecay-"${TAG}"-*.mcpb) | |
| gh release download "$TAG" --repo "${{ github.repository }}" \ | |
| --pattern "$name" --dir remote-assets/mcpbs ;; | |
| tracedecay-"${TAG}"-*.tar.gz | tracedecay-"${TAG}"-*.zip) | |
| gh release download "$TAG" --repo "${{ github.repository }}" \ | |
| --pattern "$name" --dir remote-assets/binaries ;; | |
| esac | |
| done < remote-asset-names | |
| python3 .release-automation/scripts/check-release-artifacts.py \ | |
| --manifest .release-automation/.github/release-targets.json \ | |
| --tag "$TAG" \ | |
| --binaries remote-assets/binaries \ | |
| --mcpbs remote-assets/mcpbs \ | |
| --allow-missing-targets | |
| .release-automation/scripts/verify-retained-release-assets.sh \ | |
| --tag "$TAG" \ | |
| --repo "${{ github.repository }}" \ | |
| --signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \ | |
| --source-digest "$SOURCE_SHA" \ | |
| --files remote-assets/binaries/* remote-assets/mcpbs/* | |
| - name: Mark stable release as latest | |
| if: needs.validate-release.outputs.prerelease == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: >- | |
| gh release edit "${{ needs.validate-release.outputs.tag }}" | |
| --repo "${{ github.repository }}" | |
| --latest | |
| # Rewrites the MCP registry manifest (server.json) from the actual nonempty | |
| # platform MCPBs so version, URLs, and fileSha256 values describe uploaded | |
| # assets. A missing package is a release failure, never a successful skip. | |
| update-server-json: | |
| name: Update server.json manifest | |
| if: >- | |
| always() && | |
| needs.validate-release.result == 'success' && | |
| needs.publish-assets.result == 'success' && | |
| (needs.build.result == 'success' || needs.build.result == 'skipped') && | |
| needs.validate-release.outputs.prerelease == 'false' | |
| needs: [validate-release, build, publish-assets] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: master | |
| - name: Rewrite server.json from release artifacts | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ needs.validate-release.outputs.tag }}" | |
| VERSION="${{ needs.validate-release.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| mkdir -p assets | |
| gh release download "$TAG" \ | |
| --repo "$REPO" \ | |
| --pattern "tracedecay-${TAG}-*.mcpb" \ | |
| --dir assets | |
| shopt -s nullglob | |
| bundles=(assets/tracedecay-"${TAG}"-*.mcpb) | |
| test "${#bundles[@]}" -gt 0 | |
| for bundle in "${bundles[@]}"; do | |
| platform="${bundle#*tracedecay-"${TAG}"-}" | |
| platform="${platform%.mcpb}" | |
| python3 scripts/build-mcpb.py verify \ | |
| --bundle "$bundle" \ | |
| --version "$VERSION" \ | |
| --platform "$platform" | |
| done | |
| printf '[' > sha-map.json | |
| sep="" | |
| for bundle in "${bundles[@]}"; do | |
| platform="${bundle#*tracedecay-"${TAG}"-}" | |
| platform="${platform%.mcpb}" | |
| sha=$(sha256sum "$bundle" | cut -d' ' -f1) | |
| printf '%s{"platform":"%s","sha":"%s"}' "$sep" "$platform" "$sha" >> sha-map.json | |
| sep="," | |
| done | |
| printf ']' >> sha-map.json | |
| jq --arg v "$VERSION" --arg tag "$TAG" --arg repo "$REPO" --slurpfile map sha-map.json ' | |
| .version = $v | |
| | .packages = ($map[0] | map( | |
| { | |
| "registryType": "mcpb", | |
| "identifier": "https://github.com/\($repo)/releases/download/\($tag)/tracedecay-\($tag)-\(.platform).mcpb", | |
| "version": $v, | |
| "fileSha256": .sha, | |
| "transport": { | |
| "type": "stdio" | |
| } | |
| } | |
| ) | |
| ) | |
| ' server.json > server.json.tmp | |
| mv server.json.tmp server.json | |
| test "$(jq '.packages | length' server.json)" -gt 0 | |
| jq -e ' | |
| all( | |
| .packages[]; | |
| .registryType == "mcpb" | |
| and (.identifier | length > 0) | |
| and (.version | length > 0) | |
| and (.fileSha256 | test("^[0-9a-f]{64}$")) | |
| and .transport.type == "stdio" | |
| ) | |
| ' server.json >/dev/null | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add server.json | |
| git diff --cached --quiet || git commit -m "chore(release): update server.json for ${VERSION}" | |
| git push | |
| # --- TypeScript SDK npm publication --- | |
| # | |
| # Owner decision 2026-08-07: GitHub Releases stay the binary authority, the | |
| # TypeScript SDK publishes to npm on this same stable release trigger, and | |
| # crates.io publication waits until the crate structure is settled. | |
| # | |
| # The build job never holds registry credentials or publish authority. It | |
| # regenerates the client from the canonical SDK executable-binding registry | |
| # and verifies byte-for-byte codegen parity, then runs typecheck, unit | |
| # tests, a package dry-run, and real-daemon installed-package conformance | |
| # against the exact tarball whose digest it records. It also stages and | |
| # digest-verifies the reviewed npm CLI. Only the publish job gets the | |
| # protected environment and the OIDC identity that npm trusted publishing | |
| # exchanges for a short-lived publish credential; it verifies those exact | |
| # bytes without installing executable code and holds no registry token. | |
| build-typescript: | |
| name: Build & test @tracedecay/sdk (unprivileged) | |
| needs: validate-release | |
| if: github.repository == 'ScriptedAlchemy/tracedecay' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.validate-release.outputs.source_sha }} | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable | |
| with: | |
| toolchain: stable | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "22.23.2" | |
| cache: npm | |
| cache-dependency-path: | | |
| dashboard/package-lock.json | |
| sdks/typescript/package-lock.json | |
| - name: Install reviewed npm | |
| run: npm install -g npm@12.0.2 | |
| - name: Install dependencies | |
| working-directory: sdks/typescript | |
| run: npm ci | |
| - name: Verify canonical SDK registry-client parity | |
| run: scripts/check-sdk-codegen.sh | |
| - name: Typecheck | |
| working-directory: sdks/typescript | |
| run: npm run typecheck | |
| - name: Fast unit suite | |
| working-directory: sdks/typescript | |
| run: npm test | |
| - name: Build | |
| working-directory: sdks/typescript | |
| run: npm run build | |
| - name: Verify package dry run | |
| working-directory: sdks/typescript | |
| run: npm pack --dry-run --json --ignore-scripts | |
| - name: Pack the exact publish artifact once | |
| id: pack | |
| working-directory: sdks/typescript | |
| run: | | |
| set -euo pipefail | |
| json="$(npm pack --json --ignore-scripts)" | |
| filename="$(node -e 'console.log(JSON.parse(process.argv[1])[0].filename)' "$json")" | |
| test -f "$filename" | |
| echo "filename=$filename" >> "$GITHUB_OUTPUT" | |
| - name: Stage the reviewed npm publish tool | |
| working-directory: sdks/typescript | |
| run: | | |
| set -euo pipefail | |
| npm pack npm@12.0.2 --ignore-scripts | |
| echo "5dbb86c71d07a1957f2e90734092dd6a58bdcd9ebc2d8d41ca1c6e6a21d364e1 npm-12.0.2.tgz" \ | |
| | sha256sum -c - | |
| - name: Build embedded dashboard for the conformance daemon | |
| working-directory: dashboard | |
| run: | | |
| npm ci | |
| npm run build | |
| # The workflow-global skip flag applies here too, so the freshly built | |
| # bundle's digest must accompany it for the CLI build script. | |
| - name: Export dashboard bundle digest for skip-mode builds | |
| run: | | |
| set -euo pipefail | |
| digest="$(python3 scripts/check-dashboard-bundle.py dashboard/app-dist --print-digest)" | |
| echo "TRACEDECAY_DASHBOARD_BUNDLE_SHA256=${digest}" >> "$GITHUB_ENV" | |
| - name: Build the real production daemon for conformance | |
| run: cargo build -p tracedecay-cli --bin tracedecay | |
| - name: Real-daemon conformance against the exact packed tarball | |
| working-directory: sdks/typescript | |
| run: npx vitest run test/installed-package.test.ts | |
| env: | |
| TRACEDECAY_SDK_TARBALL: ${{ github.workspace }}/sdks/typescript/${{ steps.pack.outputs.filename }} | |
| - name: Record artifact digests | |
| working-directory: sdks/typescript | |
| run: | | |
| sha256sum -- "${{ steps.pack.outputs.filename }}" \ | |
| > "${{ steps.pack.outputs.filename }}.sha256" | |
| sha256sum -- npm-12.0.2.tgz > npm-12.0.2.tgz.sha256 | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: npm-tarball | |
| path: | | |
| sdks/typescript/${{ steps.pack.outputs.filename }} | |
| sdks/typescript/${{ steps.pack.outputs.filename }}.sha256 | |
| sdks/typescript/npm-12.0.2.tgz | |
| sdks/typescript/npm-12.0.2.tgz.sha256 | |
| if-no-files-found: error | |
| retention-days: 7 | |
| publish-typescript: | |
| name: Publish @tracedecay/sdk to npm | |
| needs: [validate-release, build-typescript, verify-release] | |
| if: github.repository == 'ScriptedAlchemy/tracedecay' | |
| runs-on: ubuntu-latest | |
| environment: npm-tracedecay-sdk | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: npm-tarball | |
| path: artifact | |
| - name: Verify the downloaded artifact digests | |
| working-directory: artifact | |
| run: sha256sum -c -- *.sha256 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "22.23.2" | |
| # Prerelease SDK versions mirror the beta release convention: they take | |
| # the beta dist-tag and never take latest. Recovery re-runs are | |
| # byte-verified no-ops; a republished version with different bytes is a | |
| # hard failure that requires an SDK version bump. | |
| # | |
| # Authentication is tokenless npm trusted publishing: the reviewed npm | |
| # CLI (>= 11.5.1) exchanges this job's GitHub OIDC token itself, and npm | |
| # attaches provenance automatically. No token, registry-url, or .npmrc | |
| # auth may exist here. A configured token would shadow the OIDC path. | |
| - name: Publish the exact conformance-tested tarball with reviewed npm | |
| working-directory: artifact | |
| run: | | |
| set -euo pipefail | |
| tarball="$(ls -- tracedecay-sdk-*.tgz)" | |
| version="${tarball#tracedecay-sdk-}" | |
| version="${version%.tgz}" | |
| dist_tag=latest | |
| if [[ "$version" == *-* ]]; then | |
| dist_tag=beta | |
| fi | |
| mkdir npm-cli | |
| tar -xzf npm-12.0.2.tgz -C npm-cli | |
| local_integrity="sha512-$(openssl dgst -sha512 -binary "$tarball" | base64 -w0)" | |
| if published_integrity="$(node npm-cli/package/bin/npm-cli.js view "@tracedecay/sdk@${version}" dist.integrity 2>/dev/null)" \ | |
| && [[ -n "$published_integrity" ]]; then | |
| if [[ "$published_integrity" != "$local_integrity" ]]; then | |
| echo "::error::@tracedecay/sdk@${version} is already published with different bytes; bump the SDK version" | |
| exit 1 | |
| fi | |
| echo "@tracedecay/sdk@${version} already published with identical bytes; nothing to publish" | |
| exit 0 | |
| fi | |
| if ! node npm-cli/package/bin/npm-cli.js \ | |
| publish "$tarball" --access public --tag "$dist_tag"; then | |
| echo "::error title=npm trusted publishing failed::If npm reported ENEEDAUTH or a 404, the npm trusted publisher for @tracedecay/sdk is not configured on npmjs.com (GitHub Actions: org ScriptedAlchemy, repository tracedecay, workflow release.yml, environment npm-tracedecay-sdk), or the package does not exist yet and needs its one-time manual first publish. See docs/RELEASE-AUTOMATION.md." | |
| exit 1 | |
| fi |