diff --git a/Dockerfile b/Dockerfile index 25c182d..ead0e3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,4 +33,7 @@ COPY scripts /app/ COPY requirements.txt /app/ RUN pip install -r requirements.txt +RUN chmod +x /app/entrypoint.sh + +# ENTRYPOINT ["/app/entrypoint.sh"] ENTRYPOINT ["python3","/app/alchemy.py"] diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..9147ef6 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +set -e + +echo "githubevent path: $GITHUB_EVENT_PATH" +if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(jq -r ".pull_request.number" "$GITHUB_EVENT_PATH") + if [[ "$PR_NUMBER" == "null" ]]; then + PR_NUMBER=$(jq -r ".issue.number" "$GITHUB_EVENT_PATH") + fi + if [[ "$PR_NUMBER" == "null" ]]; then + echo "Failed to determine PR Number." + exit 1 + fi +fi + +echo "Collecting information about PR #$PR_NUMBER of $GITHUB_REPOSITORY..." + +# echo all environment variables +echo "Environment variables:" +env | sort + +if [[ -z "$GITHUB_TOKEN" ]]; then + echo "Set the GITHUB_TOKEN env variable." + exit 1 +fi + +# Fetch the comment body +COMMENT_BODY=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" | \ + jq -r ".[-1].body") + +echo "Comment Body: $COMMENT_BODY" + +URI=https://api.github.com +API_HEADER="Accept: application/vnd.github.v3+json" +AUTH_HEADER="Authorization: token $GITHUB_TOKEN" + +MAX_RETRIES=${MAX_RETRIES:-6} +RETRY_INTERVAL=${RETRY_INTERVAL:-10} +REBASEABLE="" +pr_resp="" +for ((i = 0 ; i < $MAX_RETRIES ; i++)); do + pr_resp=$(curl -X GET -s -H "${AUTH_HEADER}" -H "${API_HEADER}" \ + "${URI}/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER") + echo "--> pr_resp: $pr_resp" + REBASEABLE=$(echo "$pr_resp" | jq -r .rebaseable) + if [[ "$REBASEABLE" == "null" ]]; then + echo "The PR is not ready to rebase, retry after $RETRY_INTERVAL seconds" + sleep $RETRY_INTERVAL + continue + else + break + fi +done + +if [[ "$REBASEABLE" != "true" ]] ; then + echo "GitHub doesn't think that the PR is rebaseable!" + exit 1 +fi + +BASE_REPO=$(echo "$pr_resp" | jq -r .base.repo.full_name) +BASE_BRANCH=$(echo "$pr_resp" | jq -r .base.ref) +echo "Base repo: $BASE_REPO" +echo "Base branch: $BASE_BRANCH" +USER_LOGIN=$(jq -r ".comment.user.login" "$GITHUB_EVENT_PATH") +echo "USER_LOGIN: $USER_LOGIN" + +if [[ "$USER_LOGIN" == "null" ]]; then + USER_LOGIN=$(jq -r ".pull_request.user.login" "$GITHUB_EVENT_PATH") +fi + +user_resp=$(curl -X GET -s -H "${AUTH_HEADER}" -H "${API_HEADER}" \ + "${URI}/users/${USER_LOGIN}") + +USER_NAME=$(echo "$user_resp" | jq -r ".name") +if [[ "$USER_NAME" == "null" ]]; then + USER_NAME=$USER_LOGIN +fi +echo "USER_NAME: $USER_NAME" +USER_NAME="${USER_NAME} (Rebase PR Action)" + +USER_EMAIL=$(echo "$user_resp" | jq -r ".email") +if [[ "$USER_EMAIL" == "null" ]]; then + USER_EMAIL="$USER_LOGIN@users.noreply.github.com" +fi + +echo "USER_NAME: $USER_NAME" +echo "USER_EMAIL: $USER_EMAIL" + +if [[ -z "$BASE_BRANCH" ]]; then + echo "Cannot get base branch information for PR #$PR_NUMBER!" + exit 1 +fi + +HEAD_REPO=$(echo "$pr_resp" | jq -r .head.repo.full_name) +HEAD_BRANCH=$(echo "$pr_resp" | jq -r .head.ref) + +echo "Head repo: $HEAD_REPO, HeadBranch : $HEAD_BRANCH" +echo "Base branch for PR #$PR_NUMBER is $BASE_BRANCH" + +USER_TOKEN=${USER_LOGIN//-/_}_TOKEN +UNTRIMMED_COMMITTER_TOKEN=${!USER_TOKEN:-$GITHUB_TOKEN} +COMMITTER_TOKEN="$(echo -e "${UNTRIMMED_COMMITTER_TOKEN}" | tr -d '[:space:]')" + +# See https://github.com/actions/checkout/issues/766 for motivation. +git config --global --add safe.directory /github/workspace + +git remote set-url origin https://x-access-token:$COMMITTER_TOKEN@github.com/$GITHUB_REPOSITORY.git +git config --global user.email "$USER_EMAIL" +git config --global user.name "$USER_NAME" + +git remote add fork https://x-access-token:$COMMITTER_TOKEN@github.com/$HEAD_REPO.git + +set -o xtrace + +# make sure branches are up-to-date +git fetch origin $BASE_BRANCH +git fetch fork $HEAD_BRANCH + +# do the rebase +git checkout -b fork/$HEAD_BRANCH fork/$HEAD_BRANCH +if [[ $INPUT_AUTOSQUASH == 'true' ]]; then + GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash origin/$BASE_BRANCH +else + git rebase origin/$BASE_BRANCH +fi +git status + +# push back +git push --force-with-lease fork fork/$HEAD_BRANCH:$HEAD_BRANCH \ No newline at end of file diff --git a/scripts/main.py b/scripts/main.py new file mode 100644 index 0000000..ef89486 --- /dev/null +++ b/scripts/main.py @@ -0,0 +1,165 @@ +import json +import os +import subprocess +import time + +import requests +from requests.auth import HTTPBasicAuth + + +class GitHubAdhocAction: + def __init__(self): + self.github_token = os.environ.get('GITHUB_TOKEN') + if not self.github_token: + raise ValueError("Set the GITHUB_TOKEN env variable.") + + self.github_repo = os.environ.get('GITHUB_REPOSITORY') + self.pr_number = os.environ.get('PR_NUMBER') + print(f'PR Number: {self.pr_number}') + self.comment_body = None + self.auth_header = { + 'Authorization': f'Bearer {self.github_token}', + 'Accept': 'application/vnd.github.v3+json'} + print(f"Collecting information about PR #{self.pr_number} of {self.github_repo} ...") + self.extract_info_from_events() + print(f'-->PR Number: {self.pr_number}') + + def extract_info_from_events(self): + github_event_path = os.environ.get('GITHUB_EVENT_PATH') + if github_event_path: + with open(github_event_path, 'r') as file: + event_data = json.load(file) + if self.pr_number is None: + self.get_pr_number_from_event(event_data) + self.user_login = self.fetch_user_login_from_events(event_data) + + print("Failed to determine PR Number and user_login.") + exit(1) + + def get_pr_number_from_event(self, event_data): + self.pr_number = event_data.get("pull_request", {}).get("number") + if self.pr_number is None: + self.pr_number = event_data.get("issue", {}).get("number") + if self.pr_number is None: + raise Exception("Failed to determine PR Number.") + return self.pr_number + + def fetch_user_login_from_events(self, event_data): + user_login = event_data.get("comment", {}).get("user", {}).get("login") + if user_login is None: + user_login = event_data.get("pull_request", {}).get("user", {}).get("login") + return user_login + else: + return user_login + + def fetch_comment_body(self): + url = f'https://api.github.com/repos/{self.github_repo}/issues/{self.pr_number}/comments' + headers = {'Authorization': f'Bearer {self.github_token}'} + response = requests.get(url, headers=headers) + response.raise_for_status() + comments = response.json() + self.comment_body = comments[-1]['body'] + print(f'Comment body: {self.comment_body}') + return self.comment_body + + def rebase_pr(self): + uri = 'https://api.github.com' + max_retries = 6 + retry_interval = 10 + rebaseable = None + + for _ in range(max_retries): + pr_resp = requests.get( + f'{uri}/repos/{self.github_repo}/pulls/{self.pr_number}', + headers=self.auth_header + ) + pr_resp.raise_for_status() + rebaseable = pr_resp.json().get('rebaseable') + + if rebaseable is None: + print(f'The PR is not ready to rebase, retry after {retry_interval} seconds') + time.sleep(retry_interval) + continue + else: + break + + if rebaseable is not True: + print('GitHub doesn\'t think that the PR is rebaseable!') + exit(1) + + self.configure_git() + + head_repo, _ = self.fetch_head_repo_and_branch() + subprocess.run(['git', 'remote', 'add', 'fork', f'https://x-access-token:{self.committer_token}@github.com/{head_repo}.git']) + + self.perform_rebase() + + def configure_git(self): + user_resp = requests.get( + f'https://api.github.com/users/{self.user_login}', + headers=self.auth_header + ) + user_resp.raise_for_status() + user_data = user_resp.json() + + self.user_name = user_data.get('name') or self.user_login + self.user_email = user_data.get('email') or f'{self.user_login}@users.noreply.github.com' + + self.user_token = os.environ.get(f'{self.user_login.upper()}_TOKEN', self.github_token) + self.committer_token = self.user_token.replace(' ', '') + + subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', '/github/workspace']) + subprocess.run(['git', 'remote', 'set-url', 'origin', f'https://x-access-token:{self.committer_token}@github.com/{self.github_repo}.git']) + subprocess.run(['git', 'config', '--global', 'user.email', self.user_email]) + subprocess.run(['git', 'config', '--global', 'user.name', f'{self.user_name}']) + + def fetch_base_repo(self): + pr_resp = self.fetch_pr_info() + return pr_resp.json().get('base', {}).get('repo', {}).get('full_name') + + def fetch_user_login(self): + user_login = self.comment_body.get('comment', {}).get('user', {}).get('login') + if user_login is None: + pr_resp = self.fetch_pr_info() + user_login = pr_resp.json().get('pull_request', {}).get('user', {}).get('login') + return user_login + + def fetch_base_branch(self): + pr_resp = self.fetch_pr_info() + return pr_resp.json().get('base', {}).get('ref') + + def fetch_head_repo_and_branch(self): + pr_resp = self.fetch_pr_info() + head_repo = pr_resp.json().get('head', {}).get('repo', {}).get('full_name') + head_branch = pr_resp.json().get('head', {}).get('ref') + return head_repo, head_branch + + def fetch_pr_info(self): + uri = f'https://api.github.com/repos/{self.github_repo}/pulls/{self.pr_number}' + + pr_resp = requests.get(uri, headers=self.auth_header) + pr_resp.raise_for_status() + return pr_resp + + def perform_rebase(self): + base_branch = self.fetch_base_branch() + head_branch = self.fetch_head_repo_and_branch()[1] + + subprocess.run(['git', 'fetch', 'origin', base_branch]) + subprocess.run(['git', 'fetch', 'fork', head_branch]) + + subprocess.run(['git', 'checkout', '-b', f'fork/{head_branch}', f'fork/{head_branch}']) + + if os.environ.get('INPUT_AUTOSQUASH') == 'true': + subprocess.run(['git', 'rebase', '-i', '--autosquash', f'origin/{base_branch}']) + else: + subprocess.run(['git', 'rebase', f'origin/{base_branch}']) + + subprocess.run(['git', 'status']) + subprocess.run(['git', 'push', '--force-with-lease', 'fork', f'fork/{head_branch}:{head_branch}']) + + +if __name__ == '__main__': + adhoc_action = GitHubAdhocAction() + comment_body = adhoc_action.fetch_comment_body() + adhoc_action.rebase_pr()