name: Install & Update E2E

# Can a user on a released version get to this commit?
#
# For each release we sample, a leg installs that release through the real
# `curl | install.sh` one-liner (uv, a managed Python, Node, the venv) inside
# scripts/dev-sandbox.sh, then applies one update route and requires the
# checkout to land on this commit with a working `hermes`.
#
# The starting versions are chosen at runtime from the repo's release tags
# (scripts/sandbox/pick-release-tags.sh): newest, oldest, and a spread between.
# A hardcoded list would stop covering the newest release the day after it
# ships, and would pin an "oldest" that nobody still runs.
#
# Triggers:
#   * every 12 hours, so upstream drift (a new uv, a Node bump, a PyPI change)
#     surfaces on a schedule rather than in someone's review cycle;
#   * when a release tag is created -- the moment the set of versions users can
#     update FROM changes, and the moment a broken updater would strand them;
#   * manually, where you can pick the route and how many releases to sample.
#
# Deliberately NOT on pull_request: a leg takes ~11 minutes of real toolchain
# installation, and the matrix multiplies that. Updating is release-shaped work,
# so it is gated on releases and the clock instead.

on:
  workflow_dispatch:
    inputs:
      route:
        description: 'Which update route to exercise.'
        required: false
        type: choice
        default: both
        options: [both, update, installer]
      tag-count:
        description: 'How many release tags to sample (newest, oldest, and a spread between).'
        required: false
        type: string
        default: '5'
  schedule:
    # Every 12 hours, off the hour to avoid the top-of-hour runner crunch.
    - cron: '20 7,19 * * *'
  push:
    tags:
      # Release tags only: the repo also carries backup/* and one-off tags.
      - 'v[0-9]+.[0-9]+.[0-9]+'
      - 'v[0-9]+.[0-9]+.[0-9]+.[0-9]+'

permissions:
  contents: read

concurrency:
  group: install-e2e-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # Which released versions do we test updating FROM? Resolved once and shared
  # by both route matrices, so the two routes cover the same set.
  pick-releases:
    name: Pick release tags
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      tags: ${{ steps.pick.outputs.tags }}
    steps:
      # This job only reads tag names and runs one script, so take the cheap
      # checkout: no blobs (filter), no other files (sparse), but DO fetch tags
      # -- they are the whole input, and the default shallow checkout has none.
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          filter: blob:none
          fetch-tags: true
          sparse-checkout: scripts/sandbox/pick-release-tags.sh
          sparse-checkout-cone-mode: false
      - id: pick
        run: |
          set -euo pipefail
          tags="$(scripts/sandbox/pick-release-tags.sh --count '${{ inputs.tag-count || 5 }}')"
          echo "Testing updates from: $tags"
          echo "tags=$tags" >> "$GITHUB_OUTPUT"

  # `hermes update` -- the route most users take.
  update:
    if: github.event_name != 'workflow_dispatch' || inputs.route != 'installer'
    needs: pick-releases
    strategy:
      # One release breaking is worth knowing about even if another already
      # failed, so let every leg report.
      fail-fast: false
      matrix:
        install-ref: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
    uses: ./.github/workflows/install-e2e-run.yml
    with:
      route: update
      install-ref: ${{ matrix.install-ref }}

  # Re-running the curl one-liner over an existing checkout: autostash + pull
  # rather than the updater's own git handling.
  installer:
    if: github.event_name != 'workflow_dispatch' || inputs.route != 'update'
    needs: pick-releases
    strategy:
      fail-fast: false
      max-parallel: 3
      matrix:
        install-ref: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
    uses: ./.github/workflows/install-e2e-run.yml
    with:
      route: installer
      install-ref: ${{ matrix.install-ref }}
