Skip to content
zit Artifact [6a9c6b7493]
Login

Artifact [6a9c6b7493]

Artifact 6a9c6b7493164337532c8da023187b83433abd7002d86d0bef0e1e1448b94684:


name: Zit Release Candidate

# Every artifact this workflow uploads is transit: the candidate is downloaded
# and published to the uv store and a GitHub release the day it is built, and
# nothing reads it from here afterwards. Three days of retention covers a
# publish that slips; thirty kept ~40 MB per release against a 500 MB storage
# quota, and the quota running out failed the next upload of anything at all.

on:
  push:
    tags:
      - "version-*"
  workflow_dispatch:
    inputs:
      version:
        description: "Existing Fossil release version, without version- prefix"
        required: true
        type: string

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  source:
    runs-on: [self-hosted, Linux, X64, linux-x64-zit]
    timeout-minutes: 10
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - name: Resolve release version
        id: version
        env:
          REQUESTED_VERSION: ${{ inputs.version }}
        run: |
          set -euo pipefail
          if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
            case "${GITHUB_REF_NAME}" in
              version-*) version="${GITHUB_REF_NAME#version-}" ;;
              *) echo "release workflow requires a version-* tag" >&2; exit 1 ;;
            esac
          else
            version="${REQUESTED_VERSION}"
          fi
          if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "invalid release version: ${version}" >&2
            exit 1
          fi
          tag="version-${version}"
          echo "version=${version}" >> "${GITHUB_OUTPUT}"
          echo "tag=${tag}" >> "${GITHUB_OUTPUT}"

      - name: Materialize exact Fossil release source
        env:
          VERSION: ${{ steps.version.outputs.version }}
          TAG: ${{ steps.version.outputs.tag }}
        run: |
          set -euo pipefail
          command -v fossil >/dev/null
          fossil version
          version="${VERSION}"
          tag="version-${version}"
          repo="${RUNNER_TEMP}/zit-${version}.fossil"
          checkout="${RUNNER_TEMP}/zit-${version}-checkout"
          rm -rf "${repo}" "${checkout}"
          fossil clone https://fossil.craftdesign.group/zit "${repo}"
          mkdir -p "${checkout}"
          cd "${checkout}"
          fossil open "${repo}" "${tag}"
          fossil info current | tee "${RUNNER_TEMP}/fossil-info.txt"
          grep -F ".version = \"${version}\"" build.zig.zon >/dev/null
          grep -F "zit ${version} (targets Fossil RFC rev " src/main.zig >/dev/null
          fossil tarball --name "zit-${version}" "${tag}" \
            "${RUNNER_TEMP}/zit-${version}-source.tar.gz" -R "${repo}"
          cd "${RUNNER_TEMP}"
          sha256sum "zit-${version}-source.tar.gz" > "zit-${version}-source.tar.gz.sha256"

      - name: Upload exact release source
        uses: actions/upload-artifact@v4
        with:
          name: release-source
          path: |
            ${{ runner.temp }}/zit-${{ steps.version.outputs.version }}-source.tar.gz
            ${{ runner.temp }}/zit-${{ steps.version.outputs.version }}-source.tar.gz.sha256
            ${{ runner.temp }}/fossil-info.txt
          if-no-files-found: error
          retention-days: 3

  build:
    needs: source
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macOS
            arch: ARM64
            runner: mac-mini-zit
            artifact: macos-aarch64
            zig_target: aarch64-macos
            zig_package_target: aarch64-macos
            zig_shasum: b607e9b9234790a008116ae5bdb71c6243b84b9fb42a53a9e70fde41c06c536a
            smoke_repetitions: 3
          - os: Linux
            arch: X64
            runner: linux-x64-zit
            artifact: linux-x86_64
            zig_target: x86_64-linux
            zig_package_target: x86_64-linux
            zig_shasum: 1cbe9df9f27e6b78d14ccbca43b6703a404ef79ef1c463de901d7f088d4e2026
            smoke_repetitions: 10
          - os: Linux
            arch: ARM64
            runner: linux-arm64-zit
            artifact: linux-aarch64
            zig_target: aarch64-linux
            zig_package_target: aarch64-linux
            zig_shasum: 9e8d11661d4ae3bd57702a3832781e23ad151dde5798e16a5ccd503f65234ff8
            smoke_repetitions: 10
    runs-on: [self-hosted, "${{ matrix.os }}", "${{ matrix.arch }}", "${{ matrix.runner }}"]
    env:
      ZIG_VERSION: 0.17.0
      ZIG_TARGET: ${{ matrix.zig_target }}
      ZIG_PACKAGE_TARGET: ${{ matrix.zig_package_target }}
      ZIG_SHASUM: ${{ matrix.zig_shasum }}
      VERSION: ${{ needs.source.outputs.version }}
      ARTIFACT: ${{ matrix.artifact }}
      SMOKE_REPETITIONS: ${{ matrix.smoke_repetitions }}
    steps:
      - name: Download exact release source
        uses: actions/download-artifact@v4
        with:
          name: release-source
          path: ${{ runner.temp }}/release-source

      - name: Verify and extract exact release source
        run: |
          set -euo pipefail
          cd "${RUNNER_TEMP}/release-source"
          if command -v sha256sum >/dev/null 2>&1; then
            sha256sum -c "zit-${VERSION}-source.tar.gz.sha256"
          else
            shasum -a 256 -c "zit-${VERSION}-source.tar.gz.sha256"
          fi
          rm -rf "${RUNNER_TEMP}/zit-${VERSION}"
          tar -xzf "zit-${VERSION}-source.tar.gz" -C "${RUNNER_TEMP}"

      - name: Install Zig 0.17.0
        run: |
          set -euo pipefail
          # Same persistent, digest-verified cache as ci.yml: the runners are
          # self-hosted, and a warm content-addressed Zig cache outside the
          # per-run temp turns this build's unit and end-to-end steps from cold
          # compiles into warm ones. The tarball is refetched whenever the
          # kept copy fails the pinned digest, never trusted for being there.
          cache="${HOME}/.cache/zit-ci"
          # Zig never prunes its compilation cache: every source change leaves
          # a generation behind, and these runners grew 9-14 GB of it in a week.
          # Past the cap it is cleared WHOLE, at job start, when nothing else is
          # building on this single-slot runner. Not pruned by age: Zig does not
          # touch an entry it reuses, so an old entry may still be live, and a
          # manifest naming a deleted output costs more than one cold compile.
          cap_kb=$((8 * 1024 * 1024))
          if [ -d "${cache}/zig-local" ]; then
            used_kb=$(du -sk "${cache}/zig-local" | awk '{print $1}')
            if [ "${used_kb}" -gt "${cap_kb}" ]; then
              echo "zig cache is ${used_kb} KB, over the ${cap_kb} KB cap; clearing it"
              rm -rf "${cache}/zig-local"
            fi
          fi
          mkdir -p "${cache}/zig-local" "${cache}/zig-global" "${cache}/toolchains"
          echo "ZIG_LOCAL_CACHE_DIR=${cache}/zig-local" >> "$GITHUB_ENV"
          echo "ZIG_GLOBAL_CACHE_DIR=${cache}/zig-global" >> "$GITHUB_ENV"
          cd "${RUNNER_TEMP}"
          tarball="zig-${ZIG_PACKAGE_TARGET}-${ZIG_VERSION}.tar.xz"
          kept="${cache}/toolchains/${tarball}"
          digest() {
            if command -v sha256sum >/dev/null 2>&1; then
              sha256sum "$1" | awk '{print $1}'
            else
              shasum -a 256 "$1" | awk '{print $1}'
            fi
          }
          if [ ! -f "${kept}" ] || [ "$(digest "${kept}")" != "${ZIG_SHASUM}" ]; then
            rm -f "${kept}"
            curl -fsSL -o "${kept}.part" "https://ziglang.org/download/${ZIG_VERSION}/${tarball}"
            mv "${kept}.part" "${kept}"
          fi
          actual="$(digest "${kept}")"
          if [ "${actual}" != "${ZIG_SHASUM}" ]; then
            echo "zig tarball digest mismatch: expected ${ZIG_SHASUM}, got ${actual}" >&2
            rm -f "${kept}"
            exit 1
          fi
          rm -rf "zig-${ZIG_PACKAGE_TARGET}-${ZIG_VERSION}"
          tar -xf "${kept}"
          echo "${RUNNER_TEMP}/zig-${ZIG_PACKAGE_TARGET}-${ZIG_VERSION}" >> "${GITHUB_PATH}"

      - name: Format check
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: zig fmt --check build.zig src

      - name: Build portable ReleaseFast binary
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: zig build --release=fast -Dtarget="${ZIG_TARGET}"

      - name: Unit tests
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: zig build test

      - name: End-to-end tests
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: zig build e2e

      - name: Deterministic simulation
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: zig build dst -- --seed 1 --steps 1000

      - name: Seeded fuzz sweep
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: |
          set -euo pipefail
          targets="$(zig build fuzz -- --list)"
          [ -n "${targets}" ] || { echo "no fuzz targets enumerated" >&2; exit 1; }
          for target in ${targets}; do
            zig build fuzz -- "${target}" --seed 1 --rounds 100
          done

      - name: Fossil interoperability smoke
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: |
          set -euo pipefail
          for n in $(seq 1 "${SMOKE_REPETITIONS}"); do
            echo "smoke ${n}/${SMOKE_REPETITIONS}"
            zig build smoke
          done

      - name: Rebuild and package qualified ReleaseFast binary
        working-directory: ${{ runner.temp }}/zit-${{ needs.source.outputs.version }}
        run: |
          set -euo pipefail
          zig build --release=fast -Dtarget="${ZIG_TARGET}"
          ./zig-out/bin/zit version
          dist="${RUNNER_TEMP}/dist-${ARTIFACT}"
          stage="${dist}/zit-${VERSION}"
          archive="zit-${VERSION}-${ARTIFACT}.tar.gz"
          rm -rf "${dist}"
          mkdir -p "${stage}"
          install -m 0755 zig-out/bin/zit "${stage}/zit"
          cp README.md LICENSE "${stage}/"
          cp "docs/RELEASE-NOTES-${VERSION}.md" "${stage}/RELEASE-NOTES.md"
          tar -C "${dist}" -czf "${dist}/${archive}" "zit-${VERSION}"
          if command -v sha256sum >/dev/null 2>&1; then
            binary_sha="$(sha256sum "${stage}/zit" | awk '{print $1}')"
            archive_sha="$(sha256sum "${dist}/${archive}" | awk '{print $1}')"
          else
            binary_sha="$(shasum -a 256 "${stage}/zit" | awk '{print $1}')"
            archive_sha="$(shasum -a 256 "${dist}/${archive}" | awk '{print $1}')"
          fi
          printf '%s  %s\n' "${archive_sha}" "${archive}" > "${dist}/${archive}.sha256"
          python3 - "${dist}/${archive}.metadata.json" "${archive}" "${ARTIFACT}" \
            "${ZIG_TARGET}" "${archive_sha}" "${binary_sha}" <<'PY'
          import json, pathlib, sys
          output, archive, artifact, target, archive_sha, binary_sha = sys.argv[1:]
          data = {
              "artifact": artifact,
              "binary_sha256": binary_sha,
              "bytes": pathlib.Path(output).with_name(archive).stat().st_size,
              "name": archive,
              "qualification": "passed",
              "sha256": archive_sha,
              "target": target,
          }
          pathlib.Path(output).write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
          PY

      - name: Upload qualified binary package
        uses: actions/upload-artifact@v4
        with:
          name: release-${{ matrix.artifact }}
          path: |
            ${{ runner.temp }}/dist-${{ matrix.artifact }}/zit-${{ needs.source.outputs.version }}-${{ matrix.artifact }}.tar.gz
            ${{ runner.temp }}/dist-${{ matrix.artifact }}/zit-${{ needs.source.outputs.version }}-${{ matrix.artifact }}.tar.gz.sha256
            ${{ runner.temp }}/dist-${{ matrix.artifact }}/zit-${{ needs.source.outputs.version }}-${{ matrix.artifact }}.tar.gz.metadata.json
          if-no-files-found: error
          retention-days: 3

  qualify-linux:
    needs: [source, build]
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        include:
          - artifact: linux-x86_64
            emulator: x86_64
          - artifact: linux-aarch64
            emulator: aarch64
    env:
      VERSION: ${{ needs.source.outputs.version }}
      ARTIFACT: ${{ matrix.artifact }}
    steps:
      - name: Download packaged binary
        uses: actions/download-artifact@v4
        with:
          name: release-${{ matrix.artifact }}
          path: dist

      - name: Install deterministic CPU emulators
        run: |
          sudo apt-get update
          sudo apt-get install -y qemu-user

      - name: Verify package digest and low-feature execution
        env:
          EMULATOR: ${{ matrix.emulator }}
        run: |
          set -euo pipefail
          cd dist
          archive="zit-${VERSION}-${ARTIFACT}.tar.gz"
          sha256sum -c "${archive}.sha256"
          mkdir package
          tar -xzf "${archive}" -C package
          binary="${PWD}/package/zit-${VERSION}/zit"
          if [ "${EMULATOR}" = "x86_64" ]; then
            "${binary}" version
            qemu-x86_64 -cpu Nehalem "${binary}" version
          else
            qemu-aarch64 -cpu cortex-a53 "${binary}" version
          fi

  qualify-macos:
    needs: [source, build]
    runs-on: [self-hosted, macOS, ARM64, mac-mini-zit]
    timeout-minutes: 5
    env:
      VERSION: ${{ needs.source.outputs.version }}
    steps:
      - name: Download packaged binary
        uses: actions/download-artifact@v4
        with:
          name: release-macos-aarch64
          path: dist

      - name: Verify packaged macOS binary
        run: |
          set -euo pipefail
          cd dist
          archive="zit-${VERSION}-macos-aarch64.tar.gz"
          shasum -a 256 -c "${archive}.sha256"
          # The macOS runner is a persistent machine: a rerun, or a tag pushed
          # twice, finds the last run's package directory still there.
          rm -rf package
          mkdir package
          tar -xzf "${archive}" -C package
          "${PWD}/package/zit-${VERSION}/zit" version

  bundle:
    needs: [source, qualify-linux, qualify-macos]
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - name: Download release source
        uses: actions/download-artifact@v4
        with:
          name: release-source
          path: candidate

      - name: Download qualified binary packages
        uses: actions/download-artifact@v4
        with:
          pattern: release-*-*
          path: candidate
          merge-multiple: true

      - name: Assemble candidate checksums
        run: |
          set -euo pipefail
          cd candidate
          cat ./*.tar.gz.sha256 | sort -k2 > SHA256SUMS
          sha256sum -c SHA256SUMS

      - name: Upload publication candidate
        uses: actions/upload-artifact@v4
        with:
          name: zit-${{ needs.source.outputs.version }}-publication-candidate
          path: candidate
          if-no-files-found: error
          retention-days: 3