name: Release — Bundle Plugin & Skills

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag to attach artifacts to (e.g. v1.0.0). Leave blank to skip upload."
        required: false
        default: ""

jobs:
  bundle:
    runs-on: ubuntu-latest
    permissions:
      contents: write   # needed to upload release assets

    steps:
      - name: Checkout Coeus
        uses: actions/checkout@v5

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.12'

      - name: Install Python dependencies
        run: pip install pyyaml

      # ── Resolve release tag + assert it matches plugin.json version ───────
      #
      # Source-of-truth rule: the GitHub Release tag is authoritative. The two
      # plugin.json files (canonical + reference copy) MUST match the tag (with
      # an optional leading 'v'). If they don't, we fail the workflow rather
      # than ship a coeus.plugin whose internal version disagrees with what
      # users see in the Release page. This is the fix for "release not
      # recognising actual release version" — drift between tag and manifest
      # would otherwise produce a plugin that reports the wrong version.

      - name: Resolve release tag
        id: tag
        env:
          EVENT_NAME: ${{ github.event_name }}
          RELEASE_TAG: ${{ github.event.release.tag_name }}
          INPUT_TAG: ${{ github.event.inputs.tag }}
        run: |
          set -eu
          if [ "${EVENT_NAME}" = "release" ]; then
            TAG="${RELEASE_TAG}"
          else
            TAG="${INPUT_TAG}"
          fi
          if [ -z "${TAG}" ]; then
            echo "No tag — workflow_dispatch run without input. Will build but not upload."
            {
              echo "tag="
              echo "version="
            } >> "${GITHUB_OUTPUT}"
          else
            VERSION="${TAG#v}"   # strip leading v
            {
              echo "tag=${TAG}"
              echo "version=${VERSION}"
            } >> "${GITHUB_OUTPUT}"
            echo "Resolved tag=${TAG} version=${VERSION}"
          fi

      - name: Assert plugin.json version matches release tag
        if: steps.tag.outputs.version != ''
        env:
          EXPECTED: ${{ steps.tag.outputs.version }}
        run: |
          set -eu
          CANONICAL=$(jq -r '.version' .claude-plugin/plugin.json)
          echo "tag version       : ${EXPECTED}"
          echo "canonical manifest: ${CANONICAL}"
          if [ "${CANONICAL}" != "${EXPECTED}" ]; then
            echo ""
            echo "✗ .claude-plugin/plugin.json version (${CANONICAL}) does not match release tag (${EXPECTED})"
            echo "Bump .claude-plugin/plugin.json to ${EXPECTED} before re-running the release."
            exit 1
          fi
          echo "✅ version aligned"

      # ── Fetch caveman from upstream (canonical source) ────────────────────

      - name: Fetch caveman (JuliusBrussee/caveman @ main)
        run: |
          echo "Fetching caveman upstream..."
          curl -sL \
            "https://api.github.com/repos/JuliusBrussee/caveman/tarball/main" \
            -H "Accept: application/vnd.github+json" \
            -H "User-Agent: coeus-release-bundler" \
            -o /tmp/caveman.tar.gz

          mkdir -p /tmp/caveman-src
          tar -xzf /tmp/caveman.tar.gz -C /tmp/caveman-src --strip-components=1
          echo "caveman extracted:"
          ls /tmp/caveman-src

      - name: Package caveman.skill from upstream
        run: |
          # JuliusBrussee/caveman keeps the skill under skills/caveman/
          mkdir -p /tmp/skills/caveman

          if [ -d "/tmp/caveman-src/skills/caveman" ]; then
            rsync -a \
              --exclude='.*' \
              --exclude='__pycache__' \
              /tmp/caveman-src/skills/caveman/ \
              /tmp/skills/caveman/
          else
            # Fallback: copy entire repo root
            rsync -a \
              --exclude='.*' \
              --exclude='__pycache__' \
              /tmp/caveman-src/ \
              /tmp/skills/caveman/
          fi

          cd /tmp/skills
          zip -r caveman.skill caveman/
          echo "caveman.skill (upstream) → $(wc -c < caveman.skill) bytes"

      # ── Fetch prompt-master from upstream (canonical source) ──────────────

      - name: Fetch prompt-master (nidhinjs/prompt-master @ main)
        run: |
          echo "Fetching prompt-master..."
          curl -sL \
            "https://api.github.com/repos/nidhinjs/prompt-master/tarball/main" \
            -H "Accept: application/vnd.github+json" \
            -H "User-Agent: coeus-release-bundler" \
            -o /tmp/prompt-master.tar.gz

          mkdir -p /tmp/prompt-master-src
          tar -xzf /tmp/prompt-master.tar.gz -C /tmp/prompt-master-src --strip-components=1

      - name: Package prompt-master.skill from upstream
        run: |
          mkdir -p /tmp/skills/prompt-master
          # Mirror upstream skill files; keep Coeus-bundled layout consistent with
          # the committed prompt-master/ dir (SKILL.md + references/, no upstream
          # README/LICENSE).
          rsync -a \
            --exclude='.*' \
            --exclude='README.md' \
            --exclude='LICENSE' \
            --exclude='CONTRIBUTING.md' \
            --exclude='CLA.md' \
            --exclude='__pycache__' \
            /tmp/prompt-master-src/ \
            /tmp/skills/prompt-master/

          cd /tmp/skills
          zip -r prompt-master.skill prompt-master/
          echo "prompt-master.skill → $(wc -c < prompt-master.skill) bytes"

      # ── Package remaining Coeus skills from repo ──────────────────────────

      - name: Package individual Coeus .skill files
        run: |
          mkdir -p dist
          # Skills now live under skills/<name>/ per the Claude Code plugin spec.
          # Zip them with the skills/ prefix so the .skill artefact is
          # spec-compliant when extracted standalone.
          for skill in llm-council morpheus the-architect ep-council; do
            if [ -d "skills/${skill}" ]; then
              (cd skills && zip -r "../dist/${skill}.skill" "${skill}/")
              echo "${skill}.skill → $(wc -c < dist/${skill}.skill) bytes"
            else
              echo "WARNING: directory 'skills/${skill}/' not found — skipping"
            fi
          done

          # caveman.skill and prompt-master.skill come from upstream fetch
          cp /tmp/skills/caveman.skill dist/caveman.skill
          cp /tmp/skills/prompt-master.skill dist/prompt-master.skill
          echo "caveman.skill + prompt-master.skill (upstream) copied to dist/"

      # ── Bundle coeus.plugin (all six skills, incl. upstream prereqs) ──────

      - name: "Bundle coeus.plugin (single source of truth: scripts/build-plugin.py)"
        run: |
          # Swap in the upstream-fetched caveman + prompt-master so the release
          # bundles the latest upstream content (release-time freshness).
          # Backups MUST live outside skills/ -- check_skills() scans every
          # skills/* dir and would flag *-local-backup as broken skills.
          mkdir -p /tmp/coeus-backup
          mv skills/caveman /tmp/coeus-backup/caveman
          mv skills/prompt-master /tmp/coeus-backup/prompt-master
          cp -r /tmp/skills/caveman skills/caveman
          cp -r /tmp/skills/prompt-master skills/prompt-master

          # Normalize the upstream-fresh skills before build-plugin.py runs its
          # check_skills() invariants. Mirrors sync-upstream-skills.yml so the
          # release-time inline-swap doesn't trip:
          #   - check_skills #3 name-match (upstream caveman ships as
          #     'caveman-protocol'; we need 'caveman' to match the dir).
          #   - argument-hint required for slash-command UI (vendored skills
          #     have it re-injected on every weekly sync, but the release
          #     pull bypasses the sync, so re-inject here too).
          for entry in "caveman|[lite|full|ultra|wenyan] [text to compress, optional]" \
                       "prompt-master|[what you want the prompt to do] [target tool]"; do
            name="${entry%%|*}"
            hint="${entry#*|}"
            f="skills/${name}/SKILL.md"
            if [ -f "$f" ]; then
              sed -i -E "0,/^name:.*/s//name: ${name}/" "$f"
              if ! grep -q '^argument-hint:' "$f"; then
                sed -i -E "/^name: ${name}$/a argument-hint: \"${hint}\"" "$f"
              fi
              echo "Normalized $f -> name: ${name}"
            fi
          done

          # Delegate to the single cross-platform builder used locally. This
          # guarantees byte-for-byte parity with `python scripts/build-plugin.py`
          # run on a developer machine: UTF-8 filename flag (0x800) on every
          # entry (Cowork-safe), POSIX permissions, forward-slash paths.
          python scripts/build-plugin.py

          # Restore local dirs for any subsequent steps.
          rm -rf skills/caveman skills/prompt-master
          mv /tmp/coeus-backup/caveman skills/caveman
          mv /tmp/coeus-backup/prompt-master skills/prompt-master

          echo "coeus.plugin -> $(wc -c < dist/coeus.plugin) bytes"
          echo "Contents:"
          unzip -l dist/coeus.plugin | tail -40

      # ── Build per-skill paste bundles (for claude.ai web chat) ───────────
      # Single-message paste-prompts for surfaces that cannot install plugins.
      # One per owned council / architect / cluster skill.
      - name: Build paste bundles
        run: |
          for s in llm-council the-architect ep-council morpheus plugin-creator project-lifecycle; do
            python scripts/build-skill-paste.py "$s"
          done

      - name: List dist artifacts
        run: ls -lh dist/

      # ── Upload all artifacts to the GitHub Release ────────────────────────

      - name: Upload release assets
        if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '')
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.event_name == 'release' && github.ref_name || github.event.inputs.tag }}
          files: |
            dist/coeus.plugin
            dist/llm-council.skill
            dist/morpheus.skill
            dist/the-architect.skill
            dist/ep-council.skill
            dist/caveman.skill
            dist/prompt-master.skill
            dist/coeus-llm-council.paste.md
            dist/coeus-the-architect.paste.md
            dist/coeus-ep-council.paste.md
            dist/coeus-morpheus.paste.md
            dist/coeus-plugin-creator.paste.md
            dist/coeus-project-lifecycle.paste.md

      # Publish to public mirror repo for marketplace install path.
      # Closes the friction of the Personal-upload install path
      # (Anthropic bugs #40600 / #28554 / #63624). Mirror repo carries only
      # the plugin runtime + marketplace.json -- no source, no .git history
      # of the private repo. Bootstrap: create empty public keithceh/Coeus-plugin
      # on GitHub once, generate PAT with repo scope, add as secret
      # COEUS_MIRROR_PAT. After that this step runs on every tag.

      - name: Publish bundle to public mirror
        if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '')
        env:
          COEUS_MIRROR_PAT: ${{ secrets.COEUS_MIRROR_PAT }}
          MIRROR_REPO: keithceh/Coeus-plugin
          RELEASE_TAG: ${{ github.event_name == 'release' && github.ref_name || github.event.inputs.tag }}
        run: |
          set -eu

          if [ -z "${COEUS_MIRROR_PAT:-}" ]; then
            echo "::warning::COEUS_MIRROR_PAT secret not set -- public mirror sync skipped."
            echo "Bootstrap once: create public keithceh/Coeus-plugin on GitHub, generate fine-grained PAT with Contents:Read+Write scoped to that one repo, add as repo secret COEUS_MIRROR_PAT."
            exit 0
          fi

          # 1. Stage the bundle: extract the built coeus.plugin to a clean tree.
          rm -rf /tmp/mirror
          mkdir -p /tmp/mirror
          unzip -q dist/coeus.plugin -d /tmp/mirror

          # 2. Drop the marketplace.json template into the bundle's .claude-plugin/.
          mkdir -p /tmp/mirror/.claude-plugin
          cp scripts/marketplace.json.template /tmp/mirror/.claude-plugin/marketplace.json

          # 2b. Ship the one-line installer at the mirror root so users can run
          #   iwr https://raw.githubusercontent.com/keithceh/Coeus-plugin/main/install-coeus.ps1 | iex
          cp scripts/install-coeus.ps1 /tmp/mirror/install-coeus.ps1

          # 2c. Stage mirror-only workflows. The mirror is force-pushed
          # history-less every tag, so its .github/workflows/ must be re-shipped
          # each time. Currently: release-announce.yml posts a Discussions
          # announcement on every published release (the "users opt-in for
          # release email" channel).
          mkdir -p /tmp/mirror/.github/workflows
          cp scripts/mirror-workflows/release-announce.yml /tmp/mirror/.github/workflows/release-announce.yml

          # 3. Drop a public-facing README. Heredoc body must NOT be indented --
          # the closing EOF is unindented and the bash heredoc body needs no
          # left-padding (any indentation would appear inside the README).
          cat > /tmp/mirror/README.md <<EOF
          # Coeus -- public plugin mirror

          Public distribution mirror of the Coeus Claude plugin. Source lives
          at https://github.com/keithceh/Coeus (private, BSL 1.1 -> Apache 2.0
          in 2028). This mirror carries only the built plugin bundle.

          ## Install

          /plugin marketplace add keithceh/Coeus-plugin
          /plugin install coeus@coeus

          Persists across Cowork restart, auto-updates on /plugin update coeus.

          ## License

          BSL 1.1 (until 2028-05-28) -> Apache 2.0. See LICENSE for full terms.

          ## Updates

          Synced automatically from the source repo on every tagged release.
          File issues at https://github.com/keithceh/Coeus -- not here.

          Tag: ${RELEASE_TAG}
          EOF

          # 4. Init a fresh git tree and force-push to the mirror (history-less).
          cd /tmp/mirror
          git init -b main -q
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git config user.name "github-actions[bot]"
          git add -A
          git commit -q -m "Coeus mirror sync: ${RELEASE_TAG}"
          git tag -f "${RELEASE_TAG}"
          git push -q --force "https://x-access-token:${COEUS_MIRROR_PAT}@github.com/${MIRROR_REPO}.git" main
          git push -q --force "https://x-access-token:${COEUS_MIRROR_PAT}@github.com/${MIRROR_REPO}.git" "${RELEASE_TAG}"

          echo "::notice::Mirror sync OK to ${MIRROR_REPO}@${RELEASE_TAG}"

      # Mirror the source GitHub Release onto the public mirror so the mirror's
      # /releases page carries the same artifacts and writeups. Body is pulled
      # live from the source release (gh api ... --jq .body) so any edits the
      # maintainer makes to the source release notes flow through automatically
      # on re-run via workflow_dispatch.
      - name: Publish matching release on mirror
        if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '')
        env:
          GH_TOKEN: ${{ secrets.COEUS_MIRROR_PAT }}
          SOURCE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          MIRROR_REPO: keithceh/Coeus-plugin
          SOURCE_REPO: ${{ github.repository }}
          RELEASE_TAG: ${{ github.event_name == 'release' && github.ref_name || github.event.inputs.tag }}
        run: |
          set -eu

          if [ -z "${GH_TOKEN:-}" ]; then
            echo "::warning::COEUS_MIRROR_PAT not set -- skipping mirror release."
            exit 0
          fi

          # 1. Fetch the source release's title + body (private repo: use the
          #    job's default token, NOT the mirror PAT).
          REL_JSON=$(GH_TOKEN="${SOURCE_TOKEN}" gh api \
            "repos/${SOURCE_REPO}/releases/tags/${RELEASE_TAG}" 2>/dev/null || echo "")

          if [ -z "${REL_JSON}" ]; then
            # workflow_dispatch can fire before the source release exists -- fall
            # back to a minimal stub body so the mirror release still appears.
            REL_TITLE="${RELEASE_TAG}"
            REL_BODY="Mirror of ${SOURCE_REPO}@${RELEASE_TAG}. See the source repo for release notes."
          else
            REL_TITLE=$(echo "${REL_JSON}" | jq -r '.name // .tag_name')
            REL_BODY=$(echo "${REL_JSON}" | jq -r '.body // ""')
          fi

          # 2. Append a mirror-context footer so it's clear where this came from.
          BODY_FILE="$(mktemp)"
          {
            printf '%s\n\n' "${REL_BODY}"
            printf -- '---\n'
            printf '_Mirror of [%s@%s](https://github.com/%s/releases/tag/%s). Built and synced by `release.yml` on every tagged release of the source repo._\n' \
              "${SOURCE_REPO}" "${RELEASE_TAG}" "${SOURCE_REPO}" "${RELEASE_TAG}"
          } > "${BODY_FILE}"

          # 3. Create-or-update the mirror release. `gh release create` errors if
          #    it already exists -- in that case, edit it and re-upload assets.
          ASSETS=(
            dist/coeus.plugin
            dist/llm-council.skill
            dist/morpheus.skill
            dist/the-architect.skill
            dist/ep-council.skill
            dist/caveman.skill
            dist/prompt-master.skill
            dist/coeus-llm-council.paste.md
            dist/coeus-the-architect.paste.md
            dist/coeus-ep-council.paste.md
            dist/coeus-morpheus.paste.md
            dist/coeus-plugin-creator.paste.md
            dist/coeus-project-lifecycle.paste.md
          )

          if gh release view "${RELEASE_TAG}" --repo "${MIRROR_REPO}" >/dev/null 2>&1; then
            echo "Mirror release ${RELEASE_TAG} exists -- updating notes and re-uploading assets."
            gh release edit "${RELEASE_TAG}" --repo "${MIRROR_REPO}" \
              --title "${REL_TITLE}" --notes-file "${BODY_FILE}"
            gh release upload "${RELEASE_TAG}" --repo "${MIRROR_REPO}" --clobber "${ASSETS[@]}"
          else
            gh release create "${RELEASE_TAG}" --repo "${MIRROR_REPO}" \
              --title "${REL_TITLE}" --notes-file "${BODY_FILE}" \
              "${ASSETS[@]}"
          fi

          echo "::notice::Mirror release published at https://github.com/${MIRROR_REPO}/releases/tag/${RELEASE_TAG}"

      - name: Summary
        run: |
          echo "## Release artifacts" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| File | Notes | Size |" >> $GITHUB_STEP_SUMMARY
          echo "|------|-------|------|" >> $GITHUB_STEP_SUMMARY
          echo "| coeus.plugin | Full bundle — all 6 skills incl. upstream caveman + prompt-master | $(wc -c < dist/coeus.plugin) bytes |" >> $GITHUB_STEP_SUMMARY
          for f in dist/llm-council.skill dist/morpheus.skill dist/the-architect.skill dist/ep-council.skill; do
            echo "| $(basename $f) | Coeus skill | $(wc -c < $f) bytes |" >> $GITHUB_STEP_SUMMARY
          done
          echo "| caveman.skill | Upstream JuliusBrussee/caveman@main | $(wc -c < dist/caveman.skill) bytes |" >> $GITHUB_STEP_SUMMARY
          echo "| prompt-master.skill | Upstream nidhinjs/prompt-master@main | $(wc -c < dist/prompt-master.skill) bytes |" >> $GITHUB_STEP_SUMMARY
