# Reproducible, attested Windows bundles (solo miner + desktop wallet). # # On every `v*` tag (or by hand with workflow_dispatch) this workflow: # 1. downloads the node binaries for the SAME version from the moonbite-core # GitHub Release and verifies them three ways before touching them: # SHA256SUMS.txt, Sigstore attestation (gh attestation verify), and # minisign when deploy/moonbite-release.pub is no longer the placeholder; # 2. builds moonbite-miner-windows-x86_64.zip and # moonbite-wallet-windows-x86_64.zip with scripts/release/make-bundle.py # (fixed 1980-01-01 timestamps, sorted members, fixed modes) - TWICE, on # two separate runner VMs - and fails if the two builds differ; # 3. writes SHA256SUMS.txt, attests every asset with # actions/attest-build-provenance (Sigstore / SLSA provenance), signs the # manifest with minisign when the MINISIGN_SECRET_KEY secret exists, and # uploads everything to this repo's GitHub Release for the tag. # # Why: a pseudonymous operator cannot get Authenticode, so SmartScreen will # warn on these files. Users verify instead with scripts/release/verify-release.sh # (or Verify-Release.ps1); docs/REPRODUCIBLE-BUILDS.md explains what each check # proves. Every third-party action is pinned to a full commit SHA. name: Release bundles on: push: tags: - "v*" workflow_dispatch: inputs: core_tag: description: "moonbite-core release tag to take node binaries from (default: this tag)" required: false default: "" release_tag: description: "Tag of THIS repo to attach the bundles to (default: the ref being run)" required: false default: "" env: CORE_REPO: "moonbitecoin/moonbite-core" CORE_ASSET: "moonbite-miner-windows-x86_64.zip" ATTEST_OWNER: "moonbitecoin" TZ: "UTC" LC_ALL: "C" PYTHONHASHSEED: "0" jobs: build: name: "build bundles (pass ${{ matrix.pass }})" runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: pass: [1, 2] steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - name: Resolve versions run: | set -e CORE_TAG="${{ github.event.inputs.core_tag }}" REL_TAG="${{ github.event.inputs.release_tag }}" [ -n "$CORE_TAG" ] || CORE_TAG="${GITHUB_REF_NAME}" [ -n "$REL_TAG" ] || REL_TAG="${GITHUB_REF_NAME}" case "$CORE_TAG" in v*) ;; *) echo "::error::core_tag must look like vX.Y.Z (got '$CORE_TAG')"; exit 1;; esac echo "CORE_TAG=$CORE_TAG" >> "$GITHUB_ENV" echo "REL_TAG=$REL_TAG" >> "$GITHUB_ENV" echo "core: $CORE_REPO@$CORE_TAG -> release: $REL_TAG" # Downloaded assets are untrusted until verified; they go into their own # empty directory and nothing is executed from it. - name: Download node binaries from the core release env: GH_TOKEN: ${{ github.token }} run: | set -e mkdir -p core-dl gh release download "$CORE_TAG" --repo "$CORE_REPO" --dir core-dl \ --pattern "$CORE_ASSET" --pattern "SHA256SUMS.txt" --pattern "SHA256SUMS.txt.minisig" \ || true ls -l core-dl test -f "core-dl/$CORE_ASSET" || { echo "::error::$CORE_ASSET not found on $CORE_REPO release $CORE_TAG"; exit 1; } test -f core-dl/SHA256SUMS.txt || { echo "::error::SHA256SUMS.txt not found on $CORE_REPO release $CORE_TAG"; exit 1; } - name: Verify core assets (sha256 + Sigstore attestation + minisign) env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail cd core-dl echo "== sha256" sha256sum -c SHA256SUMS.txt --ignore-missing --strict echo "== Sigstore attestation (built by $CORE_REPO's release workflow)" gh attestation verify "$CORE_ASSET" --owner "$ATTEST_OWNER" --repo "$CORE_REPO" echo "== minisign" PUB="$GITHUB_WORKSPACE/deploy/moonbite-release.pub" KEYLINE="$(grep -v '^[[:space:]]*#' "$PUB" | grep -v '^[[:space:]]*$' | grep -v '^untrusted comment' | head -1 || true)" if [ -z "$KEYLINE" ] || [ "${KEYLINE#REPLACE_ME}" != "$KEYLINE" ]; then echo "::notice::deploy/moonbite-release.pub is still the placeholder; minisign check skipped (sha256 + attestation enforced)." elif [ ! -f SHA256SUMS.txt.minisig ]; then echo "::error::release key is configured but $CORE_REPO $CORE_TAG has no SHA256SUMS.txt.minisig"; exit 1 else sudo apt-get update -qq && sudo apt-get install -y -qq minisign minisign -Vm SHA256SUMS.txt -P "$KEYLINE" fi - name: Extract node binaries run: | set -e mkdir -p core-x python -I -c 'import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])' "core-dl/$CORE_ASSET" core-x find core-x -type f | sort for f in moonbited.exe moonbite-cli.exe; do P="$(find core-x -type f -name "$f" | head -1)" test -n "$P" || { echo "::error::$f missing from $CORE_ASSET"; exit 1; } cp "$P" "core-x/$f" done - name: Build bundles (deterministic) run: | set -e mkdir -p dist rm -f dist/SHA256SUMS.txt python -I scripts/release/make-bundle.py --sort \ --out dist/moonbite-miner-windows-x86_64.zip --root moonbite-miner --sums dist/SHA256SUMS.txt \ core-x/moonbited.exe core-x/moonbite-cli.exe \ release/miner/mine.ps1 release/miner/Mine-MoonBite.bat release/miner/README.md python -I scripts/release/make-bundle.py --sort \ --out dist/moonbite-wallet-windows-x86_64.zip --root moonbite-wallet --sums dist/SHA256SUMS.txt \ release/miner/moonbite-wallet.exe docs/WALLET.md=README.md # Pin the exact node binaries the miner bundle carries, so the manifest # ties this release to the verified core release. sha256sum core-x/moonbited.exe core-x/moonbite-cli.exe | sed 's# core-x/# *#' >> dist/SHA256SUMS.txt echo "# node binaries from $CORE_REPO release $CORE_TAG" >> dist/SHA256SUMS.txt LC_ALL=C sort -k2 -o dist/SHA256SUMS.txt dist/SHA256SUMS.txt cat dist/SHA256SUMS.txt - name: Upload artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: "bundles-pass${{ matrix.pass }}" path: dist/ if-no-files-found: error reproducibility: name: "Reproducibility check" needs: build runs-on: ubuntu-22.04 steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: bundles-pass1 path: a - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: bundles-pass2 path: b - name: Compare the two independent builds run: | set -e echo "== pass 1"; cat a/SHA256SUMS.txt echo "== pass 2"; cat b/SHA256SUMS.txt (cd a && sha256sum *.zip) > sa.txt (cd b && sha256sum *.zip) > sb.txt if ! diff sa.txt sb.txt; then echo "::error::bundles are NOT reproducible across runners"; exit 1 fi echo "REPRODUCIBLE: both passes produced identical zips" release: name: Publish bundles needs: [build, reproducibility] if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-22.04 permissions: contents: write # upload assets to the Release id-token: write # Sigstore OIDC identity attestations: write # store attestations on this repo env: # `secrets` is not usable in step `if:`; expose presence only. HAVE_MINISIGN_KEY: ${{ secrets.MINISIGN_SECRET_KEY != '' }} steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: bundles-pass1 path: dist - name: Resolve release tag run: | REL_TAG="${{ github.event.inputs.release_tag }}" [ -n "$REL_TAG" ] || REL_TAG="${GITHUB_REF_NAME}" echo "REL_TAG=$REL_TAG" >> "$GITHUB_ENV" - name: Attest build provenance (Sigstore) uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: | dist/*.zip dist/SHA256SUMS.txt - name: Sign SHA256SUMS.txt with minisign (optional) if: env.HAVE_MINISIGN_KEY == 'true' env: MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} run: | set -euo pipefail sudo apt-get update -qq && sudo apt-get install -y -qq minisign umask 077 KEY="$RUNNER_TEMP/minisign.key" printf '%s\n' "$MINISIGN_SECRET_KEY" > "$KEY" TC="MoonBite bundles ${REL_TAG} SHA256SUMS.txt, commit ${GITHUB_SHA}" if [ -n "${MINISIGN_PASSWORD:-}" ]; then printf '%s\n' "$MINISIGN_PASSWORD" | minisign -S -s "$KEY" -t "$TC" -m dist/SHA256SUMS.txt else minisign -S -W -s "$KEY" -t "$TC" -m dist/SHA256SUMS.txt fi rm -f "$KEY" cat dist/SHA256SUMS.txt.minisig - name: Note unsigned manifest if: env.HAVE_MINISIGN_KEY != 'true' run: echo "::notice::MINISIGN_SECRET_KEY not configured; SHA256SUMS.txt published unsigned (Sigstore attestation still applies)." - name: Upload to GitHub Release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: ${{ env.REL_TAG }} files: dist/* generate_release_notes: true body: | MoonBite Windows bundles: solo miner (node + CLI + mine.ps1) and desktop wallet. These files are not Authenticode-signed (see docs/REPRODUCIBLE-BUILDS.md); SmartScreen will warn. Verify instead: - `scripts/release/verify-release.sh ` or `scripts/release/Verify-Release.ps1 ` - `sha256sum -c SHA256SUMS.txt --ignore-missing` - `gh attestation verify --owner moonbitecoin` - `minisign -Vm SHA256SUMS.txt -p deploy/moonbite-release.pub` when a `.minisig` is attached Both zips were built twice on separate runners in this run and matched byte for byte. The node binaries inside come from the verified moonbite-core release of the same version (hashes listed in SHA256SUMS.txt).