Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support auto-update:opt-in label #23

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

TSMMark
Copy link

@TSMMark TSMMark commented Feb 6, 2023

Handles /issues/22


Testing now

@TSMMark
Copy link
Author

TSMMark commented Feb 6, 2023

After testing it works exactly as intended. Please consider merging and let me know if you need me to make changes

@TSMMark
Copy link
Author

TSMMark commented Feb 14, 2023

Been using this for a week and it works great! Any feedback?

@StummeJ
Copy link

StummeJ commented Mar 27, 2023

It would be helpful if you could specify the label to look for in the action config, that way if a person wants it to run on all PRs, they can leave it blank, otherwise they can specify the label in their action yml and go from there.

@TSMMark
Copy link
Author

TSMMark commented Mar 27, 2023

@StummeJ sure, feel free to implement such behavior

@StummeJ
Copy link

StummeJ commented Mar 27, 2023

I don't have the time, but just feedback if you want to see this grt mainlined

@TSMMark
Copy link
Author

TSMMark commented Mar 27, 2023

Are you a maintainer of this project? If I hear it from a maintainer I will consider it when I have time. Otherwise I'm not interested in taking guesses what the code owners want

@StummeJ
Copy link

StummeJ commented Mar 27, 2023

✌️

@ctoestreich
Copy link

ctoestreich commented Sep 7, 2023

Easy to add that label check @StummeJ. Took me like 15m

action.yml

name: Auto-update
author: Thibault Derousseaux <tibdex@gmail.com>
description: Automatically keep pull requests with auto-merged enabled up to date with their base branch.
inputs:
  github_token:
    description: Token for the GitHub API.
    default: ${{ github.token }}
    required: false
  required_label:
    description: Label required to enable auto-merge.
    default:
    required: false

runs:
  using: node16
  main: dist/index.js
branding:
  icon: refresh-cw
  color: blue
import { getInput, group, info, setFailed, warning } from "@actions/core";
import { context, getOctokit } from "@actions/github";
import type { GitHub } from "@actions/github/lib/utils.js";
import type { PaginatingEndpoints } from "@octokit/plugin-paginate-rest";
import type { PushEvent } from "@octokit/webhooks-definitions/schema.js";
import ensureError from "ensure-error";

const unupdatablePullRequestCommentBody =
  "Cannot auto-update because of conflicts.";

type PullRequest =
  PaginatingEndpoints["GET /repos/{owner}/{repo}/pulls"]["response"]["data"][number];

const handleUnupdatablePullRequest = async (
  pullRequest: PullRequest,
  {
    octokit,
  }: Readonly<{
    octokit: InstanceType<typeof GitHub>;
  }>,
): Promise<void> => {
  try {
    const {
      head: {
        repo: { full_name },
        sha,
      },
      number,
    } = pullRequest;

    const [owner, repo] = full_name.split("/");

    const {
      data: { commit: lastCommit },
    } = await octokit.request("GET /repos/{owner}/{repo}/commits/{ref}", {
      owner,
      ref: sha,
      repo,
    });

    const lastCommitter = lastCommit.committer;

    if (!lastCommitter) {
      // noinspection ExceptionCaughtLocallyJS
      throw new Error(`Missing committer on last commit ${sha}`);
    }

    const comments = await octokit.paginate(
      "GET /repos/{owner}/{repo}/issues/{issue_number}/comments",
      {
        ...context.repo,
        issue_number: number,
        since: lastCommitter.date,
      },
    );

    const existingUnupdatablePullRequestComment = comments.find(
      ({ body }) => body === unupdatablePullRequestCommentBody,
    );

    if (existingUnupdatablePullRequestComment) {
      info(
        `Already commented since last commit: ${existingUnupdatablePullRequestComment.html_url}`,
      );
      return;
    }

    const { data: newComment } = await octokit.request(
      "POST /repos/{owner}/{repo}/issues/{issue_number}/comments",
      {
        ...context.repo,
        body: unupdatablePullRequestCommentBody,
        issue_number: number,
      },
    );

    info(`Commented: ${newComment.html_url}`);
  } catch (error: unknown) {
    warning(ensureError(error));
  }
};

const handlePullRequest = async (
  pullRequest: PullRequest,
  required_label: string | undefined,
  {
    eventPayload,
    octokit,
  }: Readonly<{
    eventPayload: PushEvent;
    octokit: InstanceType<typeof GitHub>;
  }>,
): Promise<void> => {
  if (
    required_label &&
    !pullRequest.auto_merge &&
    !pullRequest.labels.some(({ name }) => name === `${required_label}`)
  ) {
    info(
      `Pull request #${pullRequest.number} does not have auto-merge enabled and does not have ${required_label} label`,
    );
    return;
  }

  if (pullRequest.base.sha === eventPayload.after) {
    info(`Pull request #${pullRequest.number} is already up to date`);
    return;
  }

  await group(
    `Attempting to update pull request #${pullRequest.number}`,
    async () => {
      try {
        await octokit.request(
          "PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch",
          {
            ...context.repo,
            pull_number: pullRequest.number,
          },
        );
        info("Updated!");
      } catch (error: unknown) {
        warning(ensureError(error));
        await handleUnupdatablePullRequest(pullRequest, { octokit });
      }
    },
  );
};

const run = async () => {
  try {
    const token = getInput("github_token", { required: true });
    const required_label = getInput("required_label", { required: false });
    const octokit = getOctokit(token);

    if (context.eventName !== "push") {
      // noinspection ExceptionCaughtLocallyJS
      throw new Error(
        `Expected to be triggered by a "push" event but received a "${context.eventName}" event`,
      );
    }

    const eventPayload = context.payload as PushEvent;
    // See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads#webhook-payload-object-34.
    const base = eventPayload.ref.slice("refs/heads/".length);

    info(`Fetching pull requests based on "${base}"`);

    const pullRequests: readonly PullRequest[] = await octokit.paginate(
      "GET /repos/{owner}/{repo}/pulls",
      {
        ...context.repo,
        base,
        state: "open",
      },
    );

    info(
      `Fetched pull requests: ${JSON.stringify(
        pullRequests.map((pullRequest) => pullRequest.number),
      )}`,
    );

    for (const pullRequest of pullRequests) {
      // PRs are handled sequentially to avoid breaking GitHub's log grouping feature.
      // eslint-disable-next-line no-await-in-loop
      await handlePullRequest(pullRequest, required_label, {
        eventPayload,
        octokit,
      });
    }
  } catch (error: unknown) {
    setFailed(ensureError(error));
  }
};

void run();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants