manasight-parser 0.5.3

MTG Arena log file parser — reads Player.log and emits typed game events
Documentation
name: Smoke Test

on:
  # Run after CI passes on main (ensures lint/test pass before smoke tests).
  workflow_run:
    workflows: ["CI"]
    types: [completed]
    branches: [main]

  # Run on PRs that touch parser or test code.
  pull_request:
    paths:
      - "src/**"
      - "tests/**"
      - "smoke-baseline.json"

  # Triggered by manasight-corpus when a new corpus release is published.
  repository_dispatch:
    types: [corpus-release]

  # Weekly on Sundays at 06:00 UTC.
  schedule:
    - cron: "0 6 * * 0"

  # Manual trigger for ad-hoc runs.
  workflow_dispatch:

permissions: {}

# Only one smoke test per ref at a time.
concurrency:
  group: smoke-test-${{ github.ref }}
  cancel-in-progress: true

jobs:
  smoke-test:
    name: Real-log smoke test
    runs-on: ubuntu-latest

    # Skip if the triggering CI workflow failed (workflow_run trigger).
    if: >-
      github.event_name != 'workflow_run' ||
      github.event.workflow_run.conclusion == 'success'

    # Needed for committing baseline updates back to main.
    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install Rust toolchain
        uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1

      # Query the latest corpus release tag for cache keying.
      - name: Resolve corpus release tag
        id: corpus-tag
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          TAG=$(gh release view --repo manasight/manasight-corpus --json tagName --jq '.tagName')
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "Corpus release tag: $TAG"

      # Cache the downloaded corpus tarball between runs.
      # The key uses the release tag so caches auto-invalidate on new releases.
      - name: Cache corpus tarball
        id: cache-corpus
        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
        with:
          path: /tmp/smoke-corpus.tar.gz
          key: smoke-corpus-${{ steps.corpus-tag.outputs.tag }}

      # Download corpus from the public manasight-corpus GitHub Release.
      - name: Download corpus
        if: steps.cache-corpus.outputs.cache-hit != 'true'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release download "${{ steps.corpus-tag.outputs.tag }}" \
            --repo manasight/manasight-corpus \
            --pattern "*.tar.gz" \
            --output /tmp/smoke-corpus.tar.gz

      # Extract corpus and decompress log files.
      # The tarball contains corpus/*.log.gz — extract and gunzip to get .log files.
      - name: Extract corpus
        run: |
          mkdir -p /tmp/test-logs
          tar -xzf /tmp/smoke-corpus.tar.gz -C /tmp/test-logs
          gunzip /tmp/test-logs/corpus/*.log.gz
          echo "Corpus files:"
          ls -lh /tmp/test-logs/corpus/

      # Run smoke tests with ratchet comparison against baseline.
      - name: Run smoke tests
        id: smoke
        env:
          MANASIGHT_TEST_LOGS: /tmp/test-logs/corpus
          CORPUS_TAG: ${{ steps.corpus-tag.outputs.tag }}
        run: |
          # pipefail is load-bearing: without it, tee's exit 0 masks cargo test failures.
          set -o pipefail
          set +e
          cargo test --all-features smoke_test_real_logs -- --nocapture 2>&1 | tee /tmp/smoke-output.txt
          SMOKE_EXIT=$?
          set -e

          echo "exit_code=$SMOKE_EXIT" >> "$GITHUB_OUTPUT"

          # Parse ratchet output for job summary.
          # Extract everything after "Ratchet" line for the summary.
          if grep -q "Ratchet" /tmp/smoke-output.txt; then
            echo "has_ratchet=true" >> "$GITHUB_OUTPUT"
          else
            echo "has_ratchet=false" >> "$GITHUB_OUTPUT"
          fi

          exit $SMOKE_EXIT

      # Write job summary with ratchet results.
      - name: Write job summary
        if: always() && steps.smoke.outcome != 'skipped'
        run: |
          {
            echo "## Smoke Test Results"
            echo ""

            if [ "${{ steps.smoke.outcome }}" = "success" ]; then
              echo "**Status:** PASS"
            else
              echo "**Status:** FAIL"
            fi
            echo ""

            # Build a comparison table from the smoke output.
            echo "### Per-File Summary"
            echo ""
            echo "| File | Entries | Claimed | Unclaimed | Ratchet |"
            echo "|------|---------|---------|-----------|---------|"

            # Parse each file's stats from the smoke output using POSIX awk
            # to avoid fd-sharing issues with nested read loops.
            # Note: uses only POSIX awk features (no gawk-specific match()).
            awk '
              /^File: .+ \([0-9]+ entries\)/ {
                # Strip "File: " prefix and " (NNN entries)" suffix.
                s = $0
                sub(/^File: /, "", s)
                sub(/ \([0-9]+ entries\)$/, "", s)
                fname = s
                # Extract entry count from between parens.
                s = $0
                sub(/.*\(/, "", s)
                sub(/ entries\).*/, "", s)
                entries = s + 0
                unclaimed = 0
                in_file = 1
                next
              }
              in_file && /unclaimed:/ {
                # Extract the unclaimed number.
                s = $0
                sub(/.*unclaimed: */, "", s)
                sub(/ .*/, "", s)
                unclaimed = s + 0
                claimed = entries - unclaimed
                pct = (entries > 0) ? int(claimed * 100 / entries) : 0
                printf "| `%s` | %d | %d (%d%%) | %d | -- |\n", fname, entries, claimed, pct, unclaimed
                in_file = 0
              }
              /^===/ { in_file = 0 }
            ' /tmp/smoke-output.txt

            echo ""

            # Include ratchet report section.
            if [ "${{ steps.smoke.outputs.has_ratchet }}" = "true" ]; then
              echo "### Ratchet Comparison"
              echo ""
              echo '```'
              sed -n '/^Ratchet/,/^$/p' /tmp/smoke-output.txt
              echo '```'
            else
              echo "### Ratchet Comparison"
              echo ""
              echo "_No baseline comparison performed._"
            fi
          } >> "$GITHUB_STEP_SUMMARY"

      # Upload full smoke test output as artifact.
      - name: Upload report
        if: always() && steps.smoke.outcome != 'skipped'
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: smoke-test-report
          path: /tmp/smoke-output.txt
          retention-days: 30

      # Auto-update baseline when improvements are detected on main.
      - name: Check for baseline improvements
        if: >-
          steps.smoke.outcome == 'success' &&
          steps.smoke.outputs.has_ratchet == 'true' &&
          github.ref == 'refs/heads/main'
        id: baseline-check
        run: |
          if grep -q '\[+\]' /tmp/smoke-output.txt; then
            echo "has_improvements=true" >> "$GITHUB_OUTPUT"
          else
            echo "has_improvements=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Update baseline and open PR
        if: >-
          steps.baseline-check.outcome == 'success' &&
          steps.baseline-check.outputs.has_improvements == 'true'
        env:
          MANASIGHT_TEST_LOGS: /tmp/test-logs/corpus
          CORPUS_TAG: ${{ steps.corpus-tag.outputs.tag }}
          SMOKE_BLESS: "1"
          # Use a PAT so the resulting pull_request event triggers CI workflows.
          # The default GITHUB_TOKEN suppresses downstream workflow triggers.
          GH_TOKEN: ${{ secrets.BASELINE_PR_TOKEN }}
        run: |
          # Re-run in bless mode to write updated baseline.
          if ! cargo test --all-features smoke_test_real_logs -- --nocapture 2>&1; then
            echo "::warning::Bless mode run exited non-zero — baseline may not have been updated"
          fi

          # Check if baseline actually changed.
          if git diff --quiet smoke-baseline.json; then
            echo "Baseline unchanged after bless — nothing to do."
            exit 0
          fi

          # Configure git identity for the commit (checkout@v4 does not set these).
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

          # Override the remote URL so git push uses the PAT (not the
          # GITHUB_TOKEN that actions/checkout@v4 configured).
          git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

          BRANCH="auto/update-smoke-baseline-$(date +%Y%m%d-%H%M%S)"
          git checkout -b "$BRANCH"
          git add smoke-baseline.json
          git commit -m "chore: auto-update smoke-baseline.json

          Smoke test detected improved parser counts on main.
          This baseline update ratchets forward to the new counts.

          Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"

          git push -u origin "$BRANCH"

          gh pr create \
            --base main \
            --title "chore: auto-update smoke test baseline" \
            --body "$(cat <<'EOF'
          ## Summary
          Smoke test CI detected improved parser counts on main.
          This PR updates `smoke-baseline.json` to ratchet forward to the new counts.

          Auto-generated by the smoke test workflow.
          EOF
          )"