From 1627957558126401c4b61cfd41cdb2f665bcd1a7 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Thu, 30 Jul 2026 14:43:40 +0200 Subject: [PATCH 1/3] create ci build action to build releases on push of "v*" tag or manual --- .github/workflows/release.yml | 346 ++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ececa62 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,346 @@ +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 tag and version + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.resolve.outputs.tag }} + version: ${{ steps.resolve.outputs.version }} + upload: ${{ steps.resolve.outputs.upload }} + # Republished as an output because a job's `container.image` cannot read + # the workflow `env`, and both runners must use the same toolkit. + cuda: ${{ steps.resolve.outputs.cuda }} + steps: + # On a tag push this is the pushed tag; on dispatch it is the tag asked + # for, which is a required input. The version is the tag without its v, + # so package names always match the release they are attached to. + - id: resolve + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tag='${{ inputs.tag || github.ref_name }}' + case "$tag" in + v*) ;; + *) + echo "::error::Refusing to build '$tag': release tags start with v." + exit 1 + ;; + esac + # Checking here turns an unresolvable ref - most often a tag that + # exists upstream but was never pushed to this fork - into one clear + # message, instead of a bare git failure inside the checkout action. + if ! gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" >/dev/null 2>&1; 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 + version="${tag#v}" + upload='${{ github.event_name == 'push' || inputs.upload }}' + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "upload=$upload" >> "$GITHUB_OUTPUT" + echo "cuda=$LINUX_CUDA" >> "$GITHUB_OUTPUT" + echo "Building $tag (version $version); upload to release: $upload" >> "$GITHUB_STEP_SUMMARY" + + linux: + name: Build Linux backends + needs: resolve + runs-on: ubuntu-latest + container: + image: nvidia/cuda:${{ needs.resolve.outputs.cuda }}-devel-ubuntu22.04 + 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@v4 + with: + ref: refs/tags/${{ needs.resolve.outputs.tag }} + + - 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@v4 + 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@v4 + with: + ref: refs/tags/${{ needs.resolve.outputs.tag }} + + # 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@v4 + 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 + steps: + - uses: actions/checkout@v4 + with: + ref: refs/tags/${{ needs.resolve.outputs.tag }} + + - uses: actions/download-artifact@v4 + with: + name: linux-binaries + path: linux-bin + + - uses: actions/download-artifact@v4 + 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 + + # Archives are built deterministically - sorted entries, fixed ownership, + # the tag's own commit date as the timestamp, and no gzip header time - so + # that two runs over identical binaries produce identical archives. The + # binaries themselves are whatever nvcc emitted; this makes the packaging + # verifiable, not the compilation. + - 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 + + ( cd "TARI.Miner-v$VERSION-windows" && \ + zip -rqX "$release/TARI.Miner-v$VERSION-windows.zip" \ + README.txt start-c29.bat start-c29.ps1 LICENSE THIRD_PARTY_NOTICES.md bin ) + + 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@v4 + 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 }} + run: | + set -euo pipefail + # 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 gh release view "$TAG" >/dev/null 2>&1; then + echo "Uploading to the existing $TAG release." + else + echo "No $TAG release yet; creating a draft." + gh release create "$TAG" \ + --draft \ + --verify-tag \ + --title "TARI.Miner C29 $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 From 285362c8191e805394f568c08cd31f3be45d9374 Mon Sep 17 00:00:00 2001 From: JustAResearcher <030manager@gmail.com> Date: Thu, 30 Jul 2026 16:32:44 -0500 Subject: [PATCH 2/3] fix: harden tagged release workflow --- .github/workflows/release.yml | 168 +++++++++++++++++++++++++--------- 1 file changed, 124 insertions(+), 44 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ececa62..237deb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,54 +44,115 @@ env: jobs: resolve: - name: Resolve tag and version + 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 }} - # Republished as an output because a job's `container.image` cannot read - # the workflow `env`, and both runners must use the same toolkit. - cuda: ${{ steps.resolve.outputs.cuda }} + sha: ${{ steps.resolve.outputs.sha }} steps: - # On a tag push this is the pushed tag; on dispatch it is the tag asked - # for, which is a required input. The version is the tag without its v, - # so package names always match the release they are attached to. - 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: | - tag='${{ inputs.tag || github.ref_name }}' - case "$tag" in - v*) ;; - *) - echo "::error::Refusing to build '$tag': release tags start with v." - exit 1 - ;; - esac - # Checking here turns an unresolvable ref - most often a tag that - # exists upstream but was never pushed to this fork - into one clear - # message, instead of a bare git failure inside the checkout action. - if ! gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" >/dev/null 2>&1; then + 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}" - upload='${{ github.event_name == 'push' || inputs.upload }}' echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT" echo "upload=$upload" >> "$GITHUB_OUTPUT" - echo "cuda=$LINUX_CUDA" >> "$GITHUB_OUTPUT" - echo "Building $tag (version $version); upload to release: $upload" >> "$GITHUB_STEP_SUMMARY" + 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:${{ needs.resolve.outputs.cuda }}-devel-ubuntu22.04 + image: nvidia/cuda:12.8.0-devel-ubuntu22.04@sha256:54f18e2a8e1b3d03f77b9a6dc905533da46ac93a5513f10e8ba8e560db9fa5ab defaults: run: # Inside a container the default shell is dash, which has no @@ -105,9 +166,10 @@ jobs: --no-install-recommends ca-certificates git rm -rf /var/lib/apt/lists/* - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: - ref: refs/tags/${{ needs.resolve.outputs.tag }} + ref: ${{ needs.resolve.outputs.sha }} + persist-credentials: false - name: Build the shipped solver and pool miner for every architecture run: | @@ -137,7 +199,7 @@ jobs: done done - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: linux-binaries path: | @@ -151,9 +213,10 @@ jobs: needs: resolve runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: - ref: refs/tags/${{ needs.resolve.outputs.tag }} + 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. @@ -202,7 +265,7 @@ jobs: done done - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: windows-binaries path: bin/*.exe @@ -217,16 +280,18 @@ jobs: # Only this job writes; the build jobs stay read-only. contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: - ref: refs/tags/${{ needs.resolve.outputs.tag }} + ref: ${{ needs.resolve.outputs.sha }} + fetch-depth: 0 + persist-credentials: false - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: linux-binaries path: linux-bin - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: windows-binaries path: windows-bin @@ -266,11 +331,9 @@ jobs: fi done - # Archives are built deterministically - sorted entries, fixed ownership, - # the tag's own commit date as the timestamp, and no gzip header time - so - # that two runs over identical binaries produce identical archives. The - # binaries themselves are whatever nvcc emitted; this makes the packaging - # verifiable, not the compilation. + # 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 }} @@ -280,9 +343,10 @@ jobs: 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" && \ - zip -rqX "$release/TARI.Miner-v$VERSION-windows.zip" \ - README.txt start-c29.bat start-c29.ps1 LICENSE THIRD_PARTY_NOTICES.md bin ) + 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" . \ @@ -311,7 +375,7 @@ jobs: echo '```' } >> "$GITHUB_STEP_SUMMARY" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: release-packages path: dist/release/* @@ -323,19 +387,35 @@ jobs: 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 gh release view "$TAG" >/dev/null 2>&1; then - echo "Uploading to the existing $TAG release." + 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 $VERSION" \ + --title "TARI.Miner C29 v$VERSION" \ --generate-notes fi gh release upload "$TAG" \ From 8dbc07c6be5bfed1c47f59b48df88ec693db0409 Mon Sep 17 00:00:00 2001 From: JustAResearcher <030manager@gmail.com> Date: Thu, 30 Jul 2026 16:33:26 -0500 Subject: [PATCH 3/3] docs: explain release write permission --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 237deb5..4b00868 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -278,7 +278,7 @@ jobs: runs-on: ubuntu-latest permissions: # Only this job writes; the build jobs stay read-only. - contents: write + contents: write # Upload packages to the draft release for this tag. steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: