# This workflow makes sure contributors don't forget to add a changelog entry or explicitly opt-out of it.
#
# Do not extend this workflow to include checking out the code (e.g. for building and testing purposes) while the pull_request_target trigger is used.
# Instead, see use of workflow_run in https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/

name: Changelog

on:
    # The pull_request_target trigger event allows PRs raised from forks to have write permissions and access secrets.
    # We uses it in this workflow to enable writing comments to the PR.
    pull_request_target:
        types:
            - opened
            - ready_for_review
            - reopened
            - synchronize
            - labeled
            - unlabeled

# This workflow runs for not-yet-reviewed external contributions.
# Following a pull_request_target trigger the workflow would have write permissions,
# so we intentionally restrict the permissions to only include write access on pull-requests.
permissions:
    contents: read
    pull-requests: write

jobs:
    check-changelog-entry:
        if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changelog-check') }}
        name: "Check Changelog Entry"
        runs-on: ubuntu-latest
        concurrency:
            group: changelog-${{ github.head_ref }}
            cancel-in-progress: true

        steps:
            - name: "Changed files"
              uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
              id: changelog
              with:
                  filters: |
                      changes:
                          - '.changes/*/*.yaml'
                      changelog:
                        - 'CHANGELOG.md'
                      version:
                        - 'version/VERSION'
                  list-files: json

            - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
              with:
                  sparse-checkout: |
                      version/VERSION
                      .changie.yaml
                      .changes/
                  sparse-checkout-cone-mode: false
                  ref: ${{ github.base_ref }} # Base ref refers to the target branch of this PR

            - name: "Check for changelog entry"
              uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
              with:
                  script: |
                      const fs = require("fs");
                      async function createOrUpdateChangelogComment(commentDetails, deleteComment) {
                          const commentStart = "## Changelog Warning"
                          
                          const body = commentStart + "\n\n" + commentDetails;
                          const { number: issue_number } = context.issue;
                          const { owner, repo } = context.repo;
                          
                          // List all comments
                          const allComments = (await github.rest.issues.listComments({
                              issue_number,
                              owner,
                              repo,
                          })).data;

                          const existingComment = allComments.find(c => c.body.startsWith(commentStart));
                          const comment_id = existingComment?.id;
                          
                          if (deleteComment) {
                              if (existingComment) {
                                  await github.rest.issues.deleteComment({
                                      owner,
                                      repo,
                                      comment_id,
                                  });
                              }
                              return;
                          }

                          core.setFailed(commentDetails);

                          if (existingComment) {
                              await github.rest.issues.updateComment({
                                  owner,
                                  repo,
                                  comment_id,
                                  body,
                              });
                          } else {
                              await github.rest.issues.createComment({
                                  owner,
                                  repo,
                                  issue_number,
                                  body,
                              });
                          }
                      }

                      const changesPresent = ${{steps.changelog.outputs.changes}};
                      const changedChangesFiles = ${{steps.changelog.outputs.changes_files}};
                      const changelogChangesPresent = ${{steps.changelog.outputs.changelog}};
                      const versionChangesPresent = ${{steps.changelog.outputs.version}};

                      const prLabels = await github.rest.issues.listLabelsOnIssue({
                          issue_number: context.issue.number,
                          owner: context.repo.owner,
                          repo: context.repo.repo
                      });
                      const backportLabels = prLabels.data.filter(label => label.name.endsWith("-backport"));
                      const backportVersions = backportLabels.map(label => label.name.split("-")[0]);

                      const currentVersionFile = fs.readFileSync("./version/VERSION", "utf-8");
                      const currentVersionParts = currentVersionFile.split(".");
                      currentVersionParts.pop();
                      const currentVersion = currentVersionParts.join(".");

                      const allVersions = [currentVersion, ...backportVersions]
                      allVersions.sort((a, b) => {
                        const as = a.split(".").map(Number);
                        const bs = b.split(".").map(Number);

                        if (as[0] !== bs[0]) {
                          return as[0] - bs[0];
                        }
                        
                        if (as[1] !== bs[1]) {
                          return as[1] - bs[1];
                        }
                      });

                      const noChangelogNeededLabel = prLabels.data.find(label => label.name === 'no-changelog-needed');
                      const dependenciesLabel = prLabels.data.find(label => label.name === 'dependencies');

                      // We want to prohibit contributors from directly changing the CHANGELOG.md, it's 
                      // generated so all changes will be lost during the release process.
                      // Therefore we only allow the changelog to change together with the version.
                      // In very rare cases where we generate changes in the changelog without changing the 
                      // version we will just ignore this failing check.
                      if (changelogChangesPresent && !versionChangesPresent) {
                          await createOrUpdateChangelogComment("Please don't edit the CHANGELOG.md manually. We use changie to control the Changelog generation, please use `npx changie new` to create a new changelog entry.");
                          return;
                      }

                      if (dependenciesLabel) {
                          return;
                      }

                      if (noChangelogNeededLabel) {
                          if (changesPresent) {
                              await createOrUpdateChangelogComment("Please remove either the 'no-changelog-needed' label or the changelog entry from this PR.");
                              return;
                          }
                          
                          // Nothing to complain about, so delete any existing comment
                          await createOrUpdateChangelogComment("", true);
                          return;
                      }

                      // We only want to have a changelog entry for the oldest version this PR will
                      // land in.
                      const onlyExpectedChangeVersion = allVersions[0]
                      const missingChangelogEntry = !changedChangesFiles.some(filePath => filePath.includes("/v"+onlyExpectedChangeVersion+"/"))
                      const unexpectedChangelogEntry = changedChangesFiles.filter(filePath => !filePath.includes("/v"+onlyExpectedChangeVersion+"/"))


                      if (missingChangelogEntry) {
                          await createOrUpdateChangelogComment(`Currently this PR would target a v${onlyExpectedChangeVersion} release. Please add a changelog entry for in the .changes/v${onlyExpectedChangeVersion} folder, or discuss which release you'd like to target with your reviewer. If you believe this change does not need a changelog entry, please add the 'no-changelog-needed' label.`);
                          return;
                      }

                      if (unexpectedChangelogEntry.length > 0) {
                          await createOrUpdateChangelogComment(`Please remove the changelog entry for the following paths: ${unexpectedChangelogEntry.join(", ")}. If you believe this change does not need a changelog entry, please add the 'no-changelog-needed' label.`);
                          return;
                      }

                      // Nothing to complain about, so delete any existing comment
                      await createOrUpdateChangelogComment("", true);

            - name: Validate changie fragment is valid
              uses: miniscruff/changie-action@5036dffa79ffc007110dc7f75eca7ef72780e147 # v2.1.0
              with:
                  version: latest
                  args: merge -u "." --dry-run
