Skip to content

v0.1.0-beta.43

v0.1.0-beta.43 #125

Workflow file for this run

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