name: Rolling bootstrap & tests

# Triggers:
#   - push to master       : keep the -pre binary fresh on every commit
#   - push to ci/** , feature/**, bootstrap/**
#                          : validate-only pre-flight for risky changes — does
#                            the rolling bootstrap still succeed?  These runs do
#                            NOT write the carry-forward cache or publish the
#                            -pre artifact (see the master-only guards on those
#                            steps), so a broken branch cannot affect master
#                            consumers or other developers.  Use a throwaway
#                            branch under one of these prefixes to prove a
#                            change before merging it to master.
#   - workflow_dispatch     : manual run from the Actions UI, against any ref
on:
  push:
    branches:
      - master
      - 'ci/**'
      - 'feature/**'
      - 'bootstrap/**'
  workflow_dispatch:
    inputs:
      force_full:
        description: "Ignore cached -pre binary and bootstrap from the release tag"
        type: boolean
        default: false

# Cancel an in-flight run on master when a newer commit lands — no point
# bootstrapping a commit that is already superseded.
concurrency:
  group: bootstrap-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  # Cold-start anchor: the newest *real* release whose binary is published as a
  # GitHub Release asset. Used only when no cached -pre binary is available.
  # v0.12.0 is the clean rolling-bootstrap anchor: the broken v0.11.0->v0.12.0
  # commit range is BEFORE it, so the replay range (v0.12.0..HEAD) is all
  # native-default + internal-toolchain and needs neither QBE nor gcc.
  STAGE1_TAG: v0.12.0

