name: Pull Request

on:
  pull_request_target:
    types:
      - labeled
      - unlabeled
      - opened
      - reopened
      - edited

# Only run one instance per PR to ensure in-order execution.
concurrency: pr-${{ github.ref }}

jobs:
  lint-title:
    name: Lint title
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          requireScope: false

  release-labels:
    name: Release labels
    runs-on: ubuntu-latest
    # Depend on lint so that title is Conventional Commits-compatible.
    needs: [lint-title]
    # Skip tagging for draft PRs.
    if: ${{ success() && !github.event.pull_request.draft }}
    steps:
      - uses: actions/github-script@v6
        with:
          # This script ensures PR title and labels are in sync:
          #
          # When release/breaking label is:
          # - Added, rename PR title to include ! (e.g. feat!:)
          # - Removed, rename PR title to strip ! (e.g. feat:)
          #
          # When title is:
          # - Renamed (+!), add the release/breaking label
          # - Renamed (-!), remove the release/breaking label
          script: |
            const releaseLabels = {
              breaking: "release/breaking",
            }

            const { action, changes, label, pull_request } = context.payload
            const { title } = pull_request
            const labels = pull_request.labels.map((label) => label.name)
            const isBreakingTitle = isBreaking(title)

            // Debug information.
            console.log("Action: %s", action)
            console.log("Title: %s", title)
            console.log("Labels: %s", labels.join(", "))

            const params = {
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
            }

            if (action === "opened" || action === "reopened") {
              if (isBreakingTitle && !labels.includes(releaseLabels.breaking)) {
                console.log('Add "%s" label', releaseLabels.breaking)
                await github.rest.issues.addLabels({
                  ...params,
                  labels: [releaseLabels.breaking],
                })
              }
            }

            if (action === "edited" && changes.title) {
              if (isBreakingTitle && !labels.includes(releaseLabels.breaking)) {
                console.log('Add "%s" label', releaseLabels.breaking)
                await github.rest.issues.addLabels({
                  ...params,
                  labels: [releaseLabels.breaking],
                })
              }

              if (!isBreakingTitle && labels.includes(releaseLabels.breaking)) {
                const wasBreakingTitle = isBreaking(changes.title.from)
                if (wasBreakingTitle) {
                  console.log('Remove "%s" label', releaseLabels.breaking)
                  await github.rest.issues.removeLabel({
                    ...params,
                    name: releaseLabels.breaking,
                  })
                } else {
                  console.log('Rename title from "%s" to "%s"', title, toBreaking(title))
                  await github.rest.issues.update({
                    ...params,
                    title: toBreaking(title),
                  })
                }
              }
            }

            if (action === "labeled") {
              if (label.name === releaseLabels.breaking && !isBreakingTitle) {
                console.log('Rename title from "%s" to "%s"', title, toBreaking(title))
                await github.rest.issues.update({
                  ...params,
                  title: toBreaking(title),
                })
              }
            }

            if (action === "unlabeled") {
              if (label.name === releaseLabels.breaking && isBreakingTitle) {
                console.log('Rename title from "%s" to "%s"', title, fromBreaking(title))
                await github.rest.issues.update({
                  ...params,
                  title: fromBreaking(title),
                })
              }
            }

            function isBreaking(t) {
              return t.split(" ")[0].endsWith("!:")
            }

            function toBreaking(t) {
              const parts = t.split(" ")
              return [parts[0].replace(/:$/, "!:"), ...parts.slice(1)].join(" ")
            }

            function fromBreaking(t) {
              const parts = t.split(" ")
              return [parts[0].replace(/!:$/, ":"), ...parts.slice(1)].join(" ")
            }
