name: Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      version:
        description: 'Release version (e.g. v0.3.0)'
        required: true
        type: string

permissions:
  contents: write

jobs:
  release:
    name: Publish GitHub Release
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 0
      # No dependency cache on the release path: a poisoned cache could be
      # baked into the published release artifacts. Fetch fresh from the
      # registry with integrity verification (zizmor: cache-poisoning).
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
      - run: npm ci
      - name: Sync version constants to tag
        env:
          DISPATCH_VERSION: ${{ inputs.version }}
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            TAG="${DISPATCH_VERSION}"
          else
            TAG="${GITHUB_REF_NAME}"
          fi
          VERSION="${TAG#v}"
          npm version "$VERSION" --no-git-tag-version --allow-same-version
          node scripts/version-sync.js
      - run: npm run build
      - name: Determine version tag
        id: version
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            echo "tag=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
          else
            echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
          fi
      - name: Verify dist headers match the release tag
        # Guard against silent mis-stamping: every built bundle header must read
        # the version being released. Fails the release if any header drifts.
        run: |
          EXPECTED="${{ steps.version.outputs.tag }}"
          EXPECTED="${EXPECTED#v}"
          fail=0
          for f in dist/*.css; do
            # Minified bundles drop the non-license comment, so only the
            # unminified bundles carry the stamped header — check those.
            case "$f" in *.min.css) continue;; esac
            header="$(head -1 "$f")"
            if ! printf '%s' "$header" | grep -q "SLASHED v${EXPECTED} "; then
              echo "::error file=${f}::expected 'SLASHED v${EXPECTED}', got: ${header}"
              fail=1
            fi
          done
          if [ "$fail" -ne 0 ]; then
            echo "Built dist headers do not match release tag v${EXPECTED}." >&2
            exit 1
          fi
          echo "All dist headers correctly stamped v${EXPECTED}."
      - name: Extract changelog for this version
        id: changelog
        run: |
          VERSION="${{ steps.version.outputs.tag }}"
          VERSION="${VERSION#v}"
          NOTES=$(awk -v target="$VERSION" '
            $1 == "##" {
              heading = $2
              gsub(/^\[/, "", heading)
              gsub(/\]$/, "", heading)
              if (found) exit
              if (heading == target) { found = 1; next }
            }
            found { print }
          ' CHANGELOG.md)
          if [ -z "$NOTES" ]; then
            echo "Warning: no CHANGELOG section found for v$VERSION" >&2
          fi
          echo "notes<<EOF" >> "$GITHUB_OUTPUT"
          echo "$NOTES" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
      - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228  # v3
        with:
          tag_name: ${{ steps.version.outputs.tag }}
          body: ${{ steps.changelog.outputs.notes }}
          files: |
            dist/slashed.optimal.css
            dist/slashed.optimal.min.css
            dist/slashed.optimal.min.css.map
            dist/slashed.optimal.flat.css
            dist/slashed.optimal.flat.min.css
            dist/slashed.optimal.flat.min.css.map
            dist/slashed.full.css
            dist/slashed.full.min.css
            dist/slashed.full.min.css.map
            dist/slashed.full.flat.css
            dist/slashed.full.flat.min.css
            dist/slashed.full.flat.min.css.map
          fail_on_unmatched_files: true

  # Aligns main's committed version artifacts (package.json, package-lock.json,
  # docs/roadmap.md) to the released tag, then pushes back to main.
  #
  # Why this lives here and not in a separate `on: release` workflow:
  # a GitHub Release created by release.yml uses the built-in GITHUB_TOKEN, and
  # GitHub does NOT emit `release` (or any) events for actions taken by that
  # token — so an `on: release` workflow would never fire. The tag push that
  # triggers THIS workflow is a real event, so syncing here always runs.
  #
  # NOTE: pushes made with GITHUB_TOKEN do NOT trigger push-based workflow
  # events (deploy-configurator.yml, publish-dist.yml). After pushing, we
  # explicitly dispatch both via workflow_dispatch (which GITHUB_TOKEN CAN
  # trigger) so the configurator and dist branch are rebuilt with the correct
  # version stamp.
  sync-main:
    name: Sync version artifacts to main
    needs: release
    runs-on: ubuntu-latest
    permissions:
      contents: write
      actions: write
    steps:
      - name: Checkout main
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          ref: main
          fetch-depth: 0
          fetch-tags: true
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
      - name: Determine version
        id: ver
        env:
          DISPATCH_VERSION: ${{ inputs.version }}
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            TAG="${DISPATCH_VERSION}"
          else
            TAG="${GITHUB_REF_NAME}"
          fi
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
          echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
      - name: Align version artifacts on main
        run: |
          npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
          node scripts/version-sync.js
      - name: Promote ## Unreleased in CHANGELOG to versioned heading
        run: node scripts/changelog-release.js "${{ steps.ver.outputs.tag }}"
      - name: Commit and push if anything changed
        id: commit_push
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git config user.name  "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add package.json package-lock.json docs/roadmap.md docs/llm-guide.md llms.txt CHANGELOG.md configurator/package.json configurator/package-lock.json
          if git diff --cached --quiet; then
            echo "Nothing to commit — version artifacts on main already match ${{ steps.ver.outputs.tag }}."
            echo "pushed=false" >> "$GITHUB_OUTPUT"
          else
            git commit -m "chore: sync version artifacts to ${{ steps.ver.outputs.tag }}"
            git push \
              "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
              HEAD:main
            echo "pushed=true" >> "$GITHUB_OUTPUT"
          fi
          # Capture the exact SHA now (post-push) so downstream dispatches use
          # the version-bumped commit, not a racy resolution of "main" via the
          # GitHub API (which may still see the pre-push HEAD for a few seconds).
          echo "deploy_ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
      - name: Trigger downstream deploys
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Always redeploy on every release, regardless of whether main needed
          # a version-artifact commit. When release-it already pushed the bump
          # before the tag fired, commit_push.pushed is false but the deploy
          # must still run so the configurator reflects the new version.
          # GITHUB_TOKEN CAN trigger workflow_dispatch (actions:write granted above).
          # Each call is independent so one failure does not prevent the other.
          # Use the exact post-sync SHA (not --ref main) so the deploy workflows
          # always build from the version-bumped commit. The GitHub API can lag
          # a few seconds behind a just-completed push, causing a racy --ref main
          # dispatch to resolve to the pre-bump HEAD and bake the wrong version.
          DEPLOY_REF="${{ steps.commit_push.outputs.deploy_ref }}"
          fail=0
          if gh workflow run deploy-configurator.yml --ref main -F git_ref="$DEPLOY_REF"; then
            echo "Dispatched deploy-configurator.yml at $DEPLOY_REF"
          else
            echo "::error::Failed to dispatch deploy-configurator.yml"
            fail=1
          fi

          if gh workflow run publish-dist.yml --ref main -F git_ref="$DEPLOY_REF"; then
            echo "Dispatched publish-dist.yml at $DEPLOY_REF"
          else
            echo "::error::Failed to dispatch publish-dist.yml"
            fail=1
          fi

          exit "$fail"
