Skip to content
zit Artifact [865b7187a5]
Login

Artifact [865b7187a5]

Artifact 865b7187a57c6c5313d36817b305dc696da555cbefc3212616463442c6482d4f:


name: Zit Release Candidate

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: 30

  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: b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489
            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: 70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00
            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: ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17
            smoke_repetitions: 10
    runs-on: [self-hosted, "${{ matrix.os }}", "${{ matrix.arch }}", "${{ matrix.runner }}"]
    env:
      ZIG_VERSION: 0.16.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.16.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"
          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: 30

  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"
          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: 30