diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4b00868 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,426 @@ +name: Release + +# Builds the three published packages from a release tag and attaches them, +# with their SHA-256 sums, to that release. The point is provenance: every +# asset comes from a tagged commit through a logged, public build, rather than +# from a developer's machine. +# +# Runs when a v* tag is pushed, and on manual dispatch against an existing tag. +# A manual run produces workflow artifacts only unless it is asked to upload, so +# rebuilding a tag to inspect it cannot overwrite published assets by accident. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: Existing tag to build, for example v1.1.5 + required: true + type: string + upload: + description: Also attach the packages to that tag's release + required: false + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: release-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false + +env: + # sm_120 needs 12.8 or newer. The two platforms differ on purpose, matching + # what the repository already targets: Linux builds against the same 12.8 + # container as ci.yml, while Windows needs 13.2, both because the build script + # looks for it and because 12.8's frontend crashes on the runner image's MSVC + # headers (cudafe++ ACCESS_VIOLATION). + LINUX_CUDA: 12.8.0 + WINDOWS_CUDA: 13.2.0 + ARCHES: sm_86 sm_89 sm_120 + +jobs: + resolve: + name: Resolve and validate release commit + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.resolve.outputs.tag }} + version: ${{ steps.resolve.outputs.version }} + upload: ${{ steps.resolve.outputs.upload }} + sha: ${{ steps.resolve.outputs.sha }} + steps: + - id: resolve + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + EVENT_TAG: ${{ github.ref_name }} + REQUESTED_TAG: ${{ inputs.tag }} + REQUESTED_UPLOAD: ${{ inputs.upload }} + run: | + set -euo pipefail + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + tag="$REQUESTED_TAG" + upload="$REQUESTED_UPLOAD" + else + tag="$EVENT_TAG" + upload=true + fi + + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Refusing to build '$tag': expected a stable tag such as v1.2.3." + exit 1 + fi + if [[ "$upload" != "true" && "$upload" != "false" ]]; then + echo "::error::Invalid upload setting '$upload'." + exit 1 + fi + + if ! object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.type' 2>/dev/null)"; then + echo "::error::$GITHUB_REPOSITORY has no tag $tag. Push it first, for example: git push origin $tag" + echo "Tags this repository does have:" + gh api "repos/$GITHUB_REPOSITORY/tags" --jq '.[].name' | head -20 + exit 1 + fi + object_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.sha')" + while [[ "$object_type" == "tag" ]]; do + object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object.type')" + object_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object.sha')" + done + if [[ "$object_type" != "commit" || ! "$object_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::$tag does not resolve to a commit." + exit 1 + fi + + version="${tag#v}" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "upload=$upload" >> "$GITHUB_OUTPUT" + echo "sha=$object_sha" >> "$GITHUB_OUTPUT" + echo "Building $tag at $object_sha (version $version); upload to release: $upload" >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + ref: ${{ steps.resolve.outputs.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Validate release ancestry and version metadata + env: + EXPECTED_SHA: ${{ steps.resolve.outputs.sha }} + VERSION: ${{ steps.resolve.outputs.version }} + run: | + set -euo pipefail + git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + if ! git merge-base --is-ancestor "$EXPECTED_SHA" refs/remotes/origin/main; then + echo "::error::$EXPECTED_SHA is not reachable from origin/main." + exit 1 + fi + + declared="$(sed -n 's/^#define TARI_MINER_VERSION "\([^"]*\)"/\1/p' version.h)" + if [[ "$declared" != "$VERSION" ]]; then + echo "::error::version.h reports '$declared', but the tag reports '$VERSION'." + exit 1 + fi + if [[ "$(sed -n 's/^CUSTOM_VERSION=//p' hiveos/h-manifest.conf)" != "$VERSION" ]]; then + echo "::error::hiveos/h-manifest.conf does not report version $VERSION." + exit 1 + fi + for readme in packaging/README.txt packaging/README-LINUX.txt packaging/README-HIVEOS.txt; do + grep -Fq "TARI.Miner C29 v$VERSION" "$readme" || { + echo "::error::$readme does not report version $VERSION." + exit 1 + } + done + grep -Fq "TARI.Miner-v$VERSION-windows.zip" README.md + grep -Fq "TARI.Miner-v$VERSION-linux.tar.gz" README.md + grep -Fq "tari-miner-hiveos-$VERSION.tar.gz" README.md + grep -Fq "/releases/download/v$VERSION/tari-miner-hiveos-$VERSION.tar.gz" packaging/README-HIVEOS.txt + for metadata in README.md packaging/README.txt packaging/README-LINUX.txt packaging/README-HIVEOS.txt; do + stale="$(grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+' "$metadata" | sort -u | grep -Fvx "v$VERSION" || true)" + if [[ -n "$stale" ]]; then + echo "::error::$metadata contains release metadata for a different version: $stale" + exit 1 + fi + done + + linux: + name: Build Linux backends + needs: resolve + runs-on: ubuntu-latest + container: + image: nvidia/cuda:12.8.0-devel-ubuntu22.04@sha256:54f18e2a8e1b3d03f77b9a6dc905533da46ac93a5513f10e8ba8e560db9fa5ab + defaults: + run: + # Inside a container the default shell is dash, which has no + # `set -o pipefail`. Every run step here expects bash. + shell: bash + steps: + - name: Install checkout prerequisites + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + --no-install-recommends ca-certificates git + rm -rf /var/lib/apt/lists/* + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + ref: ${{ needs.resolve.outputs.sha }} + persist-credentials: false + + - name: Build the shipped solver and pool miner for every architecture + run: | + set -euo pipefail + for arch in $ARCHES; do + ./build_solver.sh "$arch" + ./build_pool_miner.sh "$arch" + done + + # Cheap gate: the consensus wrapper is CPU-only, so a broken key + # derivation, packing, or difficulty change stops the release here rather + # than reaching a miner. + - name: Run the consensus self-test + run: | + g++ -O2 -std=c++17 -I. tari_c29.cpp tari_c29_selftest.cpp -o tari_c29_selftest + ./tari_c29_selftest + + - name: Check every packaged binary is present + run: | + set -euo pipefail + for arch in $ARCHES; do + for program in tari_c29_pool_miner tari_c29_solver; do + test -f "bin/${program}_${arch}" || { + echo "::error::Missing bin/${program}_${arch}" + exit 1 + } + done + done + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: linux-binaries + path: | + bin/tari_c29_pool_miner_sm_* + bin/tari_c29_solver_sm_* + if-no-files-found: error + retention-days: 7 + + windows: + name: Build Windows backends + needs: resolve + runs-on: windows-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + ref: ${{ needs.resolve.outputs.sha }} + persist-credentials: false + + # Pinned to a commit: this workflow writes to releases, so a moving tag on + # a third-party action would be a way into the published binaries. + - name: Install CUDA + uses: Jimver/cuda-toolkit@3d45d157f327c09c04b50ee6ccdea2d9d017ec76 # v0.2.35 + with: + cuda: ${{ env.WINDOWS_CUDA }} + # No sub-package list, which installs the whole toolkit. Naming a + # subset is faster but brittle: nvcc, cudart and + # visual_studio_integration look sufficient and are not, because 13.2 + # ships crt/host_config.h - included by cuda_runtime.h - outside them. + # A release build is rare enough that the extra install time is worth + # not having to track NVIDIA's component layout per version. + method: network + + - name: Build the shipped solver and pool miner for every architecture + shell: powershell + run: | + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $install = & $vswhere -latest -products * ` + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -property installationPath + if (-not $install) { + throw 'Visual Studio C++ tools were not found.' + } + $steps = @("call `"$install\VC\Auxiliary\Build\vcvars64.bat`" >nul") + foreach ($arch in $env:ARCHES.Split(' ')) { + $steps += "call build_solver.bat $arch" + $steps += "call build_pool_miner.bat $arch" + } + & cmd.exe /d /s /c ($steps -join ' && ') + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + - name: Check every packaged binary is present + shell: bash + run: | + set -euo pipefail + for arch in $ARCHES; do + for program in tari_c29_pool_miner tari_c29_solver; do + test -f "bin/${program}_${arch}.exe" || { + echo "::error::Missing bin/${program}_${arch}.exe" + exit 1 + } + done + done + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: windows-binaries + path: bin/*.exe + if-no-files-found: error + retention-days: 7 + + package: + name: Package and publish + needs: [resolve, linux, windows] + runs-on: ubuntu-latest + permissions: + # Only this job writes; the build jobs stay read-only. + contents: write # Upload packages to the draft release for this tag. + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + ref: ${{ needs.resolve.outputs.sha }} + fetch-depth: 0 + persist-credentials: false + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: linux-binaries + path: linux-bin + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: windows-binaries + path: windows-bin + + # Layout, file list, and permissions mirror package_release.ps1, which + # needs Windows and WSL and so cannot run here. + - name: Stage the three packages + env: + VERSION: ${{ needs.resolve.outputs.version }} + run: | + set -euo pipefail + mkdir -p dist/release + windows="dist/staging/TARI.Miner-v$VERSION-windows" + linux="dist/staging/TARI.Miner-v$VERSION-linux" + hive="dist/staging/tari-miner-hiveos" + mkdir -p "$windows/bin" "$linux/bin" "$hive/bin" + + install -m 644 packaging/README.txt "$windows/README.txt" + install -m 644 start-c29.bat start-c29.ps1 LICENSE THIRD_PARTY_NOTICES.md "$windows/" + install -m 644 windows-bin/*.exe "$windows/bin/" + + install -m 644 packaging/README-LINUX.txt "$linux/README.txt" + install -m 644 LICENSE THIRD_PARTY_NOTICES.md "$linux/" + install -m 755 start-c29.sh "$linux/" + install -m 755 linux-bin/* "$linux/bin/" + + install -m 644 packaging/README-HIVEOS.txt "$hive/README.txt" + install -m 644 LICENSE THIRD_PARTY_NOTICES.md hiveos/h-manifest.conf "$hive/" + install -m 755 start-c29.sh hiveos/h-config.sh hiveos/h-run.sh hiveos/h-stats.sh "$hive/" + install -m 755 linux-bin/* "$hive/bin/" + + for staged in "$windows" "$linux" "$hive"; do + count="$(find "$staged/bin" -type f | wc -l)" + if [ "$count" -ne 6 ]; then + echo "::error::$staged/bin holds $count binaries, expected 6" + exit 1 + fi + done + + # Package byte layout and timestamps are normalized. The binaries + # themselves are whatever nvcc emitted, so compilation is not claimed to + # be reproducible. + - name: Build the archives + env: + VERSION: ${{ needs.resolve.outputs.version }} + run: | + set -euo pipefail + epoch="$(git log -1 --format=%ct)" + release="$PWD/dist/release" + cd dist/staging + + TZ=UTC find "TARI.Miner-v$VERSION-windows" -exec touch -d "@$epoch" {} + + ( cd "TARI.Miner-v$VERSION-windows" && \ + find . -type f -printf '%P\n' | LC_ALL=C sort | \ + zip -qX "$release/TARI.Miner-v$VERSION-windows.zip" -@ ) + + tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@$epoch" \ + -cf - -C "TARI.Miner-v$VERSION-linux" . \ + | gzip -n > "$release/TARI.Miner-v$VERSION-linux.tar.gz" + + tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@$epoch" \ + -cf - tari-miner-hiveos \ + | gzip -n > "$release/tari-miner-hiveos-$VERSION.tar.gz" + + - name: Write the checksums + working-directory: dist/release + env: + VERSION: ${{ needs.resolve.outputs.version }} + run: | + set -euo pipefail + sha256sum \ + "TARI.Miner-v$VERSION-windows.zip" \ + "TARI.Miner-v$VERSION-linux.tar.gz" \ + "tari-miner-hiveos-$VERSION.tar.gz" > SHA256SUMS.txt + sha256sum -c SHA256SUMS.txt + { + echo '## Release packages' + echo + echo '```' + cat SHA256SUMS.txt + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: release-packages + path: dist/release/* + if-no-files-found: error + + - name: Attach the packages to the release + if: needs.resolve.outputs.upload == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.resolve.outputs.tag }} + VERSION: ${{ needs.resolve.outputs.version }} + EXPECTED_SHA: ${{ needs.resolve.outputs.sha }} + run: | + set -euo pipefail + object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --jq '.object.type')" + object_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --jq '.object.sha')" + while [[ "$object_type" == "tag" ]]; do + object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object.type')" + object_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object.sha')" + done + if [[ "$object_type" != "commit" || "$object_sha" != "$EXPECTED_SHA" ]]; then + echo "::error::$TAG moved during the build (expected $EXPECTED_SHA, found $object_sha)." + exit 1 + fi + + # A tag push usually arrives before the release exists. Create it as a + # draft rather than publishing: the packages are then attached and + # reviewable, and announcing them stays a deliberate act. + if release_json="$(gh release view "$TAG" --json isDraft,url 2>/dev/null)"; then + if [[ "$(jq -r '.isDraft' <<<"$release_json")" != "true" ]]; then + echo "::error::Refusing to overwrite assets on the published $TAG release." + exit 1 + fi + echo "Replacing assets on the existing draft $TAG release." + else + echo "No $TAG release yet; creating a draft." + gh release create "$TAG" \ + --draft \ + --verify-tag \ + --title "TARI.Miner C29 v$VERSION" \ + --generate-notes + fi + gh release upload "$TAG" \ + "dist/release/TARI.Miner-v$VERSION-windows.zip" \ + "dist/release/TARI.Miner-v$VERSION-linux.tar.gz" \ + "dist/release/tari-miner-hiveos-$VERSION.tar.gz" \ + "dist/release/SHA256SUMS.txt" \ + --clobber