diff --git a/.github/workflows/port-issue.yaml b/.github/workflows/port-issue.yaml new file mode 100644 index 0000000..4c0e678 --- /dev/null +++ b/.github/workflows/port-issue.yaml @@ -0,0 +1,119 @@ +# create a backport/forwardport of an issue when "/backport " is commented +name: Port issue +run-name: "Port issue ${{ github.event.issue.number }}: ${{ github.event.issue.title }}" + +on: + issue_comment: + types: + - created + +jobs: + port-issue: + permissions: + issues: write + runs-on: ubuntu-latest + if: ${{ !github.event.issue.pull_request && (contains(github.event.comment.body, '/backport') || contains(github.event.comment.body, '/forwardport')) }} + steps: + - name: Check org membership or repo ownership + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if the repository owner is an organization + is_org=$(gh api users/${GITHUB_REPOSITORY_OWNER} | jq -r '.type == "Organization"') + + if [[ "$is_org" == "true" ]]; then + # Check if the actor is a member of the organization + # User's membership must be set public + if gh api orgs/${GITHUB_REPOSITORY_OWNER}/members --paginate | jq -e --arg GITHUB_ACTOR "$GITHUB_ACTOR" '.[] | select(.login == $GITHUB_ACTOR)' > /dev/null; then + echo "${GITHUB_ACTOR} is a member" + echo "is_member=true" >> $GITHUB_ENV + else + echo "${GITHUB_ACTOR} is not a member of ${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_STEP_SUMMARY + echo "is_member=false" >> $GITHUB_ENV + fi + else + # If the owner is not an organization, treat it as an individual repo + if [[ "$GITHUB_REPOSITORY_OWNER" == "$GITHUB_ACTOR" ]]; then + echo "${GITHUB_ACTOR} is the repository owner" + echo "is_member=true" >> $GITHUB_ENV + else + echo "${GITHUB_ACTOR} is not the repository owner" >> $GITHUB_STEP_SUMMARY + echo "is_member=false" >> $GITHUB_ENV + fi + fi + - name: Check milestone + if: ${{ env.is_member == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORIGINAL_ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + BODY_MILESTONE=$(echo "${COMMENT_BODY}" | awk '{ print $2 }') + echo "BODY_MILESTONE '${BODY_MILESTONE}'" + + # Sanitize input + MILESTONE=${BODY_MILESTONE//[^a-zA-Z0-9\-\.]/} + echo "MILESTONE '${MILESTONE}'" + + if gh api repos/${GITHUB_REPOSITORY}/milestones --paginate | jq -e --arg MILESTONE "$MILESTONE" '.[] | select(.title == $MILESTONE)' > /dev/null; then + echo "Milestone '${MILESTONE}' exists" + echo "milestone_exists=true" >> $GITHUB_ENV + else + echo "Milestone '${MILESTONE}' does not exist" >> $GITHUB_STEP_SUMMARY + gh issue comment -R ${GITHUB_REPOSITORY} ${ORIGINAL_ISSUE_NUMBER} --body "Not creating port issue, milestone ${MILESTONE} does not exist or is not an open milestone" + echo "milestone_exists=false" >> $GITHUB_ENV + fi + - name: Port issue + if: ${{ env.is_member == 'true' && env.milestone_exists == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORIGINAL_ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + declare -a additional_cmd + BODY=$(mktemp) + ORIGINAL_ISSUE=$(gh issue view -R ${GITHUB_REPOSITORY} ${ORIGINAL_ISSUE_NUMBER} --json title,body,assignees) + ORIGINAL_TITLE=$(echo "${ORIGINAL_ISSUE}" | jq -r .title) + TYPE=$(echo "${COMMENT_BODY}" | awk '{ print $1 }' | sed -e 's_/__') + MILESTONE=$(echo "${COMMENT_BODY}" | awk '{ print $2 }') + NEW_TITLE="[${TYPE}] ${ORIGINAL_TITLE}" + if [[ $MILESTONE =~ (v[0-9]\.[0-9]+) ]]; then + NEW_TITLE="[${TYPE} ${MILESTONE}] ${ORIGINAL_TITLE}" + fi + additional_cmd+=("--label") + additional_cmd+=("QA/None") + ORIGINAL_LABELS=$(gh issue view -R ${GITHUB_REPOSITORY} ${ORIGINAL_ISSUE_NUMBER} --json labels --jq '.labels[].name' | grep -v '^\[zube\]:' | paste -sd "," -) + if [ -n "$ORIGINAL_LABELS" ]; then + additional_cmd+=("--label") + additional_cmd+=("${ORIGINAL_LABELS}") + fi + ORIGINAL_PROJECT=$(gh issue view -R ${GITHUB_REPOSITORY} ${ORIGINAL_ISSUE_NUMBER} --json projectItems --jq '.projectItems[].title') + if [ -n "$ORIGINAL_PROJECT" ]; then + additional_cmd+=("--project") + additional_cmd+=("${ORIGINAL_PROJECT}") + fi + ASSIGNEES=$(echo "${ORIGINAL_ISSUE}" | jq -r .assignees[].login) + if [ -n "$ASSIGNEES" ]; then + echo "Checking if assignee is member before assigning" + DELIMITER="" + NEW_ASSIGNEES="" + for ASSIGNEE in $ASSIGNEES; do + if gh api orgs/${GITHUB_REPOSITORY_OWNER}/members --paginate | jq -e --arg GITHUB_ACTOR "$GITHUB_ACTOR" '.[] | select(.login == $GITHUB_ACTOR)' > /dev/null; then + echo "${ASSIGNEE} is a member, adding to assignees" + NEW_ASSIGNEES="${NEW_ASSIGNEES}${DELIMITER}${ASSIGNEE}" + DELIMITER="," + fi + done + if [ -n "$NEW_ASSIGNEES" ]; then + echo "Assignees for new issue: ${NEW_ASSIGNEES}" + additional_cmd+=("--assignee") + additional_cmd+=("${NEW_ASSIGNEES}") + fi + fi + if [ -n "$MILESTONE" ]; then + echo -e "This is a ${TYPE} issue for #${ORIGINAL_ISSUE_NUMBER}, automatically created via [GitHub Actions workflow]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) initiated by @${GITHUB_ACTOR}\n" > $BODY + echo -e "\nOriginal issue body:\n" >> $BODY + echo "${ORIGINAL_ISSUE}" | jq -r '.body[0:65536]' >> $BODY + NEW_ISSUE=$(gh issue create -R "${GITHUB_REPOSITORY}" --title "${NEW_TITLE}" --body-file "${BODY}" -m "${MILESTONE}" "${additional_cmd[@]}") + echo "Port issue created: ${NEW_ISSUE}" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.github/workflows/port-pr.yaml b/.github/workflows/port-pr.yaml new file mode 100644 index 0000000..45824c1 --- /dev/null +++ b/.github/workflows/port-pr.yaml @@ -0,0 +1,143 @@ +# create a backport/forwardport of a PR when "/backport " is commented +name: Port PR +run-name: "Port PR ${{ github.event.issue.number }}: ${{ github.event.issue.title }}" + +on: + issue_comment: + types: + - created + +env: + ORIGINAL_ISSUE_NUMBER: ${{ github.event.issue.number }} + +jobs: + port-pr: + permissions: + pull-requests: write + runs-on: ubuntu-latest + if: ${{ github.event.issue.pull_request && (startsWith(github.event.comment.body, '/backport') || startsWith(github.event.comment.body, '/forwardport')) }} + steps: + - name: Check org membership or repo ownership + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if the repository owner is an organization + is_org=$(gh api users/${GITHUB_REPOSITORY_OWNER} | jq -r '.type == "Organization"') + + if [[ "$is_org" == "true" ]]; then + # Check if the actor is a member of the organization + # User's membership must be set public + if gh api orgs/${GITHUB_REPOSITORY_OWNER}/members --paginate | jq -e --arg GITHUB_ACTOR "$GITHUB_ACTOR" '.[] | select(.login == $GITHUB_ACTOR)' > /dev/null; then + echo "${GITHUB_ACTOR} is a member" + echo "is_member=true" >> $GITHUB_ENV + else + echo "${GITHUB_ACTOR} is not a member of ${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_STEP_SUMMARY + echo "is_member=false" >> $GITHUB_ENV + fi + else + # If the owner is not an organization, treat it as an individual repo + if [[ "$GITHUB_REPOSITORY_OWNER" == "$GITHUB_ACTOR" ]]; then + echo "${GITHUB_ACTOR} is the repository owner" + echo "is_member=true" >> $GITHUB_ENV + else + echo "${GITHUB_ACTOR} is not the repository owner" >> $GITHUB_STEP_SUMMARY + echo "is_member=false" >> $GITHUB_ENV + fi + fi + - name: Check milestone + if: ${{ env.is_member == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + BODY_MILESTONE=$(echo "${COMMENT_BODY}" | awk '{ print $2 }') + # Sanitize input + MILESTONE=${BODY_MILESTONE//[^a-zA-Z0-9\-\.]/} + if gh api repos/${GITHUB_REPOSITORY}/milestones --paginate | jq -e --arg MILESTONE "$MILESTONE" '.[] | select(.title == $MILESTONE)' > /dev/null; then + echo "Milestone ${MILESTONE} exists" >> $GITHUB_STEP_SUMMARY + echo "milestone_exists=true" >> $GITHUB_ENV + echo "milestone=${MILESTONE}" >> $GITHUB_ENV + else + echo "Milestone ${MILESTONE} does not exist" >> $GITHUB_STEP_SUMMARY + echo "milestone_exists=false" >> $GITHUB_ENV + fi + - name: Get target branch + if: ${{ env.is_member == 'true' }} + env: + COMMENT_BODY: ${{ github.event.comment.body }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TYPE=$(echo "${COMMENT_BODY}" | awk '{ print $1 }' | sed -e 's_/__') + echo "Type: ${TYPE}" >> $GITHUB_STEP_SUMMARY + echo "type=${TYPE}" >> $GITHUB_ENV + TARGET_BRANCH=$(echo "${COMMENT_BODY}" | awk '{ print $3 }') + echo "Target branch: ${TARGET_BRANCH}" >> $GITHUB_STEP_SUMMARY + echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV + if gh api repos/${GITHUB_REPOSITORY}/branches --paginate | jq -e --arg TARGET_BRANCH "$TARGET_BRANCH" '.[] | select(.name == $TARGET_BRANCH)' > /dev/null; then + echo "target_branch_exists=true" >> $GITHUB_ENV + else + gh issue comment -R ${GITHUB_REPOSITORY} ${ORIGINAL_ISSUE_NUMBER} --body "Not creating port issue, target ${TARGET_BRANCH} does not exist" + echo "target_branch_exists=false" >> $GITHUB_ENV + fi + - name: Checkout + if: ${{ env.is_member == 'true' && env.target_branch_exists == 'true' }} + uses: actions/checkout@v4 + with: + ref: ${{ env.target_branch }} + fetch-depth: '0' + token: ${{ secrets.GITHUB_TOKEN }} + - name: Port PR + if: ${{ env.is_member == 'true' && env.target_branch_exists == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TYPE: ${{ env.type }} + TARGET_BRANCH: ${{ env.target_branch }} + MILESTONE: ${{ env.milestone }} + run: | + PATCH_FILE=$(mktemp) + gh pr diff $ORIGINAL_ISSUE_NUMBER --patch > $PATCH_FILE + BRANCH="gha-portpr-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + echo "branch=${BRANCH}" >> $GITHUB_ENV + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "Rancher PR Port Bot" + git checkout -b $BRANCH + + if ! git am -3 "$PATCH_FILE" > error.log 2>&1; then + ERROR_MESSAGE=$(cat error.log) + FORMATTED_ERROR_MESSAGE=$(printf "\n\`\`\`\n%s\n\`\`\`" "$ERROR_MESSAGE") + gh issue comment ${ORIGINAL_ISSUE_NUMBER} --body "Not creating port PR, there was an error running git am -3: $FORMATTED_ERROR_MESSAGE" + echo "Port PR not created." >> $GITHUB_STEP_SUMMARY + else + git push origin $BRANCH + ORIGINAL_PR=$(gh pr view ${ORIGINAL_ISSUE_NUMBER} --json title,body,assignees) + ORIGINAL_TITLE=$(echo "${ORIGINAL_PR}" | jq -r .title) + ORIGINAL_ASSIGNEE=$(echo "${ORIGINAL_PR}" | jq -r '.assignee.login // empty') + BODY=$(mktemp) + echo -e "This is an automated request to port PR #${ORIGINAL_ISSUE_NUMBER} by @${GITHUB_ACTOR}\n\n" > $BODY + echo -e "Original PR body:\n\n" >> $BODY + echo "${ORIGINAL_PR}" | jq -r .body >> $BODY + ASSIGNEES=$(echo "${ORIGINAL_PR}" | jq -r .assignees[].login) + if [ -n "$ASSIGNEES" ]; then + echo "Checking if assignee is member before assigning" + DELIMITER="" + NEW_ASSIGNEES="" + for ASSIGNEE in $ASSIGNEES; do + if gh api orgs/${GITHUB_REPOSITORY_OWNER}/members --paginate | jq -e --arg GITHUB_ACTOR "$GITHUB_ACTOR" '.[] | select(.login == $GITHUB_ACTOR)' > /dev/null; then + echo "${ASSIGNEE} is a member, adding to assignees" + NEW_ASSIGNEES="${NEW_ASSIGNEES}${DELIMITER}${ASSIGNEE}" + DELIMITER="," + fi + done + if [ -n "$NEW_ASSIGNEES" ]; then + echo "Assignees for new issue: ${NEW_ASSIGNEES}" + additional_cmd+=("--assignee") + additional_cmd+=("${NEW_ASSIGNEES}") + fi + fi + NAMED_TARGET=${MILESTONE} + if [ -z "${MILESTONE}" ]; then + NAMED_TARGET="${TARGET_BRANCH}" + fi + NEW_PR=$(gh pr create --title="[${TYPE} ${NAMED_TARGET}] ${ORIGINAL_TITLE}" --body-file="${BODY}" --head "${BRANCH}" --base "${TARGET_BRANCH}" --milestone "${MILESTONE}" "${additional_cmd[@]}") + echo "Port PR created: ${NEW_PR}" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file