jobs:
  bootstrap:
    runs-on: ubuntu-latest
    timeout-minutes: 120   # full ~190-commit replay is slow; incremental is fast

    steps:
      # --------------------------------------------------------------------
      # 1. Check out the full history. rolling-bootstrap.sh replays commits,
      #    so a shallow clone is useless — fetch-depth: 0 gets everything.
      # --------------------------------------------------------------------
      - name: Checkout (full history)
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      # --------------------------------------------------------------------
      # 2. Toolchain. From the v0.12.0 anchor onward the bootstrap chain is
      #    native-default with the internal assembler + linker, so neither QBE
      #    nor gcc is needed to replay it — only make (runtime Makefile) and the
      #    distro CRT objects (build-essential) the internal linker reads.
      # --------------------------------------------------------------------
      - name: Install build tools
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y build-essential unzip

      # --------------------------------------------------------------------
      # 3b. Build vendored QBE. The bootstrap chain is native-default and does
      #     NOT need QBE, but the E2E test suite still shells out to it (the
      #     QBE backend arm of AssertRunsOnAll, plus the toolchain-gated
      #     round-trip tests). Without it ~218 tests fail with
      #     "<toolchain-missing>" / "[qbe] compile+run". It is vendored, so a
      #     plain `make` builds the binary the tests' BLAISE_QBE fallback finds.
      # --------------------------------------------------------------------
      - name: Build vendored QBE
        run: make -C vendor/qbe

      # --------------------------------------------------------------------
      # 4. Restore the carry-forward cache: the previous successful master run
      #    stashes its -pre binary + a marker file recording the commit SHA it
      #    was built at. We restore them into releases/<sha>/ so the script can
      #    resume with --from <sha> instead of replaying from the release tag.
      #
      #    Keyed by run id so every run writes a fresh entry; restore-keys does
      #    a prefix match to pull the most recent prior cache. (A fixed key
      #    would never update — caches are immutable once written.)
      # --------------------------------------------------------------------
      - name: Restore bootstrap cache
        id: cache-restore
        if: ${{ !inputs.force_full }}
        uses: actions/cache/restore@v5
        with:
          path: .bootstrap-cache
          key: blaise-bootstrap-${{ github.run_id }}
          restore-keys: |
            blaise-bootstrap-

      # --------------------------------------------------------------------
      # 5. Decide the start point.
      #    - If a cached binary exists AND its marker SHA is an ancestor of
      #      HEAD, stage it into releases/<sha>/ and set FROM=<sha>.
      #    - Otherwise fall back to the published release tag (cold start),
      #      downloading its binary asset from the GitHub Release.
      # --------------------------------------------------------------------
      - name: Resolve start binary
        id: resolve
        run: |
          set -euo pipefail
          FROM=""

          CACHE_BIN=".bootstrap-cache/blaise"
          CACHE_RTL=".bootstrap-cache/blaise_rtl.a"
          CACHE_MARK=".bootstrap-cache/commit.sha"

          if [ -x "$CACHE_BIN" ] && [ -f "$CACHE_MARK" ]; then
            SHA="$(cat "$CACHE_MARK")"
            if git cat-file -e "${SHA}^{commit}" 2>/dev/null \
               && git merge-base --is-ancestor "$SHA" HEAD 2>/dev/null; then
              echo "Cached -pre binary found at commit $SHA — resuming from there."
              mkdir -p "releases/$SHA"
              cp "$CACHE_BIN" "releases/$SHA/blaise"
              cp "$CACHE_RTL" "releases/$SHA/blaise_rtl.a"
              chmod +x "releases/$SHA/blaise"
              FROM="$SHA"
            else
              echo "Cached binary's commit ($SHA) is not an ancestor of HEAD — ignoring it."
            fi
          else
            echo "No cached binary present."
          fi

          if [ -z "$FROM" ]; then
            echo "Cold start from release tag $STAGE1_TAG."
            DEST="releases/$STAGE1_TAG"
            mkdir -p "$DEST"
            # The stage-1 release ships as a versioned tarball, not bare assets.
            curl -fsSL -o /tmp/stage1.tar.gz \
              "https://github.com/graemeg/blaise/releases/download/${STAGE1_TAG}/blaise-${STAGE1_TAG}-linux-x86_64.tar.gz"
            mkdir -p /tmp/stage1
            tar -xzf /tmp/stage1.tar.gz -C /tmp/stage1
            # Normalise to the names rolling-bootstrap.sh expects, wherever the
            # files landed inside the archive.
            BIN="$(find /tmp/stage1 -type f -name blaise | head -1)"
            RTL="$(find /tmp/stage1 -type f -name blaise_rtl.a | head -1)"
            if [ -z "$BIN" ] || [ -z "$RTL" ]; then
              echo "::error::blaise / blaise_rtl.a not found inside the stage-1 tarball."
              find /tmp/stage1 -maxdepth 2 -type f | sed 's/^/  /'
              exit 1
            fi
            cp "$BIN" "$DEST/blaise"
            cp "$RTL" "$DEST/blaise_rtl.a"
            chmod +x "$DEST/blaise"
            FROM="$STAGE1_TAG"
          fi

          echo "from=$FROM" >> "$GITHUB_OUTPUT"

      # --------------------------------------------------------------------
      # 6. Run the rolling bootstrap. Installs the final -pre binary under
      #    releases/v<version>-pre/ (the script computes the dir from
      #    Blaise.pas's Version constant).
      # --------------------------------------------------------------------
      - name: Rolling bootstrap
        run: |
          set -euo pipefail
          ./scripts/rolling-bootstrap.sh --from "${{ steps.resolve.outputs.from }}" --to HEAD

      # --------------------------------------------------------------------
      # 7. Locate the freshly-installed -pre binary for the steps that follow.
      # --------------------------------------------------------------------
      - name: Locate -pre binary
        id: pre
        run: |
          set -euo pipefail
          PRE_DIR="$(ls -d releases/v*-pre 2>/dev/null | sort -V | tail -1)"
          if [ -z "$PRE_DIR" ] || [ ! -x "$PRE_DIR/blaise" ]; then
            echo "No -pre binary produced — bootstrap must have failed." >&2
            exit 1
          fi
          echo "Using $PRE_DIR/blaise"
          echo "dir=$PRE_DIR" >> "$GITHUB_OUTPUT"
          echo "bin=$PWD/$PRE_DIR/blaise" >> "$GITHUB_OUTPUT"

      # --------------------------------------------------------------------
      # 8. Install RTL + build the test runner with the -pre binary, then run
      #    the full test suite. PasBuild is NOT on the runner, so we mirror the
      #    documented manual build path: make install for the RTL, then
      #    pasbuild only if available — otherwise we fall back to the binary's
      #    own test-compile. (Adjust here if PasBuild gets installed below.)
      # --------------------------------------------------------------------
      - name: Build RTL with -pre binary
        run: |
          set -euo pipefail
          BLAISE="${{ steps.pre.outputs.bin }}"
          # Native default backend builds the RTL with no external tools.
          ( cd runtime && make BLAISE="$BLAISE" && make BLAISE="$BLAISE" install )

      - name: Install PasBuild
        run: |
          set -euo pipefail
          curl -fsSL -o /tmp/pasbuild.zip \
            https://github.com/graemeg/PasBuild/releases/download/v1.9.0/pasbuild-1.9.0-x86_64-linux.zip
          unzip -o -q /tmp/pasbuild.zip -d /tmp/pasbuild
          # The binary may sit at the zip root or inside a subdir — find it.
          PB="$(find /tmp/pasbuild -type f -name pasbuild | head -1)"
          if [ -z "$PB" ]; then
            echo "::error::pasbuild binary not found inside the release zip."
            find /tmp/pasbuild -maxdepth 2 -type f | sed 's/^/  /'
            exit 1
          fi
          chmod +x "$PB"
          sudo install -m 0755 "$PB" /usr/local/bin/pasbuild
          pasbuild --version || true

      - name: Build test runner & run tests
        run: |
          set -euo pipefail
          BLAISE="${{ steps.pre.outputs.bin }}"
          pasbuild test-compile -m blaise-compiler --compiler "$BLAISE"
          # Point the CLI-contract and internal-assembler suites at the
          # bootstrapped -pre binary.  Without this they fall back to the
          # /tmp/fp_blaise* fixpoint binaries (which only exist after a local
          # fixpoint.sh run) and silently Ignore ~24 tests in CI.
          # BLAISE_QBE points the E2E suite at the vendored QBE built above
          # (its fallback is vendor/qbe/qbe, but set it explicitly).
          BLAISE_QBE="$PWD/vendor/qbe/qbe" \
          BLAISE_QBE_COMPILER="$BLAISE" compiler/target/TestRunner

      # --------------------------------------------------------------------
      # 9. Save the carry-forward cache for the next run: the -pre binary, its
      #    rtl, and a marker recording the HEAD SHA it was built at.
      #    MASTER ONLY: the carry-forward cache is the resume anchor for master
      #    runs.  A branch run must never overwrite it — otherwise a feature
      #    branch's -pre binary would leak into the next master run and a broken
      #    branch could poison master's bootstrap.  Branch runs are validate-only.
      # --------------------------------------------------------------------
      - name: Stage carry-forward cache
        if: ${{ success() && github.ref == 'refs/heads/master' }}
        run: |
          set -euo pipefail
          rm -rf .bootstrap-cache
          mkdir -p .bootstrap-cache
          cp "${{ steps.pre.outputs.dir }}/blaise"        .bootstrap-cache/blaise
          cp "${{ steps.pre.outputs.dir }}/blaise_rtl.a"  .bootstrap-cache/blaise_rtl.a
          git rev-parse HEAD > .bootstrap-cache/commit.sha
          echo "Marked cache at $(cat .bootstrap-cache/commit.sha)"

      - name: Save bootstrap cache
        if: ${{ success() && github.ref == 'refs/heads/master' }}
        uses: actions/cache/save@v5
        with:
          path: .bootstrap-cache
          key: blaise-bootstrap-${{ github.run_id }}

      # --------------------------------------------------------------------
      # 10. Publish the -pre binary + rtl as downloadable workflow artifacts.
      #     Runs on branches too (the name is keyed by commit SHA, so a branch
      #     artifact never collides with master's and is handy for inspecting a
      #     pre-flight build).  Unlike the cache, an artifact is pull-only — it
      #     is never consumed by a later run, so it cannot affect master.
      # --------------------------------------------------------------------
      - name: Upload -pre binary artifact
        if: success()
        uses: actions/upload-artifact@v7
        with:
          name: blaise-pre-${{ github.sha }}
          path: |
            ${{ steps.pre.outputs.dir }}/blaise
            ${{ steps.pre.outputs.dir }}/blaise_rtl.a
          retention-days: 30
          if-no-files-found: error
