name: 🦋 Changesets Release

on:
  pull_request:
    types: [closed]
    branches:
      - main
  workflow_dispatch:
    inputs:
      type:
        description: "Select release type"
        required: true
        type: choice
        options:
          - release
          - prerelease
        default: "prerelease"
      ref:
        description: "The ref (branch, tag, or SHA) to checkout and release from"
        required: true
        type: string
      prerelease_tag:
        description: "The npm dist-tag for the prerelease (e.g., 'v4-prerelease')"
        required: false
        type: string
        default: "prerelease"

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false

jobs:
  show-release-summary:
    name: 📋 Release Summary
    runs-on: ubuntu-latest
    permissions: {}
    if: |
      github.repository == 'triggerdotdev/trigger.dev' &&
      github.event_name == 'pull_request' &&
      github.event.pull_request.merged == true &&
      github.event.pull_request.head.ref == 'changeset-release/main'
    steps:
      - name: Show release summary
        env:
          PR_BODY: ${{ github.event.pull_request.body }}
        run: |
          echo "$PR_BODY" | sed -n '/^# Releases/,$p' >> "$GITHUB_STEP_SUMMARY"

  release:
    name: 🚀 Release npm packages
    runs-on: ubuntu-latest
    environment: npm-publish
    permissions:
      contents: write
      packages: write
      id-token: write
    if: |
      github.repository == 'triggerdotdev/trigger.dev' &&
      (
        (github.event_name == 'workflow_dispatch' && github.event.inputs.type == 'release') ||
        (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'changeset-release/main')
      )
    outputs:
      published: ${{ steps.changesets.outputs.published }}
      published_packages: ${{ steps.changesets.outputs.publishedPackages }}
      published_package_version: ${{ steps.get_version.outputs.package_version }}
      is_prerelease: ${{ steps.get_version.outputs.is_prerelease }}
    steps:
      - name: Checkout repo
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # zizmor: ignore[artipacked] needs persisted git creds for tag push; no artifact upload here so no leak path
        with:
          fetch-depth: 0
          ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.sha }}

      - name: Verify ref is on main
        if: github.event_name == 'workflow_dispatch'
        run: |
          if ! git merge-base --is-ancestor "${GITHUB_EVENT_INPUTS_REF}" origin/main; then
            echo "Error: ref must be an ancestor of main (i.e., already merged)"
            exit 1
          fi
        env:
          GITHUB_EVENT_INPUTS_REF: ${{ github.event.inputs.ref }}

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
        with:
          version: 10.33.2

      - name: Setup node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: 22.23.1
          cache: "pnpm"

      # npm v11.5.1 or newer is required for OIDC support
      # https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/#whats-new
      - name: Setup npm 11.x for OIDC
        run: npm install -g npm@11.6.4

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Generate Prisma client
        run: pnpm run generate

      - name: Build
        run: pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"

      - name: Type check
        run: pnpm run typecheck --filter "@trigger.dev/*" --filter "trigger.dev"

      - name: Publish
        id: changesets
        uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
        with:
          publish: pnpm run changeset:release
          createGithubReleases: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Show package version
        if: steps.changesets.outputs.published == 'true'
        id: get_version
        run: |
          package_version=$(echo "${STEPS_CHANGESETS_OUTPUTS_PUBLISHEDPACKAGES}" | jq -r '.[0].version')
          echo "package_version=${package_version}" >> "$GITHUB_OUTPUT"
          # Any semver with a hyphen is a prerelease (e.g. 4.5.0-rc.0, 0.0.0-snapshot-...)
          if [[ "${package_version}" == *-* ]]; then
            echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
          else
            echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
          fi
        env:
          STEPS_CHANGESETS_OUTPUTS_PUBLISHEDPACKAGES: ${{ steps.changesets.outputs.publishedPackages }}

      - name: Create unified GitHub release
        if: steps.changesets.outputs.published == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_PR_BODY: ${{ github.event.pull_request.body }}
          STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION: ${{ steps.get_version.outputs.package_version }}
          STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE: ${{ steps.get_version.outputs.is_prerelease }}
        run: |
          VERSION="${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"

          # On the pull_request merge event RELEASE_PR_BODY is the merged "Version Packages"
          # PR body, which holds the changelog. On a workflow_dispatch re-run (the recovery
          # path after a failed merge-triggered run) there is no pull_request context, so it's
          # empty. Fall back to the body of the most recently merged changeset-release/main PR
          # so the "What's changed" section is still populated.
          if [ -z "${RELEASE_PR_BODY}" ]; then
            echo "RELEASE_PR_BODY is empty (workflow_dispatch re-run); fetching merged changeset-release/main PR body"
            RELEASE_PR_BODY="$(gh pr list --repo triggerdotdev/trigger.dev \
              --head changeset-release/main --state merged \
              --json body,mergedAt --limit 10 \
              -q 'sort_by(.mergedAt) | reverse | .[0].body')"
          fi
          export RELEASE_PR_BODY

          node scripts/generate-github-release.mjs "$VERSION" > /tmp/release-body.md
          PRERELEASE_FLAG=""
          if [ "${STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE}" = "true" ]; then
            PRERELEASE_FLAG="--prerelease"
          fi
          gh release create "v${VERSION}" \
            --title "trigger.dev v${VERSION}" \
            --notes-file /tmp/release-body.md \
            --target main \
            $PRERELEASE_FLAG

      - name: Create and push Docker tag
        if: steps.changesets.outputs.published == 'true'
        run: |
          set -e
          git tag "v.docker.${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
          git push origin "v.docker.${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
        env:
          STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION: ${{ steps.get_version.outputs.package_version }}

      - name: Create and push Helm chart tag
        if: steps.changesets.outputs.published == 'true'
        run: |
          set -e
          git tag "helm-v${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
          git push origin "helm-v${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
        env:
          STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION: ${{ steps.get_version.outputs.package_version }}

  # Trigger Docker builds directly via workflow_call since tags pushed with
  # GITHUB_TOKEN don't trigger other workflows (GitHub Actions limitation).
  publish-docker:
    name: 🐳 Publish Docker images
    needs: release
    if: needs.release.outputs.published == 'true'
    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write
    uses: ./.github/workflows/publish.yml
    secrets:
      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
    with:
      image_tag: v${{ needs.release.outputs.published_package_version }}

  # Trigger Helm chart release directly via workflow_call (same GITHUB_TOKEN
  # limitation as the Docker path). Runs after Docker images are published so
  # the chart never references images that don't exist yet.
  publish-helm:
    name: 🧭 Publish Helm chart
    needs: [release, publish-docker]
    if: needs.release.outputs.published == 'true'
    permissions:
      contents: write
      packages: write
    uses: ./.github/workflows/release-helm.yml
    with:
      chart_version: ${{ needs.release.outputs.published_package_version }}

  # After Docker images are published, update the GitHub release with the exact GHCR tag URL.
  # The GHCR package version ID is only known after the image is pushed, so we query for it here.
  update-release:
    name: 🔗 Update release Docker link
    needs: [release, publish-docker]
    if: needs.release.outputs.published == 'true'
    runs-on: warp-ubuntu-latest-x64-2x
    permissions:
      contents: write
      packages: read
    steps:
      - name: Update GitHub release with Docker image link
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NEEDS_RELEASE_OUTPUTS_PUBLISHED_PACKAGE_VERSION: ${{ needs.release.outputs.published_package_version }}
        run: |
          set -e
          VERSION="${NEEDS_RELEASE_OUTPUTS_PUBLISHED_PACKAGE_VERSION}"
          TAG="v${VERSION}"

          # Query GHCR for the version ID matching this tag
          VERSION_ID=$(gh api --paginate -H "Accept: application/vnd.github+json" \
            /orgs/triggerdotdev/packages/container/trigger.dev/versions \
            --jq ".[] | select(.metadata.container.tags[] == \"${TAG}\") | .id" \
            | head -1)

          if [ -z "$VERSION_ID" ]; then
            echo "Warning: Could not find GHCR version ID for tag ${TAG}, skipping update"
            exit 0
          fi

          DOCKER_URL="https://github.com/triggerdotdev/trigger.dev/pkgs/container/trigger.dev/${VERSION_ID}?tag=${TAG}"
          GENERIC_URL="https://github.com/triggerdotdev/trigger.dev/pkgs/container/trigger.dev"

          # Get current release body and replace the generic link with the tag-specific one.
          # Use word boundary after GENERIC_URL (closing paren) to avoid matching URLs that
          # already have a version ID appended (idempotent on re-runs).
          gh release view "${TAG}" --repo triggerdotdev/trigger.dev --json body --jq '.body' > /tmp/release-body.md
          sed -i "s|${GENERIC_URL})|${DOCKER_URL})|g" /tmp/release-body.md

          gh release edit "${TAG}" --repo triggerdotdev/trigger.dev --notes-file /tmp/release-body.md

  # Dispatch changelog entry creation to the marketing site repo.
  # Runs after update-release so the GitHub release body already has the exact Docker image URL.
  dispatch-changelog:
    name: 📝 Dispatch changelog PR
    needs: [release, update-release]
    if: needs.release.outputs.published == 'true' && needs.release.outputs.is_prerelease != 'true'
    runs-on: warp-ubuntu-latest-x64-2x
    permissions: {}
    steps:
      - uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
        with:
          token: ${{ secrets.CROSS_REPO_PAT }}
          repository: triggerdotdev/trigger.dev-site-v3
          event-type: new-release
          client-payload: '{"version": "${{ needs.release.outputs.published_package_version }}"}'

  # The prerelease job needs to be on the same workflow file due to a limitation related to how npm verifies OIDC claims.
  prerelease:
    name: 🧪 Prerelease
    runs-on: ubuntu-latest
    environment: npm-publish
    permissions:
      contents: read
      id-token: write
    if: github.repository == 'triggerdotdev/trigger.dev' && github.event_name == 'workflow_dispatch' && github.event.inputs.type == 'prerelease'
    steps:
      - name: Checkout repo
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0
          ref: ${{ github.event.inputs.ref }}
          persist-credentials: false

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
        with:
          version: 10.33.2

      - name: Setup node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: 22.23.1
          cache: "pnpm"

      # npm v11.5.1 or newer is required for OIDC support
      # https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/#whats-new
      - name: Setup npm 11.x for OIDC
        run: npm install -g npm@11.6.4

      - name: Download deps
        run: pnpm install --frozen-lockfile

      - name: Generate Prisma Client
        run: pnpm run generate

      - name: Exit changeset pre mode (if active)
        run: |
          if [ -f .changeset/pre.json ]; then
            echo "Repo is in changeset pre mode; exiting so snapshot release can run"
            pnpm exec changeset pre exit
          fi

      - name: Snapshot version
        run: pnpm exec changeset version --snapshot "${GITHUB_EVENT_INPUTS_PRERELEASE_TAG}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_EVENT_INPUTS_PRERELEASE_TAG: ${{ github.event.inputs.prerelease_tag }}

      - name: Clean
        run: pnpm run clean --filter "@trigger.dev/*" --filter "trigger.dev"

      - name: Build
        run: pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"

      - name: Publish prerelease
        run: pnpm exec changeset publish --no-git-tag --snapshot --tag "${GITHUB_EVENT_INPUTS_PRERELEASE_TAG}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_EVENT_INPUTS_PRERELEASE_TAG: ${{ github.event.inputs.prerelease_tag }}
