name: Publish dist branch

# Builds the CSS bundles after every merge to main and force-pushes the
# resulting dist/ tree to a dedicated `dist` branch. The `dist` branch is
# orphan (no shared history with main) and contains only the generated
# bundles at its root, so consumers can reference them via:
#
#   https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@dist/slashed.optimal.min.css
#
# Source files (core/, optional/, etc.) live only on main; built files live
# only on dist. This keeps main free of generated artifacts while still
# giving the CDN something to serve.

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      git_ref:
        description: 'Exact git SHA or ref to build from (leave blank to use HEAD of the triggered ref)'
        required: false
        default: ''

# Skip only commits that explicitly opt out via [skip ci] in the message.
# The version-sync commit pushed by release.yml deliberately does NOT carry
# that tag, so a version bump always rebuilds the dist branch with the correct
# header.

# If several merges land in quick succession, cancel any in-flight publish
# and rerun against the newest commit only. Prevents racing force-pushes.
concurrency:
  group: publish-dist
  cancel-in-progress: true

permissions:
  contents: write

jobs:
  publish-dist:
    name: Build and force-push dist branch
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, '[skip ci]')"
    steps:
      - name: Checkout main
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 1
          # Don't write GITHUB_TOKEN into .git/config; the build steps
          # (npm install, npm run build) shouldn't have implicit push
          # credentials. We re-authenticate explicitly at push time below.
          persist-credentials: false
          # When dispatched from release.yml, git_ref is the exact post-sync SHA
          # so the dist bundles are always stamped with the released version.
          ref: ${{ inputs.git_ref || github.sha }}

      # Resolve the SHA that was actually checked out. When dispatched with a
      # custom git_ref, GITHUB_SHA still points at the dispatch trigger commit,
      # not the built tree — use this for provenance stamping instead.
      - name: Resolve checked-out source SHA
        id: source
        run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

      # No dependency cache on the publish path: a poisoned cache could be
      # baked into the dist bundles served from the CDN. Fetch fresh from the
      # registry with integrity verification (zizmor: cache-poisoning).
      - name: Setup Node
        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22

      - name: Install dependencies
        run: npm ci

      - name: Build dist bundles
        run: npm run build

      - name: Stage dist for orphan branch
        run: |
          set -euo pipefail
          mkdir -p /tmp/sf-dist
          # CSS bundles + css-custom-data.json live in dist/; the shields.io
          # badge endpoints (badge-*.json) live in badges/. Both must coexist at
          # the published `dist` branch root, so merge them here at publish time.
          cp -R dist/. /tmp/sf-dist/
          cp    badges/*.json /tmp/sf-dist/
          # Ship the machine-readable API index alongside the bundles so
          # integrations can fetch it from the same CDN path, e.g.:
          #   https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@dist/api-index.json
          cp docs/api-index.json /tmp/sf-dist/api-index.json
          # Stamp file so consumers can verify exactly which source commit
          # produced this dist tree.
          {
            echo "source-commit: ${{ steps.source.outputs.sha }}"
            echo "source-ref:    ${GITHUB_REF}"
            echo "built-at:      $(date -u +%Y-%m-%dT%H:%M:%SZ)"
            echo "built-by:      .github/workflows/publish-dist.yml"
          } > /tmp/sf-dist/SOURCE.txt
          # git rm -rf . (below) deletes .gitignore from the working tree, which
          # would cause `git add -A` to stage node_modules/ (134 MB). Ship a
          # minimal .gitignore on the dist branch to prevent that.
          echo 'node_modules/' > /tmp/sf-dist/.gitignore

      - name: Force-push dist branch
        env:
          # Scoped to this step only — checkout was started with
          # persist-credentials: false, so GITHUB_TOKEN never landed in
          # .git/config and never leaked into the build/install steps.
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GIT_AUTHOR_NAME: github-actions[bot]
          GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
          GIT_COMMITTER_NAME: github-actions[bot]
          GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
        run: |
          set -euo pipefail
          git checkout --orphan dist
          # The orphan checkout still stages main's tree; remove it all so
          # only built artifacts end up on the dist branch.
          git rm -rf . > /dev/null
          shopt -s dotglob
          cp -R /tmp/sf-dist/. .
          git add -A
          git commit -m "build: dist for ${{ steps.source.outputs.sha }}"
          git push --force \
            "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
            dist
