# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release # * builds artifacts with dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip # * on success, uploads the artifacts to a GitHub Release # # Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release permissions: "contents": "write" # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that # package (erroring out if it doesn't have the given version or isn't dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all # (dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will # spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' jobs: # Run 'dist plan' (or host) to determine what tasks we need to do plan: runs-on: "ubuntu-22.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} publishing: ${{ !github.event.pull_request }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/dist # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json echo "dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json # Build and packages all the platform-specific things build-local-artifacts: name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) # Wait for WASM extensions so we can patch manifests with SHA256 checksums # before build.rs bakes them into the embedded catalog. needs: - plan - build-wasm-extensions if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') && (needs.build-wasm-extensions.result == 'skipped' || needs.build-wasm-extensions.result == 'success') }} strategy: fail-fast: false # Target platforms/runners are computed by dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner # - dist-args: cli flags to pass to dist # - install-dist: expression to run to install dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} container: ${{ matrix.container && matrix.container.image || null }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: - name: enable windows longpaths run: | git config --global core.longpaths true - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install Rust non-interactively if not already installed if: ${{ matrix.container }} run: | if ! command -v cargo > /dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y echo "$HOME/.cargo/bin" >> $GITHUB_PATH fi - uses: swatinem/rust-cache@v2 with: key: ${{ join(matrix.targets, '-') }} cache-provider: ${{ matrix.cache_provider }} - name: Install dist run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - name: Patch manifests with WASM checksums if: ${{ needs.plan.outputs.publishing == 'true' }} shell: bash env: RELEASE_TAG: ${{ github.ref_name }} run: | CHECKSUMS="target/distrib/checksums.txt" if [ ! -f "$CHECKSUMS" ]; then echo "No checksums.txt found, skipping manifest patching" exit 0 fi while IFS= read -r line; do sha256=$(echo "$line" | awk '{print $1}') filename=$(echo "$line" | awk '{print $2}') # Skip non-WASM entries (e.g. binary tarballs from cargo-dist) case "$filename" in *-wasm32-wasip2.tar.gz) ;; *) continue ;; esac # Parse kind-prefixed filename: "tool-slack-0.2.1-wasm32-wasip2.tar.gz" # → kind=tool, name=slack kind=$(echo "$filename" | cut -d'-' -f1) if [ "$kind" != "tool" ] && [ "$kind" != "channel" ]; then echo "::warning::Skipping '$filename': unrecognized kind prefix '$kind'" continue fi name=$(echo "$filename" | sed "s/^${kind}-//" | sed 's/-[0-9].*-wasm32-wasip2\.tar\.gz$//') url="https://github.com/nearai/ironclaw/releases/download/${RELEASE_TAG}/${filename}" manifest="registry/${kind}s/${name}.json" if [ -f "$manifest" ]; then jq --arg sha "$sha256" --arg url "$url" \ '.artifacts["wasm32-wasip2"].sha256 = $sha | .artifacts["wasm32-wasip2"].url = $url' \ "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" echo "Patched $manifest with sha256=$sha256 url=$url" fi done < "$CHECKSUMS" - name: Install dependencies run: | ${{ matrix.packages_install }} - name: Build artifacts run: | # Actually do builds and make zips and whatnot dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json echo "dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up # to "real" actions without writing to env-vars, and writing to env-vars has # inconsistent syntax between shell and powershell. shell: bash run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-local-${{ join(matrix.targets, '_') }} path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Build and package all the platform-agnostic(ish) things build-global-artifacts: needs: - plan - build-local-artifacts runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - id: cargo-dist shell: bash run: | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json echo "dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-global path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Build WASM extension bundles (tar.gz with .wasm + .capabilities.json) build-wasm-extensions: needs: - plan if: ${{ needs.plan.outputs.publishing == 'true' }} runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install Rust toolchain + wasm target run: | rustup target add wasm32-wasip2 cargo install cargo-component --locked || true - uses: swatinem/rust-cache@v2 with: key: wasm-extensions - name: Build and package WASM extensions shell: bash run: | set -euo pipefail mkdir -p target/wasm-bundles # Process each manifest in registry/tools/ and registry/channels/ for manifest in registry/tools/*.json registry/channels/*.json; do [ -f "$manifest" ] || continue # file_stem: JSON filename without extension (e.g. "slack" for slack.json). file_stem=$(basename "$manifest" .json) # kind: "tool" or "channel" — used as bundle filename prefix to avoid # collisions when a tool and channel share the same file_stem (e.g. slack). kind=$(jq -r '.kind' "$manifest") if [ "$kind" != "tool" ] && [ "$kind" != "channel" ]; then echo "::error::Manifest '$manifest' has invalid or missing .kind ('$kind'); expected 'tool' or 'channel'" exit 1 fi # ext_name: the manifest's .name field (e.g. "slack-tool"). # Used for file names *inside* the archive — the installer extracts by manifest.name. ext_name=$(jq -r '.name' "$manifest") source_dir=$(jq -r '.source.dir' "$manifest") caps_file=$(jq -r '.source.capabilities' "$manifest") crate_name=$(jq -r '.source.crate_name' "$manifest") ext_version=$(jq -r '.version // ""' "$manifest") if [ ! -d "$source_dir" ]; then echo "::warning::Source dir '$source_dir' not found for '$file_stem', skipping" continue fi # Skip rebuild if this exact version was already built and checksummed. # Checks that (1) the manifest already has a sha256, and (2) the version # embedded in the existing artifact URL matches the current manifest version. # This ensures stable checksums: only rebuild when the source version changes. existing_sha=$(jq -r '.artifacts["wasm32-wasip2"].sha256 // ""' "$manifest") existing_url=$(jq -r '.artifacts["wasm32-wasip2"].url // ""' "$manifest") url_version=$(echo "$existing_url" | sed -n 's/.*-\([0-9].*\)-wasm32-wasip2\.tar\.gz$/\1/p') if [[ -n "$ext_version" && "$url_version" == "$ext_version" && -n "$existing_sha" ]]; then echo "=== Skipping $file_stem v$ext_version — already checksummed at $existing_url ===" continue fi echo "=== Building $file_stem ($ext_name) v$ext_version from $source_dir ===" # Build WASM component cargo component build --release --manifest-path "$source_dir/Cargo.toml" || { echo "::warning::Build failed for '$file_stem', skipping" continue } # Find the built WASM file (Cargo uses underscores in artifact names) wasm_artifact="${crate_name//-/_}" wasm_path="" for target_dir in wasm32-wasip2 wasm32-wasip1 wasm32-wasi; do candidate="$source_dir/target/$target_dir/release/${wasm_artifact}.wasm" if [ -f "$candidate" ]; then wasm_path="$candidate" break fi done if [ -z "$wasm_path" ]; then echo "::warning::No WASM output found for '$file_stem', skipping" continue fi # Archive contents use ext_name (manifest .name) — the installer extracts # files by manifest.name, so these must match even when file_stem differs. cp "$wasm_path" "target/wasm-bundles/${ext_name}.wasm" caps_path="$source_dir/$caps_file" if [ -f "$caps_path" ]; then cp "$caps_path" "target/wasm-bundles/${ext_name}.capabilities.json" else echo "::warning::No capabilities file at '$caps_path' for '$file_stem'" fi # Bundle filename uses kind+file_stem to avoid collisions when a tool # and channel share the same name (e.g. tool-slack vs channel-slack). bundle_name="${kind}-${file_stem}-${ext_version}-wasm32-wasip2.tar.gz" bundle="target/wasm-bundles/${bundle_name}" (cd target/wasm-bundles && if [ -f "${ext_name}.capabilities.json" ]; then tar czf "${bundle_name}" "${ext_name}.wasm" "${ext_name}.capabilities.json" else tar czf "${bundle_name}" "${ext_name}.wasm" fi) # Compute SHA256 sha256=$(sha256sum "$bundle" | cut -d' ' -f1) echo "$sha256 ${bundle_name}" >> target/wasm-bundles/checksums.txt # Clean up intermediate files rm -f "target/wasm-bundles/${ext_name}.wasm" "target/wasm-bundles/${ext_name}.capabilities.json" echo " -> $bundle ($sha256)" done echo "=== WASM bundles built ===" ls -la target/wasm-bundles/ - name: "Upload WASM bundles" uses: actions/upload-artifact@v4 with: name: artifacts-wasm-extensions path: | target/wasm-bundles/*.tar.gz target/wasm-bundles/checksums.txt # Determines if we should publish/announce host: needs: - plan - build-local-artifacts - build-global-artifacts - build-wasm-extensions # Only run if we're "publishing", and only if plan, local, global, and wasm didn't fail (skipped is fine) if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') && (needs.build-wasm-extensions.result == 'skipped' || needs.build-wasm-extensions.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - id: host shell: bash run: | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" uses: actions/download-artifact@v4 with: pattern: artifacts-* path: artifacts merge-multiple: true - name: Cleanup run: | # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - name: Create GitHub Release env: PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" RELEASE_COMMIT: "${{ github.sha }}" run: | # Write and read notes from a file to avoid quoting breaking things echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* # Commit patched manifest SHA256 checksums back to main so the repo # stays in sync with the released artifacts. update-registry-checksums: needs: - plan - host - build-wasm-extensions if: ${{ always() && needs.host.result == 'success' && needs.build-wasm-extensions.result == 'success' }} runs-on: "ubuntu-22.04" permissions: contents: write pull-requests: write env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: ref: main - name: Fetch WASM checksums uses: actions/download-artifact@v4 with: name: artifacts-wasm-extensions path: target/wasm-bundles/ - name: Patch manifests with SHA256 and version-pinned URL shell: bash env: RELEASE_TAG: ${{ github.ref_name }} run: | CHECKSUMS="target/wasm-bundles/checksums.txt" if [ ! -f "$CHECKSUMS" ]; then echo "No checksums.txt found" exit 0 fi while IFS= read -r line; do sha256=$(echo "$line" | awk '{print $1}') filename=$(echo "$line" | awk '{print $2}') # Skip non-WASM entries (defensive — this checksums.txt should only have WASM) case "$filename" in *-wasm32-wasip2.tar.gz) ;; *) continue ;; esac # Parse kind-prefixed filename: "tool-slack-0.2.1-wasm32-wasip2.tar.gz" # → kind=tool, name=slack kind=$(echo "$filename" | cut -d'-' -f1) if [ "$kind" != "tool" ] && [ "$kind" != "channel" ]; then echo "::warning::Skipping '$filename': unrecognized kind prefix '$kind'" continue fi name=$(echo "$filename" | sed "s/^${kind}-//" | sed 's/-[0-9].*-wasm32-wasip2\.tar\.gz$//') url="https://github.com/nearai/ironclaw/releases/download/${RELEASE_TAG}/${filename}" manifest="registry/${kind}s/${name}.json" if [ -f "$manifest" ]; then jq --arg sha "$sha256" --arg url "$url" \ '.artifacts["wasm32-wasip2"].sha256 = $sha | .artifacts["wasm32-wasip2"].url = $url' \ "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" echo "Patched $manifest with sha256=$sha256 url=$url" fi done < "$CHECKSUMS" - name: Create PR with updated manifests run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add registry/ if git diff --cached --quiet; then echo "No manifest changes to commit" else BRANCH="chore/update-checksums-$(date +%s)" git checkout -b "$BRANCH" git commit -m "chore: update WASM artifact SHA256 checksums [skip ci]" git push origin "$BRANCH" gh pr create \ --title "chore: update WASM artifact checksums and version-pinned URLs" \ --body "Auto-generated by release CI. Updates SHA256 checksums and version-pinned artifact URLs in registry manifests to match the released WASM artifacts. Only extensions whose version changed since the last release are included." \ --base main \ --head "$BRANCH" fi announce: needs: - plan - host # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive