[
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 7\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n# Label to use when marking an issue as stale\nstaleLabel: wontfix\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# CHANGELOG\n\nThis is a manually generated log to track changes to the repository for each release. \nEach section should include general headers such as **Implemented enhancements** \nand **Merged pull requests**. All closed issued and bug fixes should be \nrepresented by the pull requests that fixed them. Critical items to know are:\n\n - renamed commands\n - deprecated / removed commands\n - changed defaults\n - backward incompatible changes\n\n\nVersions correspond with GitHub releases that can be referenced with @ using actions.\n\n## [master](https://github.com/vsoch/pull-request-action/tree/master) (master)\n - alpine cannot install to system python anymore (1.1.0)\n - bugfix of missing output values (1.0.23)\n - bugfix of token handling if 401 error received (missing 401 case) (1.0.21)\n - bugfix of writing to environment file (missing newline) (1.0.19)\n - bugfix of missing from branch with scheduled run (1.0.16)\n - forgot to add assignees (1.0.15)\n - output and environment variables for PR number and return codes (1.0.5)\n - added support for reviewer (individual and team) assignments (1.0.4)\n - added support for maintainer can modify and assignees (1.0.3)\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine\n\n# docker build -t vanessa/pull-request-action .\n\nLABEL \"com.github.actions.name\"=\"Pull Request on Branch Push\"\nLABEL \"com.github.actions.description\"=\"Create a pull request when a branch is created or updated\"\nLABEL \"com.github.actions.icon\"=\"activity\"\nLABEL \"com.github.actions.color\"=\"yellow\"\n\n# Newer alpine we are not allowed to install to system python\nRUN apk --no-cache add python3 py3-pip py3-virtualenv git bash && \\\n    python3 -m venv /opt/env && \\\n    /opt/env/bin/pip3 install --break-system-packages requests\nCOPY pull-request.py /pull-request.py\n\nRUN chmod u+x /pull-request.py\nENTRYPOINT [\"/opt/env/bin/python3\", \"/pull-request.py\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019-2023 Vanessa Sochat\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Automated Branch Pull Requests\n\nThis action will open a pull request to master branch (or otherwise specified)\nwhenever a branch with some prefix is pushed to. The idea is that you can\nset up some workflow that pushes content to branches of the repostory,\nand you would then want this push reviewed for merge to master.\n\nHere is an example of what to put in your `.github/workflows/pull-request.yml` file to\ntrigger the action.\n\n```yaml\nname: Pull Request on Branch Push\non:\n  push:\n    branches-ignore:\n      - staging\n      - launchpad\n      - production\njobs:\n  auto-pull-request:\n    name: PullRequestAction\n    runs-on: ubuntu-latest\n    steps:\n      - name: pull-request-action\n        uses: vsoch/pull-request-action@master\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BRANCH_PREFIX: \"update/\"\n          PULL_REQUEST_BRANCH: \"master\"\n```\n\n**Important**: Make sure to use a stable [release](https://github.com/vsoch/pull-request-action/releases) instead of a branch for your workflow.\n\n\n## Environment Variable Inputs\n\nUnlike standard actions, this action just uses variables from the environment.\n\n| Name | Description | Required | Default |\n|------|-------------|----------|---------|\n| BRANCH_PREFIX | the prefix to filter to. If the branch doesn't start with the prefix, it will be ignored | false | \"\" |\n| PULL_REQUEST_REPOSITORY | Choose another repository instead of default GITHUB_REPOSITORY for the PR  | false | |\n| PULL_REQUEST_TOKEN | [Personal Access Token(PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) only if you define a different repository with PULL_REQUEST_REPOSITORY | false | |\n| PULL_REQUEST_BRANCH | open pull request against this branch | false | master |\n| PULL_REQUEST_FROM_BRANCH | if a branch isn't found in your GitHub payload, use this branch | false | |\n| PULL_REQUEST_BODY | the body for the pull request | false | |\n| PULL_REQUEST_TITLE | the title for the pull request | false | |\n| PULL_REQUEST_DRAFT | should this be a draft PR? | false | unset |\n| MAINTAINER_CANT_MODIFY | Do not allow the maintainer to modify the PR | false | unset |\n| PULL_REQUEST_ASSIGNEES | A list (string with spaces) of users to assign | false | unset |\n| PULL_REQUEST_REVIEWERS | A list (string with spaces) of users to assign review | false | unset |\n| PULL_REQUEST_TEAM_REVIEWERS | A list (string with spaces) of teams to assign review | false | unset |\n| PASS_ON_ERROR | Instead of failing on an error response, pass | false | unset |\n| PASS_IF_EXISTS | Instead of failing if the pull request already exists, pass | false | unset |\n| PULL_REQUEST_UPDATE | If the pull request already exists, update it | false | unset |\n| PULL_REQUEST_STATE | If `PULL_REQUEST_UPDATE` is true, update to this state (open, closed) | false |open |\n\nFor `PULL_REQUEST_DRAFT`, `PASS_ON_ERROR`, `PASS_IF_EXISTS`, and `MAINTAINER_CANT_MODIFY`, these are\ntreated as environment booleans. If they are defined in the environment, they trigger the\n\"true\" condition. E.g.,:\n\n - Define `MAINTAINER_CANT_MODIFY` if you don't want the maintainer to be able to modify the pull request.\n - Define `PULL_REQUEST_DRAFT` if you want the PR to be a draft.\n - Define `PASS_ON_ERROR` if you want the PR to not exit given any non 200/201 response.\n - Define `PASS_IF_EXISTS` if you want the PR to not exit given the pull request is already open.\n - Define `PULL_REQUEST_UPDATE` if you want the pull request to be updated if it already exits.\n\nFor `PULL_REQUEST_ASSIGNEES`, `PULL_REQUEST_REVIEWERS`, and `PULL_REQUEST_TEAM_REVIEWERS` \nyou can provide a string of one or more GitHub usernames (or team names) to\nassign to the issue. Note that only users with push access can add assigness to \nan issue or PR, they are ignored otherwise.\n\nThe `GITHUB_TOKEN` secret is required to interact and authenticate with the GitHub API to open\nthe pull request. The example is [deployed here](https://github.com/vsoch/pull-request-action-example) with an example opened (and merged) [pull request here](https://github.com/vsoch/pull-request-action-example/pull/1) if needed.\n\nIf you want to create a pull request to another repository, for example, a pull request to the upstream repository, you need to define PULL_REQUEST_REPOSITORY and PULL_REQUEST_TOKEN. The PULL_REQUEST_TOKEN is one [Personal Access Token(PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token), which can be save in the [encrypted secrets](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository)\n\n## Outputs\n\nThe action sets a few useful output and environment variables. An output can\nbe referenced later as `${{ steps.<stepname>.outputs.<output-name> }}`.\nAn environment variable of course can be referenced as you usually would.\n\n| Name | Description | Environment | \n|------|-------------|-------------|\n| pull_request_number |If the pull request is opened, this is the number for it. | PULL_REQUEST_NUMBER |\n| pull_request_url |If the pull request is opened, the html url for it. | PULL_REQUEST_URL |\n| pull_request_return_code | Return code for the pull request | PULL_REQUEST_RETURN_CODE |\n| assignees_return_code | Return code for the assignees request | ASSIGNEES_RETURN_CODE |\n| reviewers_return_code | Return code for the reviewers request | REVIEWERS_RETURN_CODE |\n\nSee the [examples/outputs-example.yml](examples/outputs-example.yml) for how this works. \nIn this example, we can reference `${{ steps.pull_request.outputs.pull_request_url }}`\nin either another environment variable declaration, or within a run statement to access\nour variable `pull_request_url` that was generated in a step with id `pull_request`.\nThe screenshot below shows the example in action to interact with outputs in several ways.\n\n![img/outputs.png](img/outputs.png)\n\n## Examples\n\nExample workflows are provided in [examples](examples), and please contribute any\nexamples that you might have to help other users! You can get the same commit hashes\nand commented tags if you use the [action-updater](https://github.com/vsoch/action-updater)\nalso maintained by @vsoch. We will walk through a basic\nexample here for a niche case. Let's say that we are opening a pull request on the release event. This would mean\nthat the payload's branch variable would be null. We would need to define `PULL_REQUEST_FROM`. How would\nwe do that? We can [set environment variables](https://github.com/actions/toolkit/blob/main/docs/commands.md#environment-files) for next steps. Here is an example:\n\n```yaml\nname: Pull Request on Branch Push\non: [release]\njobs:\n  pull-request-on-release:\n    name: PullRequestAction\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v2\n      - name: Derive from branch name\n        run: |\n            # do custom parsing of your code / date to derive a branch from\n            PR_BRANCH_FROM=release-v$(cat VERSION)\n            echo \"PULL_REQUEST_FROM_BRANCH=${PR_BRANCH_FROM}\" >> $GITHUB_ENV\n      - name: pull-request-action\n        uses: vsoch/pull-request-action@master\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          PULL_REQUEST_BRANCH: \"master\"\n```\n\nThe above workflow is triggered on a release, so the branch will be null in the GItHub\npayload. Since we want the release PR to come from a special branch, we derive it\nin the second step, and then set the `PULL_REQUEST_FROM_BRANCH` variable in the environment\nfor the next step. In the Pull Request Action step, the pull request\nwill be opened from `PULL_REQUEST_FROM_BRANCH` against `PULL_REQUEST_BRANCH`, which is\nmaster. If we do not set this variable, the job will exit in an error,\nas it is not clear what action to take.\n\n\n## Example use Case: Update Registry\n\nAs an example, I created this action to be intended for an \n[organizational static registry](https://www.github.com/singularityhub/registry-org) for container builds. \nSpecifically, you have modular repositories building container recipes, and then opening pull requests to the \nregistry to update it. \n\n - the container collection content should be generated from a separate GitHub repository, including the folder structure (manifests, tags, collection README) that are expected.\n - the container collection metadata is pushed to a new branch on the registry repository, with namespace matching the GitHub repository, meaning that each GitHub repository always has a unique branch for its content.\n - pushing this branch that starts with the prefix (update/<namespace>) triggers the GitHub actions to open the pull request.\n\nIf the branch is already open for PR, it updates it. Take a look at [this example](https://github.com/singularityhub/registry-org/pull/8)\nfor the pull request opened when we updated the previous GitHub syntax to the new yaml syntax. Although this\ndoesn't describe the workflow above, it works equivalently in terms of the triggers.\n"
  },
  {
    "path": "action.yml",
    "content": "name: 'Pull Request Action'\ndescription: 'A GitHub action to open a pull request'\nauthor: 'vsoch'\nruns:\n  using: 'docker'\n  image: 'Dockerfile'\nbranding:\n  icon: 'activity'  \n  color: 'yellow'\noutputs:\n  pull_request_number:\n    description: 'If the pull request is opened, this is the number for it.'\n  pull_request_url:\n    description: 'If the pull request is opened, the html url for it.'\n  pull_request_return_code:\n    description: 'The pull request return code.'\n  assignees_return_code:\n    description: 'The add assignees post return code.'\n  reviewers_return_code:\n    description: 'The add reviewers post return code.'\n"
  },
  {
    "path": "examples/assignees-example.yml",
    "content": "name: Pull Request on Branch Push\non:\n  push:\n    branches-ignore:\n    - devel\njobs:\n  auto-pull-request:\n    name: PullRequestAction\n    runs-on: ubuntu-latest\n    steps:\n    - name: pull-request-action\n      uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        BRANCH_PREFIX: update/\n        PULL_REQUEST_BRANCH: master\n        PULL_REQUEST_ASSIGNEES: vsoch\n"
  },
  {
    "path": "examples/branch-from-environment.yml",
    "content": "name: derive-branch-from-environment\n\non:\n  schedule: -\n    cron: 0 0 * * 0\n\njobs:\n  DoSomeUpdate:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout Repository\n      uses: actions/checkout@v3\n    - name: Install or Do Something to Change repository\n      run: |\n        echo \"This is a new file.\" >> newfile.txt\n\n    - name: Checkout New Branch\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        BRANCH_AGAINST: master\n      run: |\n        printf \"GitHub Actor: ${GITHUB_ACTOR}\\n\"\n        export BRANCH_FROM=\"update/newfile-$(date '+%Y-%m-%d')\"\n        git remote set-url origin \"https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git\"\n        git branch\n        git checkout -b \"${BRANCH_FROM}\" || git checkout \"${BRANCH_FROM}\"\n        git branch\n\n        git config --global user.name \"github-actions\"\n        git config --global user.email \"github-actions@users.noreply.github.com\"\n\n        git add newfile.txt\n\n        if git diff-index --quiet HEAD --; then\n           printf \"No changes\\n\"\n        else\n           printf \"Changes\\n\"\n           git commit -m \"Automated deployment to update software database $(date '+%Y-%m-%d')\"\n           git push origin \"${BRANCH_FROM}\"\n        fi\n        # Here is where we are setting the environment variable!\n        echo \"PULL_REQUEST_FROM_BRANCH=${BRANCH_FROM}\" >> $GITHUB_ENV\n\n    - name: Open Pull Request\n      uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        PULL_REQUEST_BRANCH: master\n"
  },
  {
    "path": "examples/custom-body-example.yml",
    "content": "name: Hotfix Branch Pull Request\non:\n  push:\n    branches-ignore:\n    - master\n    - production\n\n# See https://github.com/vsoch/pull-request-action/issues/47#issuecomment-707109132\n\njobs:\n  auto-pull-request:\n    name: PullRequestAction\n    runs-on: ubuntu-latest\n    steps:\n    - name: Generate branch name\n      uses: actions/github-script@v6\n      id: set-branch-name\n      with:\n        script: |\n          const capitalize = (name) => name.charAt(0).toUpperCase() + name.slice(1);\n          const emoji = context.payload.ref.startsWith(\"refs/heads/feature\")\n            ? \"✨ \"\n            : context.payload.ref.startsWith(\"refs/heads/hotfix\")\n            ? \"🚑 \"\n            : \"\";\n          return `${emoji}${capitalize(\n            context.payload.ref\n              .replace(\"refs/heads/\", \"\")\n              .replace(/-/g, \" \")\n              .replace(\"feature \", \"\")\n              .replace(\"hotfix \", \"\")\n          )}`;\n        result-encoding: string\n    - name: Set branch name\n      run: echo \"PULL_REQUEST_TITLE=${{steps.set-branch-name.outputs.result}}\" >> $GITHUB_ENV\n    - name: Generate PR body\n      uses: actions/github-script@v6\n      id: set-pr-body\n      with:\n        script: |\n          return `I'm opening this pull request for this branch, pushed by @${\n            context.payload.head_commit.author.username\n          } with ${context.payload.commits.length} commit${\n            context.payload.commits.length === 1 ? \"\" : \"s\"\n          }.`;\n        result-encoding: string\n    - name: Set PR body\n      run: echo \"PULL_REQUEST_BODY=${{steps.set-pr-body.outputs.result}}\" >> $GITHUB_ENV\n    - name: pull-request-action\n      uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        BRANCH_PREFIX: hotfix-\n        PULL_REQUEST_BRANCH: production\n        PULL_REQUEST_REVIEWERS: AnandChowdhary\n"
  },
  {
    "path": "examples/outputs-example.yml",
    "content": "name: Pull Request on Branch Push\non:\n  push:\n    branches-ignore:\n    - devel\njobs:\n  auto-pull-request:\n    name: PullRequestAction\n    runs-on: ubuntu-latest\n    steps:\n    - name: pull-request-action\n      id: pull_request\n      uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        BRANCH_PREFIX: update/\n        PULL_REQUEST_BRANCH: master\n        PULL_REQUEST_REVIEWERS: vsoch\n    - name: Test outputs\n      env:\n        pull_request_number_output: ${{ steps.pull_request.outputs.pull_request_number }}\n        pull_request_url_output: ${{ steps.pull_request.outputs.pull_request_url }}\n      run: |\n        echo \"Pull request number from output: ${pull_request_number_output}\"\n        echo \"Pull request url from output: ${pull_request_url_output}\"\n        echo \"Pull request number from environment: ${PULL_REQUEST_NUMBER}\"\n        echo \"Pull request url from environment: ${PULL_REQUEST_URL}\"\n        echo \"Another way to specify from output ${{ steps.pull_request.outputs.pull_request_number }}\"\n"
  },
  {
    "path": "examples/push-example.yml",
    "content": "name: Pull Request on Branch Push\non:\n  push:\n    branches-ignore:\n    - staging\n    - launchpad\n    - production\njobs:\n  auto-pull-request:\n    name: PullRequestAction\n    runs-on: ubuntu-latest\n    steps:\n    - name: pull-request-action\n      uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        BRANCH_PREFIX: update/\n        PULL_REQUEST_BRANCH: master\n"
  },
  {
    "path": "examples/release-example.yml",
    "content": "on:\n  release:\n    types:\n    - published\n\njobs:\n  persist-new-suite-yml:\n    name: Commit Suite Release YML\n    runs-on: ubuntu-latest\n\n    steps:\n      # Likely other steps go here\n    - name: Set BRANCH_NAME\n      run: |\n        tag_name=${{github.event.release.tag_name}}\n        echo \"Tag: $tag_name\"\n\n        version=$(echo \"$tag_name\" | sed 's/^v//')\n        echo \"Version: $version\"\n\n        echo \"suite_version=${version}\" >> $GITHUB_OUTPUT\n        echo \"suite_update_branch=suite_${version}\" >> $GITHUB_OUTPUT\n      id: data\n\n    - name: Permanently save the new suite release\n      run: |\n        mkdir -p releases\n        new_suite_version_yml=\"releases/suite_${{ steps.data.outputs.suite_version }}.yml\"\n        echo \"Suite target file: $new_suite_version_yml\"\n        cp suite.yml \"${new_suite_version_yml}\"\n        git add \"${new_suite_version_yml}\"\n        git commit -m \"Suite v${{ steps.data.outputs.suite_version }} auto-commit of new release files\"\n\n    - name: Push files\n      run: git push --force \"https://${{ github.actor }}:${{secrets.GITHUB_TOKEN}}@github.com/${{ github.repository }}.git\" \"HEAD:${{ steps.data.outputs.suite_update_branch }}\"\n\n    - name: Open a PR to the default branch\n      uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        PULL_REQUEST_FROM_BRANCH: ${{ steps.data.outputs.suite_update_branch }}\n        PULL_REQUEST_BRANCH: master\n        PULL_REQUEST_TITLE: 'Action: Update suite release file for v${{ steps.data.outputs.suite_version }}'\n        PULL_REQUEST_BODY: Auto-generated PR!\n"
  },
  {
    "path": "examples/reviewers-example.yml",
    "content": "name: Pull Request on Branch Push\non:\n  push:\n    branches-ignore:\n    - devel\njobs:\n  auto-pull-request:\n    name: PullRequestAction\n    runs-on: ubuntu-latest\n    steps:\n    - name: pull-request-action\n      uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        BRANCH_PREFIX: update/\n        PULL_REQUEST_BRANCH: master\n        PULL_REQUEST_REVIEWERS: vsoch\n"
  },
  {
    "path": "pull-request.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport os\nimport json\nimport requests\n\n################################################################################\n# Helper Functions\n################################################################################\n\n\ndef get_envar(name):\n    \"\"\"\n    Given a name, return the corresponding environment variable. Exit if not\n    defined, as using this function indicates the envar is required.\n\n    Parameters:\n    name (str): the name of the environment variable\n    \"\"\"\n    value = os.environ.get(name)\n    if not value:\n        sys.exit(\"%s is required for vsoch/pull-request-action\" % name)\n    return value\n\n\ndef check_events_json():\n    \"\"\"the github events json is required in order to indicate that we are\n    in an action environment.\n    \"\"\"\n    events = get_envar(\"GITHUB_EVENT_PATH\")\n    if not os.path.exists(events):\n        sys.exit(\"Cannot find Github events file at ${GITHUB_EVENT_PATH}\")\n    print(\"Found ${GITHUB_EVENT_PATH} at %s\" % events)\n    return events\n\n\ndef abort_if_fail(response, reason):\n    \"\"\"If PASS_ON_ERROR, don't exit. Otherwise exit with an error and print\n    the reason.\n\n    Parameters:\n    response (requests.Response) : an unparsed response from requests\n    reason                 (str) : a message to print to the user for fail.\n    \"\"\"\n    message = \"%s: %s: %s\\n %s\" % (\n        reason,\n        response.status_code,\n        response.reason,\n        response.json(),\n    )\n\n    if os.environ.get(\"PASS_ON_ERROR\"):\n        print(\"Error, but PASS_ON_ERROR is set, continuing: %s\" % message)\n    else:\n        sys.exit(message)\n\n\ndef parse_into_list(values):\n    \"\"\"A list of reviewers or assignees to parse from a string to a list\n\n    Parameters:\n    values (str) : a list of space separated, quoted values to parse to a list\n    \"\"\"\n    if values:\n        values = values.replace('\"', \"\").replace(\"'\", \"\")\n    if not values:\n        return []\n    return [x.strip() for x in values.split(\" \")]\n\n\ndef set_env_and_output(name, value):\n    \"\"\"helper function to echo a key/value pair to the environement file\n\n    Parameters:\n    name (str)  : the name of the environment variable\n    value (str) : the value to write to file\n    \"\"\"\n    for env_var in (\"GITHUB_ENV\", \"GITHUB_OUTPUT\"):\n        environment_file_path = os.environ.get(env_var)\n        if not environment_file_path:\n            print(f\"Warning: {env_var} is unset, skipping.\")\n            continue\n        print(\"Writing %s=%s to %s\" % (name, value, env_var))\n\n        with open(environment_file_path, \"a\") as environment_file:\n            environment_file.write(\"%s=%s\\n\" % (name, value))\n\n\ndef open_pull_request(title, body, target, source, is_draft=False, can_modify=True):\n    \"\"\"Open pull request opens a pull request with a given body and content,\n    and sets output variables. An unparsed response is returned.\n\n    Parameters:\n    title       (str) : the title to set for the new pull request\n    body        (str) : the body to set for the new pull request\n    target      (str) : the target branch\n    source      (str) : the source branch\n    is_draft   (bool) : indicate the pull request is a draft\n    can_modify (bool) : indicate the maintainer can modify\n    \"\"\"\n    print(\"No pull request from %s to %s is open, continuing!\" % (source, target))\n\n    # Post the pull request\n    data = {\n        \"title\": title,\n        \"body\": body,\n        \"base\": target,\n        \"head\": source,\n        \"draft\": is_draft,\n        \"maintainer_can_modify\": can_modify,\n    }\n    print(\"Data for opening pull request: %s\" % data)\n    response = requests.post(PULLS_URL, json=data, headers=HEADERS)\n    if response.status_code != 201:\n        print(f\"pull request url is {PULLS_URL}\")\n        abort_if_fail(response, \"Unable to create pull request\")\n\n    return response\n\n\ndef update_pull_request(entry, title, body, target, state=None):\n    \"\"\"Given an existing pull request, update it.\n\n    Parameters:\n    entry      (dict) : the pull request metadata\n    title       (str) : the title to set for the new pull request\n    body        (str) : the body to set for the new pull request\n    target      (str) : the target branch\n    state      (bool) : the state of the PR (open, closed)\n    \"\"\"\n    print(\"PULL_REQUEST_UPDATE is set, updating existing pull request.\")\n\n    data = {\n        \"title\": title,\n        \"body\": body,\n        \"base\": target,\n        \"state\": state or \"open\",\n    }\n    # PATCH /repos/{owner}/{repo}/pulls/{pull_number}\n    url = \"%s/%s\" % (PULLS_URL, entry.get(\"number\"))\n    print(\"Data for updating pull request: %s\" % data)\n    response = requests.patch(url, json=data, headers=HEADERS)\n    if response.status_code != 200:\n        abort_if_fail(response, \"Unable to create pull request\")\n\n    return response\n\n\ndef set_pull_request_groups(response):\n    \"\"\"Given a response for an open or updated PR, set metadata\n\n    Parameters:\n    response (requests.Response) : a requests response, unparsed\n    \"\"\"\n    # Expected return codes are 0 for success\n    pull_request_return_code = (\n        0 if response.status_code == 201 else response.status_code\n    )\n    response = response.json()\n    print(\"::group::github response\")\n    print(response)\n    print(\"::endgroup::github response\")\n    number = response.get(\"number\")\n    html_url = response.get(\"html_url\")\n    print(\"Number opened for PR is %s\" % number)\n    set_env_and_output(\"PULL_REQUEST_NUMBER\", number)\n    set_env_and_output(\"PULL_REQUEST_RETURN_CODE\", pull_request_return_code)\n    set_env_and_output(\"PULL_REQUEST_URL\", html_url)\n\n\ndef list_pull_requests(target, source):\n    \"\"\"Given a target and source, return a list of pull requests that match\n    (or simply exit given some kind of error code)\n\n    Parameters:\n    target (str) : the target branch\n    source (str) : the source branch\n    \"\"\"\n    # Check if the branch already has a pull request open\n    params = {\"base\": target, \"head\": source, \"state\": \"open\"}\n    print(\"Params for checking if pull request exists: %s\" % params)\n    response = requests.get(PULLS_URL, params=params)\n\n    # Case 1: 401, 404 might warrant needing a token\n    if response.status_code in [401, 404]:\n        response = requests.get(PULLS_URL, params=params, headers=HEADERS)\n    if response.status_code != 200:\n        abort_if_fail(response, \"Unable to retrieve information about pull requests\")\n\n    return response.json()\n\n\ndef add_assignees(entry, assignees):\n    \"\"\"Given a pull request metadata (from create or update) add assignees\n\n    Parameters:\n    entry (dict)    : the pull request metadata\n    assignees (str) : comma separated assignees string set by action\n    \"\"\"\n    # Remove leading and trailing quotes\n    assignees = parse_into_list(assignees)\n    number = entry.get(\"number\")\n\n    print(\n        \"Attempting to assign %s to pull request with number %s\" % (assignees, number)\n    )\n\n    # POST /repos/:owner/:repo/issues/:issue_number/assignees\n    data = {\"assignees\": assignees}\n    ASSIGNEES_URL = \"%s/%s/assignees\" % (ISSUE_URL, number)\n    response = requests.post(ASSIGNEES_URL, json=data, headers=HEADERS)\n    if response.status_code != 201:\n        abort_if_fail(response, \"Unable to create assignees\")\n\n    assignees_return_code = 0 if response.status_code == 201 else response.status_code\n    print(\"::group::github assignees response\")\n    print(response.json())\n    print(\"::endgroup::github assignees response\")\n    set_env_and_output(\"ASSIGNEES_RETURN_CODE\", assignees_return_code)\n\n\ndef find_pull_request(listing, source):\n    \"\"\"Given a listing and a source, find a pull request based on the source\n    (the branch name).\n\n    Parameters:\n    listing (list) : the list of PR objects (dict) to parse over\n    source   (str) : the source (head) branch to look for\n    \"\"\"\n    if listing:\n        for entry in listing:\n            if entry.get(\"head\", {}).get(\"ref\", \"\") == source:\n                print(\"Pull request from %s is already open!\" % source)\n                return entry\n\n\ndef find_default_branch():\n    \"\"\"Find default branch for a repo (only called if branch not provided)\"\"\"\n    response = requests.get(REPO_URL)\n\n    # Case 1: 401, 404 might need a token\n    if response.status_code in [401, 404]:\n        response = requests.get(REPO_URL, headers=HEADERS)\n    if response.status_code != 200:\n        abort_if_fail(response, \"Unable to retrieve default branch\")\n\n    default_branch = response.json()[\"default_branch\"]\n    print(\"Found default branch: %s\" % default_branch)\n    return default_branch\n\n\ndef add_reviewers(entry, reviewers, team_reviewers):\n    \"\"\"Given regular or team reviewers, add them to a PR.\n\n    Parameters:\n    entry (dict) : the pull request metadata\n    \"\"\"\n    print(\"Found reviewers: %s and team reviewers: %s\" % (reviewers, team_reviewers))\n    team_reviewers = parse_into_list(team_reviewers)\n    reviewers = parse_into_list(reviewers)\n    print(\"Parsed reviewers: %s and team reviewers: %s\" % (reviewers, team_reviewers))\n\n    # POST /repos/:owner/:repo/pulls/:pull_number/requested_reviewers\n    REVIEWERS_URL = \"%s/%s/requested_reviewers\" % (PULLS_URL, entry.get(\"number\"))\n\n    data = {\"reviewers\": reviewers, \"team_reviewers\": team_reviewers}\n    response = requests.post(REVIEWERS_URL, json=data, headers=HEADERS)\n    if response.status_code != 201:\n        abort_if_fail(response, \"Unable to assign reviewers\")\n    reviewers_return_code = 0 if response.status_code == 201 else response.status_code\n\n    print(\"::group::github reviewers response\")\n    print(response.json())\n    print(\"::endgroup::github reviewers response\")\n    set_env_and_output(\"REVIEWERS_RETURN_CODE\", reviewers_return_code)\n\n\n################################################################################\n# Global Variables (we can't use GITHUB_ prefix)\n################################################################################\n\nAPI_VERSION = \"v3\"\n\n# Allow for a GitHub enterprise URL\nBASE = os.environ.get(\"GITHUB_API_URL\") or \"https://api.github.com\"\n\nPR_TOKEN = os.environ.get(\"PULL_REQUEST_TOKEN\") or get_envar(\"GITHUB_TOKEN\")\nPR_REPO = os.environ.get(\"PULL_REQUEST_REPOSITORY\") or get_envar(\"GITHUB_REPOSITORY\")\n\nHEADERS = {\n    \"Authorization\": \"token %s\" % PR_TOKEN,\n    \"Accept\": \"application/vnd.github.%s+json;application/vnd.github.antiope-preview+json;application/vnd.github.shadow-cat-preview+json\"\n    % API_VERSION,\n}\n\n# URLs\nREPO_URL = \"%s/repos/%s\" % (BASE, PR_REPO)\nISSUE_URL = \"%s/issues\" % REPO_URL\nPULLS_URL = \"%s/pulls\" % REPO_URL\n\n\ndef create_pull_request(\n    source,\n    target,\n    body,\n    title,\n    assignees,\n    reviewers,\n    team_reviewers,\n    is_draft=False,\n    can_modify=True,\n    state=\"open\",\n):\n    \"\"\"Create pull request is the base function that determines if the PR exists,\n    and then updates or creates it depending on user preferences.\n    \"\"\"\n    listing = list_pull_requests(target, source)\n\n    # Determine if the pull request is already open\n    entry = find_pull_request(listing, source)\n    response = None\n\n    # Case 1: we found the PR, the user wants to pass\n    if entry and os.environ.get(\"PASS_IF_EXISTS\"):\n        print(\"PASS_IF_EXISTS is set, exiting with success status.\")\n        sys.exit(0)\n\n    # Does the user want to update the existing PR?\n    if entry and os.environ.get(\"PULL_REQUEST_UPDATE\"):\n        response = update_pull_request(entry, title, body, target, state)\n        set_pull_request_groups(response)\n\n    # If it's not open, we open a new pull request\n    elif not entry:\n        response = open_pull_request(title, body, target, source, is_draft, can_modify)\n        set_pull_request_groups(response)\n\n    # If we have a response, parse into json (no longer need retvals)\n    response = response.json() if response else None\n\n    # If we have opened or updated, we can add assignees\n    if response and assignees:\n        add_assignees(response, assignees)\n    if response and (reviewers or team_reviewers):\n        add_reviewers(response, reviewers, team_reviewers)\n\n\ndef main():\n    \"\"\"main primarily parses environment variables to prepare for creation\"\"\"\n\n    # path to file that contains the POST response of the event\n    # Example: https://github.com/actions/bin/tree/master/debug\n    # Value: /github/workflow/event.json\n    check_events_json()\n\n    branch_prefix = os.environ.get(\"BRANCH_PREFIX\", \"\")\n    print(\"Branch prefix is %s\" % branch_prefix)\n    if not branch_prefix:\n        print(\"No branch prefix is set, all branches will be used.\")\n\n    # Default to project default branch if none provided\n    pull_request_branch = os.environ.get(\"PULL_REQUEST_BRANCH\")\n    if not pull_request_branch:\n        pull_request_branch = find_default_branch()\n\n    print(\"Pull requests will go to %s\" % pull_request_branch)\n\n    # Pull request draft\n    pull_request_draft = os.environ.get(\"PULL_REQUEST_DRAFT\")\n    if not pull_request_draft:\n        print(\"No explicit preference for draft PR: created PRs will be normal PRs.\")\n        pull_request_draft = False\n    else:\n        print(\"PULL_REQUEST_DRAFT set to a value: created PRs will be draft PRs.\")\n        pull_request_draft = True\n\n    # If an update is true, we can change the state\n    pull_request_state = os.environ.get(\"PULL_REQUEST_STATE\", \"open\")\n    if pull_request_state not in [\"open\", \"closed\"]:\n        sys.exit(\"State is required to be one of 'open' or 'closed'\")\n\n    # Maintainer can modify, defaults to CAN, unless user sets MAINTAINER_CANT_MODIFY\n    maintainer_can_modify = os.environ.get(\"MAINTAINER_CANT_MODIFY\")\n    if not maintainer_can_modify:\n        print(\"No preference for maintainer being able to modify: default is true.\")\n        maintainer_can_modify = True\n    else:\n        print(\n            \"MAINTAINER_CANT_MODIFY set to a value: maintainer will not be able to modify.\"\n        )\n        maintainer_can_modify = False\n\n    # Assignees\n    assignees = os.environ.get(\"PULL_REQUEST_ASSIGNEES\")\n    if not assignees:\n        print(\"PULL_REQUEST_ASSIGNEES is not set, no assignees.\")\n    else:\n        print(\"PULL_REQUEST_ASSIGNEES is set, %s\" % assignees)\n\n    # Reviewers (individual and team)\n\n    reviewers = os.environ.get(\"PULL_REQUEST_REVIEWERS\")\n    team_reviewers = os.environ.get(\"PULL_REQUEST_TEAM_REVIEWERS\")\n    if not reviewers:\n        print(\"PULL_REQUEST_REVIEWERS is not set, no reviewers.\")\n    else:\n        print(\"PULL_REQUEST_REVIEWERS is set, %s\" % reviewers)\n\n    if not team_reviewers:\n        print(\"PULL_REQUEST_TEAM_REVIEWERS is not set, no team reviewers.\")\n    else:\n        print(\"PULL_REQUEST_TEAM_REVIEWERS is set, %s\" % team_reviewers)\n\n    # The user is allowed to explicitly set the name of the branch\n    from_branch = os.environ.get(\"PULL_REQUEST_FROM_BRANCH\")\n    if not from_branch:\n        print(\"PULL_REQUEST_FROM_BRANCH is not set, checking branch in payload.\")\n        with open(check_events_json(), \"r\") as fd:\n            from_branch = json.loads(fd.read()).get(\"ref\", \"\")\n        from_branch = from_branch.replace(\"refs/heads/\", \"\").strip(\"/\")\n    else:\n        print(\"PULL_REQUEST_FROM_BRANCH is set.\")\n\n    # At this point, we must have a branch\n    if from_branch:\n        print(\"Found branch %s to open PR from\" % from_branch)\n    else:\n        sys.exit(\n            \"You are required to define PULL_REQUEST_FROM_BRANCH in the environment.\"\n        )\n\n    # If it's to the target branch, ignore it\n    if from_branch == pull_request_branch:\n        print(\"Target and current branch are identical (%s), skipping.\" % from_branch)\n        sys.exit(0)\n\n    # If the prefix for the branch matches\n    if not branch_prefix or from_branch.startswith(branch_prefix):\n\n        # Pull request body (optional)\n        pull_request_body = os.environ.get(\n            \"PULL_REQUEST_BODY\",\n            \"This is an automated pull request to update from branch %s\" % from_branch,\n        )\n\n        print(\"::group::pull request body\")\n        print(pull_request_body)\n        print(\"::endgroup::pull request body\")\n\n        # Pull request title (optional)\n        pull_request_title = os.environ.get(\n            \"PULL_REQUEST_TITLE\", \"Update from %s\" % from_branch\n        )\n        print(\"::group::pull request title\")\n        print(pull_request_title)\n        print(\"::endgroup::pull request title\")\n\n        # Create the pull request\n        create_pull_request(\n            target=pull_request_branch,\n            source=from_branch,\n            body=pull_request_body,\n            title=pull_request_title,\n            is_draft=pull_request_draft,\n            can_modify=maintainer_can_modify,\n            assignees=assignees,\n            reviewers=reviewers,\n            team_reviewers=team_reviewers,\n            state=pull_request_state,\n        )\n\n\nif __name__ == \"__main__\":\n    print(\"==========================================================================\")\n    print(\"START: Running Pull Request on Branch Update Action!\")\n    main()\n    print(\"==========================================================================\")\n    print(\"END: Finished\")\n"
  }
]