PR Commands: Authenticate using civibot App Token if configured #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Commands | ||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
| jobs: | ||
| pr_commands: | ||
| name: Execute PR Command | ||
| if: | | ||
| github.event.issue.pull_request != null && | ||
| (startsWith(github.event.comment.body, '/squash') || startsWith(github.event.comment.body, '/rebase') || startsWith(github.event.comment.body, '/lintroll') || startsWith(github.event.comment.body, '/port') || startsWith(github.event.comment.body, '/SQUASH') || startsWith(github.event.comment.body, '/REBASE') || startsWith(github.event.comment.body, '/LINTROLL') || startsWith(github.event.comment.body, '/PORT')) | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| pull-requests: write | ||
| issues: write | ||
| contents: write | ||
| steps: | ||
| - name: Check Authorization | ||
| id: auth | ||
| run: | | ||
| COMMENT_AUTHOR="${{ github.event.comment.user.login }}" | ||
| PR_AUTHOR="${{ github.event.issue.user.login }}" | ||
| AUTHOR_ASSOCIATION="${{ github.event.comment.author_association }}" | ||
| IS_AUTHORIZED=false | ||
| if [ "$COMMENT_AUTHOR" = "$PR_AUTHOR" ]; then | ||
| IS_AUTHORIZED=true | ||
| elif [[ "$AUTHOR_ASSOCIATION" =~ ^(OWNER|MEMBER|COLLABORATOR)$ ]]; then | ||
| IS_AUTHORIZED=true | ||
| fi | ||
| if [ "$IS_AUTHORIZED" = "false" ]; then | ||
| echo "Error: User $COMMENT_AUTHOR is not authorized to run commands on this PR." | ||
| exit 1 | ||
| fi | ||
| - name: Generate GitHub App Token | ||
| id: generate-token | ||
| if: ${{ secrets.CIVIBOT_APP_ID != '' }} | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ secrets.CIVIBOT_APP_ID }} | ||
| private-key: ${{ secrets.CIVIBOT_PRIVATE_KEY }} | ||
| - name: Add Eyes Reaction | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh api \ | ||
| --method POST \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| -H "X-GitHub-Api-Version: 2022-11-28" \ | ||
| /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \ | ||
| -f content='eyes' | ||
| - name: Fetch PR Details | ||
| id: pr_info | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| PR_JSON=$(gh pr view ${{ github.event.issue.number }} --repo ${{ github.repository }} --json headRefName,headRepository,headRepositoryOwner,baseRefName,state,mergeCommit) | ||
| STATE=$(echo "$PR_JSON" | jq -r '.state') | ||
| MERGE_COMMIT=$(echo "$PR_JSON" | jq -r '.mergeCommit.oid // empty') | ||
| HEAD_REF=$(echo "$PR_JSON" | jq -r '.headRefName') | ||
| HEAD_OWNER=$(echo "$PR_JSON" | jq -r '.headRepositoryOwner.login // empty') | ||
| HEAD_REPO=$(echo "$PR_JSON" | jq -r '.headRepository.name // empty') | ||
| BASE_REF=$(echo "$PR_JSON" | jq -r '.baseRefName') | ||
| echo "state=$STATE" >> $GITHUB_OUTPUT | ||
| echo "merge_commit=$MERGE_COMMIT" >> $GITHUB_OUTPUT | ||
| echo "head_ref=$HEAD_REF" >> $GITHUB_OUTPUT | ||
| echo "head_owner=$HEAD_OWNER" >> $GITHUB_OUTPUT | ||
| echo "head_repo=$HEAD_REPO" >> $GITHUB_OUTPUT | ||
| echo "base_ref=$BASE_REF" >> $GITHUB_OUTPUT | ||
| - name: Checkout upstream | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| - name: Parse and Execute Command | ||
| id: exec | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| # Configure Git | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| STATE="${{ steps.pr_info.outputs.state }}" | ||
| MERGE_COMMIT="${{ steps.pr_info.outputs.merge_commit }}" | ||
| if [ "$STATE" = "MERGED" ]; then | ||
| git fetch origin pull/${{ github.event.issue.number }}/head | ||
| git checkout -b pr-branch FETCH_HEAD | ||
| else | ||
| # Connect remote head_repo | ||
| git remote add head_repo "https://x-access-token:${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }}@github.com/${{ steps.pr_info.outputs.head_owner }}/${{ steps.pr_info.outputs.head_repo }}.git" | ||
| git fetch head_repo ${{ steps.pr_info.outputs.head_ref }} | ||
| git checkout -b pr-branch head_repo/${{ steps.pr_info.outputs.head_ref }} | ||
| fi | ||
| COMMENT_BODY="${{ github.event.comment.body }}" | ||
| CMD=$(echo "$COMMENT_BODY" | head -n 1 | xargs | tr '[:upper:]' '[:lower:]') | ||
| if [ "$STATE" = "MERGED" ] && [[ ! "$CMD" =~ ^/port ]]; then | ||
| echo "Error: Only /port command is allowed on merged pull requests." | ||
| exit 1 | ||
| fi | ||
| if [[ "$CMD" =~ ^/squash ]]; then | ||
| echo "Running squash..." | ||
| base_branch="${{ steps.pr_info.outputs.base_ref }}" | ||
| head_branch="${{ steps.pr_info.outputs.head_ref }}" | ||
| git fetch origin $base_branch | ||
| merge_base=$(git merge-base origin/$base_branch HEAD) | ||
| first_commit=$(git log --reverse --format="%H" origin/$base_branch..HEAD | head -n 1) | ||
| if [ -z "$first_commit" ]; then | ||
| echo "No commits found to squash." | ||
| exit 1 | ||
| fi | ||
| commit_count=$(git log --oneline origin/$base_branch..HEAD | wc -l) | ||
| if [ "$commit_count" -le 1 ]; then | ||
| echo "Only one commit found. Nothing to squash." | ||
| exit 0 | ||
| fi | ||
| git reset --soft $merge_base | ||
| git commit --reuse-message=$first_commit | ||
| git push --force-with-lease head_repo HEAD:$head_branch | ||
| elif [[ "$CMD" =~ ^/(rebase|port)[[:space:]]+([^[:space:]]+) ]]; then | ||
| ACTION="${BASH_REMATCH[1]}" | ||
| NEW_BASE="${BASH_REMATCH[2]}" | ||
| echo "TARGET_BASE=$NEW_BASE" >> $GITHUB_ENV | ||
| echo "Running $ACTION to $NEW_BASE..." | ||
| base_branch="${{ steps.pr_info.outputs.base_ref }}" | ||
| head_branch="${{ steps.pr_info.outputs.head_ref }}" | ||
| git fetch origin $base_branch | ||
| git fetch origin $NEW_BASE | ||
| # Determine the upstream commit for rebase | ||
| if [ "$STATE" = "MERGED" ]; then | ||
| # Check if the PR head is already an ancestor of the base branch (standard merge) | ||
| if git merge-base --is-ancestor HEAD origin/$base_branch; then | ||
| UPSTREAM="$MERGE_COMMIT^1" | ||
| else | ||
| UPSTREAM=$(git merge-base origin/$base_branch HEAD) | ||
| fi | ||
| else | ||
| UPSTREAM="origin/$base_branch" | ||
| fi | ||
| if [ "$ACTION" = "rebase" ] && git merge-base --is-ancestor origin/$NEW_BASE HEAD; then | ||
| echo "Branch is already fast-forward mergeable with $NEW_BASE. Changing PR base branch..." | ||
| gh pr edit ${{ github.event.issue.number }} --base $NEW_BASE --repo ${{ github.repository }} | ||
| else | ||
| if ! git rebase --onto origin/$NEW_BASE $UPSTREAM HEAD; then | ||
| echo "REBASE_FAILED=true" >> $GITHUB_ENV | ||
| CONFLICTS=$(git diff --name-only --diff-filter=U) | ||
| echo "CONFLICTS<<EOF" >> $GITHUB_ENV | ||
| echo "$CONFLICTS" >> $GITHUB_ENV | ||
| echo "EOF" >> $GITHUB_ENV | ||
| git rebase --abort | ||
| exit 1 | ||
| fi | ||
| if [ "$ACTION" = "rebase" ]; then | ||
| git push --force-with-lease head_repo HEAD:$head_branch | ||
| gh pr edit ${{ github.event.issue.number }} --base $NEW_BASE --repo ${{ github.repository }} | ||
| else | ||
| PR_TITLE=$(gh pr view ${{ github.event.issue.number }} --repo ${{ github.repository }} --json title --jq '.title') | ||
| PORT_BRANCH="port-${{ github.event.issue.number }}-$NEW_BASE" | ||
| git push --force origin HEAD:refs/heads/$PORT_BRANCH | ||
| EXISTING_PR=$(gh pr list --repo ${{ github.repository }} --head "$PORT_BRANCH" --base "$NEW_BASE" --json url --jq '.[0].url') | ||
| if [ -n "$EXISTING_PR" ]; then | ||
| NEW_PR_URL="$EXISTING_PR" | ||
| COMMENT_BODY="\`$NEW_BASE\` port $NEW_PR_URL has been updated to match the latest changes in this PR." | ||
| else | ||
| NEW_PR_URL=$(gh pr create \ | ||
| --repo ${{ github.repository }} \ | ||
| --base "$NEW_BASE" \ | ||
| --head "$PORT_BRANCH" \ | ||
| --title "Port: \"$PR_TITLE\" to $NEW_BASE" \ | ||
| --body "Port of #${{ github.event.issue.number }} to $NEW_BASE." \ | ||
| --label "port") | ||
| printf -v COMMENT_BODY "\`$NEW_BASE\` port: $NEW_PR_URL\n\nIf this PR changes, you can update the port by running \`/port $NEW_BASE\` again." | ||
| fi | ||
| gh pr comment ${{ github.event.issue.number }} --repo ${{ github.repository }} --body "$COMMENT_BODY" | ||
| fi | ||
| fi | ||
| elif [[ "$CMD" =~ ^/lintroll ]]; then | ||
| echo "Running lintroll..." | ||
| base_branch="${{ steps.pr_info.outputs.base_ref }}" | ||
| head_branch="${{ steps.pr_info.outputs.head_ref }}" | ||
| git fetch origin $base_branch | ||
| # Switch PHP version to match composer.json if pre-installed | ||
| PHP_VERSION=$(jq -r '.config.platform.php // "8.1"' composer.json | cut -d. -f1-2) | ||
| if [ -x "/usr/bin/php$PHP_VERSION" ]; then | ||
| echo "Switching PHP to version $PHP_VERSION..." | ||
| sudo update-alternatives --set php /usr/bin/php$PHP_VERSION | ||
| else | ||
| echo "PHP version $PHP_VERSION is not pre-installed. Using default: $(php -v | head -n 1)" | ||
| fi | ||
| # Setup buildkit/civilint | ||
| echo "Cloning civicrm-buildkit..." | ||
| git clone -b master https://github.com/civicrm/civicrm-buildkit.git $GITHUB_WORKSPACE/civicrm-buildkit | ||
| cd $GITHUB_WORKSPACE/civicrm-buildkit | ||
| # Prune composer.json to only keep phpcs and coder | ||
| jq '.require = {"drupal/coder": .require["drupal/coder"], "squizlabs/php_codesniffer": .require["squizlabs/php_codesniffer"]}' composer.json > composer.json.tmp && mv composer.json.tmp composer.json | ||
| composer install --no-interaction --no-plugins --no-scripts | ||
| npm install jshint | ||
| BUILDKIT_DIR=$GITHUB_WORKSPACE/civicrm-buildkit | ||
| cd $GITHUB_WORKSPACE | ||
| EXEC_CMD='files=$(git diff-tree --no-commit-id --name-only -r HEAD); if [ -n "$files" ]; then echo "$files" | '"$BUILDKIT_DIR"'/bin/civilint --fix -; fi; git diff --quiet || git commit -a --amend --no-edit' | ||
| if ! git rebase --exec "$EXEC_CMD" origin/$base_branch; then | ||
| echo "LINTROLL_FAILED=true" >> $GITHUB_ENV | ||
| git rebase --abort | ||
| exit 1 | ||
| fi | ||
| git push --force-with-lease head_repo HEAD:$head_branch | ||
| else | ||
| echo "Unknown command: $CMD" | ||
| exit 1 | ||
| fi | ||
| - name: Add Success Reaction | ||
| if: success() | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh api \ | ||
| --method POST \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| -H "X-GitHub-Api-Version: 2022-11-28" \ | ||
| /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \ | ||
| -f content='+1' | ||
| - name: Add Failure Reaction and Comment | ||
| if: failure() | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| # 1. Add reaction | ||
| gh api \ | ||
| --method POST \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| -H "X-GitHub-Api-Version: 2022-11-28" \ | ||
| /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \ | ||
| -f content='confused' | ||
| # 2. Compile failure message | ||
| if [ "${{ env.REBASE_FAILED }}" = "true" ]; then | ||
| printf -v MSG "### ❌ Rebase Failed due to Conflicts\n\nThe rebase onto \`%s\` failed due to merge conflicts. The following files have conflicts:\n\`\`\`\n%s\n\`\`\`\n\nPlease resolve these conflicts locally and force-push your changes:\n\`\`\`bash\n# Fetch latest branches\ngit fetch origin\n# Rebase locally\ngit rebase --onto origin/%s origin/%s\n# Resolve conflicts in your editor, then run:\ngit add <conflicted-files>\ngit rebase --continue\n# Force push to your branch\ngit push --force-with-lease\n\`\`\`" "${{ env.TARGET_BASE }}" "${{ env.CONFLICTS }}" "${{ env.TARGET_BASE }}" "${{ steps.pr_info.outputs.base_ref }}" | ||
| elif [ "${{ env.LINTROLL_FAILED }}" = "true" ]; then | ||
| MSG="### ❌ Lintroll Failed due to Conflicts"$'\n\n'"The automated formatting rebase failed due to merge conflicts. Please run \`civilint --fix\` and resolve any conflicts locally, then force-push your changes." | ||
| else | ||
| MSG="### ❌ Command Execution Failed"$'\n\n'"The workflow failed to execute the command. Please check the action run logs for details." | ||
| fi | ||
| # 3. Post comment | ||
| gh pr comment ${{ github.event.issue.number }} --repo ${{ github.repository }} --body "$MSG" | ||