# Release workflow - orchestrates version validation, testing, and artifact publishing
#
# Triggered on version tags (v*.*.*) or manual dispatch.
# Runs full test suite before publishing to PyPI and Docker registries.

name: Release

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"
      - "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
      - "v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+"
      - "v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+"
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag (e.g., v1.0.0)"
        required: true
        type: string
      skip_tests:
        description: "Skip test suite (use with caution)"
        required: false
        type: boolean
        default: false

env:
  PYTHON_VERSION: "3.11"

# Default the whole workflow to read-only. Without this every job inherits the
# repository default, which is write on nearly everything -- Contents, Packages,
# Actions, Deployments, SecurityEvents. That is broadest exactly where it should
# be narrowest: `smoke-wheel` and `smoke-published` DOWNLOAD AND EXECUTE the
# freshly built artifact (they start a gateway, spawn subprocess backends and
# drive a real MCP server), and `test` runs the suite. Those four jobs only ever
# need to read the repo.
#
# The three jobs that genuinely write already declare their own `permissions:`,
# and a job-level block REPLACES this default rather than merging with it, so
# they are unaffected: publish-pypi (id-token + attestations), publish-docker
# (packages) and create-release (contents: write).
permissions:
  contents: read

jobs:
  # Validate release tag matches pyproject.toml version
  validate:
    name: Validate Release
    runs-on: ubuntu-latest
    timeout-minutes: 10
    outputs:
      version: ${{ steps.extract.outputs.version }}
      is_prerelease: ${{ steps.extract.outputs.is_prerelease }}

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Extract version from tag
        id: extract
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            TAG="${{ github.event.inputs.tag }}"
          else
            TAG="${GITHUB_REF#refs/tags/}"
          fi

          # Strip 'v' prefix for version comparison
          VERSION="${TAG#v}"
          echo "version=${VERSION}" >> $GITHUB_OUTPUT

          # Check if prerelease
          if [[ "$VERSION" =~ (alpha|beta|rc) ]]; then
            echo "is_prerelease=true" >> $GITHUB_OUTPUT
          else
            echo "is_prerelease=false" >> $GITHUB_OUTPUT
          fi

          echo "📦 Release version: ${VERSION}"

      - name: Validate version in pyproject.toml
        run: |
          PYPROJECT_VERSION=$(grep -E '^version = "' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
          TAG_VERSION="${{ steps.extract.outputs.version }}"

          # For prereleases, strip suffix for comparison (e.g., 1.0.0-rc.1 -> 1.0.0)
          BASE_TAG_VERSION=$(echo "$TAG_VERSION" | sed 's/-.*//')

          # Normalize a semver-style prerelease tag suffix to its PEP 440 form so a
          # PEP 440 pyproject version (e.g. 2.0.0a1) validates against its release
          # tag (v2.0.0-alpha.1): -alpha.N -> aN, -beta.N -> bN, -rc.N -> rcN.
          NORM_TAG_VERSION=$(echo "$TAG_VERSION" | sed -E 's/-alpha\.?([0-9]+)/a\1/; s/-beta\.?([0-9]+)/b\1/; s/-rc\.?([0-9]+)/rc\1/')

          if [ "$PYPROJECT_VERSION" != "$BASE_TAG_VERSION" ] && [ "$PYPROJECT_VERSION" != "$TAG_VERSION" ] && [ "$PYPROJECT_VERSION" != "$NORM_TAG_VERSION" ]; then
            echo "❌ Version mismatch!"
            echo "   pyproject.toml: ${PYPROJECT_VERSION}"
            echo "   Git tag: ${TAG_VERSION}"
            echo ""
            echo "Please update pyproject.toml version before tagging."
            exit 1
          fi

          echo "✅ Version validated: ${TAG_VERSION}"

  # Run full test suite before release
  test:
    name: Test Suite
    needs: validate
    if: ${{ github.event.inputs.skip_tests != 'true' }}
    runs-on: ubuntu-latest
    # Backstop for a hung test/step: the whole suite runs in a few minutes, so a
    # job running long means something wedged -- fail fast instead of burning to
    # the 6h GitHub ceiling (which cancelled a prior release on 3.12).
    timeout-minutes: 30
    defaults:
      run:
        working-directory: .
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.11", "3.12", "3.13", "3.14"]

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"

      - name: Install dependencies
        # The same install as ci-core's `test` job: `.[dev]` carries only the
        # OTel API (via `mcp`), and under CI the tracing contract tests fail
        # without the SDK from the `opentelemetry` extra (tests/conftest.py).
        # Installing `.[dev]` alone here failed every one of them and blocked
        # the v2.19.0 publish.
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev,opentelemetry]"

      # No image build step and no MCP_CONTAINER_* env: the tests that launched
      # real containers (tests/feature, tests/integration/containers) were gated
      # behind `--run-containers` / `--run-slow`, which nothing ever passed, so
      # they skipped on every run while three docker builds paid for them.
      - name: Run tests
        run: |
          python -m pytest tests/ -v --tb=short --color=yes

  # Gate D of the release matrix (#550): smoke the ARTIFACT, not the tree.
  #
  # Every other job here tests the working copy, which is exactly the blind spot
  # that shipped #561 -- the code was fine, the packaging was not, and no test
  # importing from `src/` can see that. This builds the wheel, installs it into
  # a clean venv (dependencies resolve from the index, as a user's install
  # would), and drives a real tool call through the gateway. It runs BEFORE
  # publish so a bad wheel can still be stopped; a published wheel cannot be
  # edited.
  smoke-wheel:
    name: Published-artifact smoke (pre-publish gate)
    needs: [validate, test]
    if: always() && needs.validate.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped')
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
        with:
          python-version: "3.11"
      - name: Build the wheel
        run: |
          python -m pip install --upgrade pip build
          python -m build --wheel
      - name: Smoke the built wheel in a clean venv
        run: python scripts/smoke_published_artifact.py --wheel "dist/mcp_hangar-*.whl"

  # Publish Python package to PyPI
  publish-pypi:
    name: Publish to PyPI
    needs: [validate, test, smoke-wheel]
    if: always() && needs.validate.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped') && needs.smoke-wheel.result == 'success'
    runs-on: ubuntu-latest
    timeout-minutes: 20
    defaults:
      run:
        working-directory: .
    environment:
      name: pypi
      url: https://pypi.org/p/mcp-hangar
    permissions:
      id-token: write  # Required for trusted publishing and OIDC signing
      attestations: write  # Required for build provenance attestations

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Set up Python
        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: Install build tools
        run: |
          python -m pip install --upgrade pip
          # `hatchling` is deliberately NOT installed here. `python -m build`
          # resolves the backend in an isolated env from `[build-system]
          # requires` in pyproject.toml, which carries the ==1.32.0 pin (#1513).
          # An unpinned install on this line would pull the broken 1.32.1 back
          # in beside the pin and mislead anyone reading it.
          pip install build

      - name: Build package
        run: python -m build

      - name: Generate build provenance attestation
        id: attest
        uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
        with:
          subject-path: 'dist/*'

      # The attestation is only useful to somebody who can fetch it beside the
      # artifact, so the bundle travels with the wheel onto the GitHub Release
      # (`create-release` downloads this). It is also what makes the release
      # legible to OpenSSF Signed-Releases, which reads release ASSETS: the
      # releases here carried none, so the check reported "no releases found"
      # even though every tag has one.
      #
      # NOT in `dist/`. `gh-action-pypi-publish` uploads every file in
      # `packages-dir`, and PyPI rejects anything that is not a distribution --
      # which is how 2.15.0 built, attested, passed `twine check` and then
      # failed at the upload with the wheel never leaving the runner.
      - name: Collect the attestation bundle
        run: |
          mkdir -p attestation
          cp "${{ steps.attest.outputs.bundle-path }}" attestation/mcp-hangar.intoto.jsonl

      - name: Upload the artifacts for the release job
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: release-artifacts
          path: |
            dist/
            attestation/
          if-no-files-found: error

      - name: Verify package
        run: |
          pip install twine
          # `dist/*` again, not a whitelist: the directory holds only
          # distributions now, and checking everything in it is what catches a
          # stray file before PyPI does.
          twine check dist/*

      # Prereleases (alpha/beta/rc) and stable releases both publish to prod PyPI.
      # pip ignores prereleases unless --pre or an explicit prerelease pin is used,
      # so an alpha on prod PyPI never affects a plain `pip install mcp-hangar`.
      # (TestPyPI was dropped: its trusted-publisher was never configured, and prod
      # PyPI already hosts the `pypi` environment's trusted publisher.)
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
        with:
          packages-dir: dist/
          # Idempotent: a re-run/recovery of an already-published version is a
          # no-op instead of a hard failure (PyPI rejects duplicate uploads).
          skip-existing: true

  # Publish the server.json entry to the Official MCP Registry.
  #
  # Here rather than in its own tag-triggered workflow because the registry
  # validates ownership by fetching the PyPI metadata for exactly
  # `packages[].version` and looking for the `mcp-name:` token in the README it
  # serves. That artifact only exists once `publish-pypi` has succeeded, so the
  # ordering is a `needs:`, not a polling loop -- with a short retry left for
  # PyPI's own metadata propagation.
  #
  # Stable releases only. The registry fetches `/pypi/<pkg>/<version>/json` with
  # the version spelled as it appears in server.json -- the semver form
  # release-please writes (`2.5.0-rc.1`) -- while PyPI serves the PEP 440 form
  # (`2.5.0rc1`). A prerelease would 404 there, and a prerelease listing in a
  # discovery registry is noise anyway.
  publish-registry:
    name: Publish to MCP Registry
    needs: [validate, publish-pypi]
    # `always()`, as publish-pypi has: without it a skipped `test` (a
    # `skip_tests` dispatch) skips this job through the needs chain even when
    # PyPI published, and v2.19.0 reached PyPI but not the registry.
    if: always() && needs.validate.result == 'success' && needs.publish-pypi.result == 'success' && needs.validate.outputs.is_prerelease == 'false'
    runs-on: ubuntu-latest
    timeout-minutes: 20
    # The private key is an ENVIRONMENT secret on this environment, never a repo
    # or org secret: it authorises publishing anything under `io.mcp-hangar/*`.
    # The `environment:` key alone protects nothing -- the protection is the
    # environment's own rules (tags `v*` only, required reviewer).
    environment:
      name: mcp-registry-publish
      url: https://registry.modelcontextprotocol.io
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      # release-please owns both version fields in server.json (extra-files).
      # If that updater ever silently stops firing, publishing would claim a
      # stale version against a fresh tag -- and the registry rejects a
      # duplicate version, so the mistake would be permanent for that release.
      - name: server.json version matches the tag
        run: |
          SERVER_VERSION=$(python3 -c 'import json;d=json.load(open("server.json"));print(d["version"])')
          PKG_VERSION=$(python3 -c 'import json;d=json.load(open("server.json"));print(d["packages"][0]["version"])')
          TAG_VERSION="${{ needs.validate.outputs.version }}"
          if [ "$SERVER_VERSION" != "$TAG_VERSION" ] || [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
            echo "❌ server.json says ${SERVER_VERSION}/${PKG_VERSION}, tag says ${TAG_VERSION}"
            echo "   Check the release-please extra-files updater for server.json."
            exit 1
          fi

      # The PyPI JSON API lags an upload by minutes (#680 saw the same lag break
      # `smoke-published`), and the registry reads that API to verify ownership.
      - name: Wait for PyPI metadata
        run: |
          V="${{ needs.validate.outputs.version }}"
          for _ in $(seq 1 30); do
            if curl -sf "https://pypi.org/pypi/mcp-hangar/$V/json" >/dev/null; then
              echo "✅ PyPI serves ${V}"
              exit 0
            fi
            sleep 20
          done
          echo "❌ mcp-hangar==$V not on PyPI after 10 minutes"
          exit 1

      # From `releases/latest` every run on purpose: a publisher binary older
      # than the registry fails authentication with "invalid audience".
      - name: Install mcp-publisher
        run: |
          curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher

      - name: Validate server.json
        run: ./mcp-publisher validate

      - name: Authenticate
        run: ./mcp-publisher login dns --domain mcp-hangar.io --private-key "${{ secrets.MCP_PRIVATE_KEY }}"

      # Not a hard gate: re-running this job after a successful publish hits
      # "cannot publish duplicate version". The verify step below is what
      # decides whether the release is actually listed.
      - name: Publish
        continue-on-error: true
        run: ./mcp-publisher publish

      - name: Verify the registry serves this version
        run: |
          V="${{ needs.validate.outputs.version }}"
          curl -sf "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.mcp-hangar/hangar" \
            | tee /dev/stderr | grep -q "\"version\":\"$V\""

  # Build and publish Docker images
  publish-docker:
    name: Publish Docker Image
    needs: [validate, test]
    if: always() && needs.validate.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped')
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
      packages: write
      id-token: write

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
            type=raw,value=latest,enable=${{ needs.validate.outputs.is_prerelease == 'false' }}
            # Explicit version tag from the validated release version, so a
            # workflow_dispatch run (github.ref is a branch, not a tag) still
            # gets an image tag — the semver patterns above only fire on a tag ref.
            type=raw,value=${{ needs.validate.outputs.version }}

      - name: Build and push Docker image
        id: docker-build
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
        with:
          context: .
          file: Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

      - name: Install cosign
        uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

      - name: Sign container image
        env:
          DIGEST: ${{ steps.docker-build.outputs.digest }}
          TAGS: ${{ steps.meta.outputs.tags }}
        run: |
          images=""
          for tag in ${TAGS}; do
            images="${images} ${tag}@${DIGEST}"
          done
          cosign sign --yes ${images}

  # Create GitHub Release with changelog
  create-release:
    name: Create GitHub Release
    needs: [validate, publish-pypi, publish-docker]
    if: always() && needs.validate.result == 'success' && needs.publish-pypi.result == 'success'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
        with:
          fetch-depth: 0

      - name: Extract changelog for version
        id: changelog
        run: |
          VERSION="${{ needs.validate.outputs.version }}"
          BASE_VERSION=$(echo "$VERSION" | sed 's/-.*//')

          # Extract section for this version from CHANGELOG.md
          CHANGELOG=$(awk "/^## \[${BASE_VERSION}\]/{flag=1; next} /^## \[/{flag=0} flag" CHANGELOG.md)

          # If no specific version found, use Unreleased section
          if [ -z "$CHANGELOG" ]; then
            CHANGELOG=$(awk '/^## \[Unreleased\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md)
          fi

          # Decode HTML entities inserted by release-please (option B from issue)
          CHANGELOG=$(echo "$CHANGELOG" | sed 's/&gt;/>/g; s/&lt;/</g; s/&amp;/\&/g')

          # Derive the previous release tag for the Full Changelog compare link
          PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^v${BASE_VERSION}$" | head -1)
          if [ -n "$PREV_TAG" ]; then
            FULL_CHANGELOG_LINE="**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...v${VERSION}"
          else
            FULL_CHANGELOG_LINE=""
          fi

          echo "content<<EOF" >> $GITHUB_OUTPUT
          echo "$CHANGELOG" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

          echo "full_changelog_line<<EOF" >> $GITHUB_OUTPUT
          echo "$FULL_CHANGELOG_LINE" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      # Into the workspace root, NOT into `dist/`. The artifact carries two
      # directories (`dist/` and `attestation/`), because upload-artifact keys
      # a multi-path upload on the least common ancestor -- so unpacking it
      # under `dist/` produces `dist/dist/*.whl`, every glob below matches
      # nothing, and `action-gh-release` publishes a release with no assets
      # while reporting success. That is what 2.15.0 shipped.
      - name: Download the published artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: release-artifacts
          path: .

      - name: Create GitHub Release
        uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3
        with:
          name: "v${{ needs.validate.outputs.version }}"
          # The wheel, the sdist and the provenance bundle. Without assets a
          # release is not verifiable by anyone downstream, and OpenSSF
          # Signed-Releases cannot see it at all.
          files: |
            dist/*.whl
            dist/*.tar.gz
            attestation/*.intoto.jsonl
          # Explicit tag so a workflow_dispatch run (github.ref is a branch)
          # attaches the release to the version tag instead of failing.
          tag_name: "v${{ needs.validate.outputs.version }}"
          body: |
            ## What's Changed

            ${{ steps.changelog.outputs.content }}

            ## Installation

            **PyPI:**
            ```bash
            pip install mcp-hangar==${{ needs.validate.outputs.version }}
            ```

            **Docker:**
            ```bash
            docker pull ghcr.io/${{ github.repository }}:${{ needs.validate.outputs.version }}
            ```

            ## Links
            - [PyPI Package](https://pypi.org/project/mcp-hangar/${{ needs.validate.outputs.version }}/)
            - [Docker Image](https://ghcr.io/${{ github.repository }}:${{ needs.validate.outputs.version }})
            - [Documentation](https://mcp-hangar.io)

            ${{ steps.changelog.outputs.full_changelog_line }}
          prerelease: ${{ needs.validate.outputs.is_prerelease == 'true' }}

  # The same smoke against what the index actually serves. This one cannot block
  # anything -- the wheel is already immutable -- but it is the only check that
  # sees precisely what a user receives, including the `--pre` resolve that the
  # v2-preview docs recommend (advisory: an upstream alpha breaking that resolve
  # is not a defect in this artifact, so it warns rather than fails).
  smoke-published:
    name: Published-artifact smoke (post-publish check)
    needs: [validate, publish-pypi]
    if: always() && needs.publish-pypi.result == 'success'
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
        with:
          python-version: "3.11"
      # NORMALIZED, not merely read. `pyproject.toml` carries whatever spelling
      # wrote it -- release-please writes the semver form, `2.5.0-rc.1` -- while
      # an installed distribution always reports the PEP 440 normal form,
      # `2.5.0rc1`. Both name the same release, and pip resolves `==` between
      # them happily, so the install succeeds and the smoke's identity assert
      # then fails on the spelling: "installed 2.5.0rc1, expected 2.5.0-rc.1",
      # on the first release-please-cut prerelease (2.5.0-rc.1).
      #
      # Normalized here rather than in the script, and by `packaging` rather
      # than by another sed: the repository already has one hand-rolled
      # normalization of these suffixes, in `validate` above, and a second copy
      # of a rule is a second thing that can disagree. This one asks the
      # implementation that defines the answer.
      - name: Resolve the PEP 440 version
        id: pep440
        run: |
          python -m pip install --quiet packaging
          raw=$(grep -m1 -E '^version = "' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
          normalized=$(python -c 'import sys; from packaging.version import Version; print(Version(sys.argv[1]))' "$raw")
          echo "version=${normalized}" >> $GITHUB_OUTPUT
          echo "pyproject says ${raw}; the index will serve ${normalized}"
      # `--tolerate-index-lag`: the simple index pip reads lags a publish by
      # minutes, and on 2.0.1 this job ran inside that window and went red on a
      # healthy release (#680). The script now retries for ~3.5 minutes and, if
      # the version is still invisible, asks PyPI's JSON API whether it exists at
      # all. Published-but-unindexed becomes a warning here and only here: this
      # placement cannot stop a release anyway, so a red would buy nothing and
      # cost the reader's attention. Every other failure -- a wheel that will not
      # install, a version PyPI never received -- still fails the job.
      - name: Smoke what PyPI serves
        run: |
          python scripts/smoke_published_artifact.py \
            --version "${{ steps.pep440.outputs.version }}" --also-pre --tolerate-index-lag
