├── .flake8
├── .github
├── CODEOWNERS
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── support_request.md
├── blunderbuss.yml
├── release-please.yml
├── release-trigger.yml
└── sync-repo-settings.yaml
├── .gitignore
├── .kokoro-autorelease
├── build.sh
├── common.cfg
├── tag.cfg
├── tag.sh
├── trigger.cfg
└── trigger.sh
├── .kokoro
├── build.sh
├── common.cfg
├── continuous.cfg
├── populate-release-secrets.sh
├── populate-secrets.sh
├── presubmit.cfg
├── release.sh
├── release
│ ├── common.cfg
│ └── release.cfg
├── trampoline.sh
└── trampoline_release.sh
├── .trampolinerc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── SECURITY.md
├── autorelease
├── __init__.py
├── __main__.py
├── common.py
├── github.py
├── kokoro.py
├── report.xml.j2
├── reporter.py
├── tag.py
└── trigger.py
├── noxfile.py
├── protos
├── __init__.py
├── kokoro_api.proto
└── kokoro_api_pb2.py
├── pytest.ini
├── releasetool
├── __init__.py
├── __main__.py
├── circleci.py
├── commands
│ ├── __init__.py
│ ├── common.py
│ ├── publish_reporter.py
│ ├── publish_reporter.sh
│ ├── start
│ │ ├── __init__.py
│ │ ├── go.py
│ │ ├── java.py
│ │ ├── nodejs.py
│ │ ├── python.py
│ │ ├── python_tool.py
│ │ └── ruby.py
│ └── tag
│ │ ├── __init__.py
│ │ ├── cpp.py
│ │ ├── dotnet.py
│ │ ├── go.py
│ │ ├── java.py
│ │ ├── nodejs.py
│ │ ├── php.py
│ │ ├── python.py
│ │ ├── python_tool.py
│ │ └── ruby.py
├── filehelpers.py
├── git.py
├── github.py
├── secrets.py
└── update_check.py
├── requirements-dev.in
├── requirements-dev.txt
├── requirements.in
├── requirements.txt
├── setup.py
├── testing
├── constraints-3.10.txt
├── constraints-3.11.txt
├── constraints-3.12.txt
├── constraints-3.8.txt
└── constraints-3.9.txt
└── tests
├── __init__.py
├── commands
├── start
│ ├── test_python.py
│ └── test_ruby.py
└── tag
│ ├── test_tag_cpp.py
│ ├── test_tag_dotnet.py
│ ├── test_tag_go.py
│ ├── test_tag_java.py
│ ├── test_tag_nodejs.py
│ ├── test_tag_php.py
│ ├── test_tag_python.py
│ ├── test_tag_python_tool.py
│ └── test_tag_ruby.py
├── common_test.py
├── test_autorelease_tag.py
├── test_autorelease_trigger.py
├── test_dummy.py
├── test_github.py
├── test_publish_reporter.py
└── testdata
├── fake-private-key.pem
└── repos.json
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E501, W503
3 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Code owners file.
2 | # This file controls who is tagged for review for any given pull request.
3 | #
4 | # For syntax help see:
5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
6 |
7 | * @googleapis/yoshi @googleapis/cloud-client-library-release-automation-team
8 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | Thanks for stopping by to let us know something could be better!
8 |
9 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
10 |
11 | Please run down the following list and make sure you've tried the usual "quick fixes":
12 |
13 | - Search the issues already opened: https://github.com/googleapis/releasetool/issues
14 |
15 | If you are still having issues, please be sure to include as much information as possible:
16 |
17 | #### Environment details
18 |
19 | - OS:
20 | - Python version:
21 | - pip version:
22 | - `releasetool` version:
23 |
24 | #### Steps to reproduce
25 |
26 | 1. ?
27 | 2. ?
28 |
29 | Making sure to follow these steps will guarantee the quickest resolution possible.
30 |
31 | Thanks!
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this library
4 |
5 | ---
6 |
7 | Thanks for stopping by to let us know something could be better!
8 |
9 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
10 |
11 | **Is your feature request related to a problem? Please describe.**
12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 | **Additional context**
18 | Add any other context or screenshots about the feature request here.
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Support request
3 | about: If you have a support contract with Google, please create an issue in the Google Cloud Support console.
4 |
5 | ---
6 |
7 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
8 |
--------------------------------------------------------------------------------
/.github/blunderbuss.yml:
--------------------------------------------------------------------------------
1 | assign_issues:
2 | - chingor13
3 | - SurferJeffAtGoogle
4 | assign_prs:
5 | - chingor13
6 | - SurferJeffAtGoogle
7 |
--------------------------------------------------------------------------------
/.github/release-please.yml:
--------------------------------------------------------------------------------
1 | releaseType: python
2 | handleGHRelease: true
3 |
--------------------------------------------------------------------------------
/.github/release-trigger.yml:
--------------------------------------------------------------------------------
1 | enabled: true
2 | multiScmName: releasetool
3 |
--------------------------------------------------------------------------------
/.github/sync-repo-settings.yaml:
--------------------------------------------------------------------------------
1 | rebaseMergeAllowed: true
2 | squashMergeAllowed: true
3 | mergeCommitAllowed: false
4 | branchProtectionRules:
5 | - pattern: master
6 | requiredStatusCheckContexts:
7 | - 'Kokoro CI'
8 | - 'cla/google'
9 | requiredApprovingReviewCount: 1
10 | requiresCodeOwnerReviews: true
11 | requiresStrictStatusChecks: true
12 | permissionRules:
13 | - team: yoshi
14 | permission: push
15 | - team: yoshi-admins
16 | permission: admin
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | .pytest_cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | .hypothesis/
50 | .pytest_cache/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | .static_storage/
59 | .media/
60 | local_settings.py
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # Environments
88 | .env
89 | .venv
90 | env/
91 | venv/
92 | ENV/
93 | env.bak/
94 | venv.bak/
95 |
96 | # Spyder project settings
97 | .spyderproject
98 | .spyproject
99 |
100 | # Rope project settings
101 | .ropeproject
102 |
103 | # mkdocs documentation
104 | /site
105 |
106 | # mypy
107 | .mypy_cache/
108 |
109 | app
110 | installation
111 | pem
112 |
113 | # IDEs
114 | .vscode/
115 | .idea/
--------------------------------------------------------------------------------
/.kokoro-autorelease/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2018 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | set -eo pipefail
17 |
18 | pushd github/releasetool
19 |
20 | # Docker images currently uses 3.7.4
21 |
22 | # Disable buffering, so that the logs stream through.
23 | export PYTHONUNBUFFERED=1
24 |
25 | # The key for triggering Kokoro jobs is a Keystore resource, so it'll be here.
26 | export AUTORELEASE_KOKORO_CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_kokoro_trigger_credentials
27 |
28 | # install release-please binary to do tagging
29 | npm i release-please
30 | npx release-please --version
31 |
32 | python3 -m pip install --quiet --user --upgrade --require-hashes -r requirements.txt
33 |
34 | python3 -m autorelease --report sponge_log.xml ${AUTORELEASE_COMMAND}
35 |
--------------------------------------------------------------------------------
/.kokoro-autorelease/common.cfg:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # Format: //devtools/kokoro/config/proto/build.proto
16 |
17 | # Download trampoline resources.
18 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
19 |
20 | build_file: "releasetool/.kokoro/trampoline_release.sh"
21 |
22 | # Configure the docker image for kokoro-trampoline.
23 | env_vars: {
24 | key: "TRAMPOLINE_IMAGE"
25 | value: "us-central1-docker.pkg.dev/cloud-sdk-release-custom-pool/release-images/node18"
26 | }
27 |
28 | # Tell the trampoline which build file to use.
29 | env_vars: {
30 | key: "TRAMPOLINE_BUILD_FILE"
31 | value: "github/releasetool/.kokoro-autorelease/build.sh"
32 | }
33 |
34 | # Build logs will be here
35 | action {
36 | define_artifacts {
37 | regex: "**/*sponge_log.xml"
38 | }
39 | }
40 |
41 | # Magictoken to access GitHub
42 | # TODO(busunkim): Remove this key once KMS setup is complete.
43 | before_action {
44 | fetch_keystore {
45 | keystore_resource {
46 | keystore_config_id: 73713
47 | keyname: "autorelease-magictoken"
48 | }
49 | }
50 | }
51 |
52 | # GitHub token for yoshi-automation
53 | # TODO(busunkim): Remove this key once KMS setup is complete.
54 | before_action {
55 | fetch_keystore {
56 | keystore_resource {
57 | keystore_config_id: 73713
58 | keyname: "yoshi-automation-github-key"
59 | }
60 | }
61 | }
62 |
63 | # Magic GitHub Proxy API
64 | before_action {
65 | fetch_keystore {
66 | keystore_resource {
67 | keystore_config_id: 73713
68 | keyname: "magic-github-proxy-api-key"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/.kokoro-autorelease/tag.cfg:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | before_action {
16 | fetch_keystore {
17 | keystore_resource {
18 | keystore_config_id: 73713
19 | keyname: "kokoro_trigger_credentials"
20 | }
21 | }
22 | }
23 |
24 | env_vars: {
25 | key: "AUTORELEASE_COMMAND"
26 | value: "tag"
27 | }
28 |
--------------------------------------------------------------------------------
/.kokoro-autorelease/tag.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | CURRENT_DIR=$(dirname "${BASH_SOURCE[0]}")
17 | AUTORELEASE_COMMAND=tag "${CURRENT_DIR}/build.sh"
18 |
--------------------------------------------------------------------------------
/.kokoro-autorelease/trigger.cfg:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | before_action {
16 | fetch_keystore {
17 | keystore_resource {
18 | keystore_config_id: 73713
19 | keyname: "kokoro_trigger_credentials"
20 | }
21 | }
22 | }
23 |
24 | env_vars: {
25 | key: "AUTORELEASE_COMMAND"
26 | value: "trigger"
27 | }
28 |
--------------------------------------------------------------------------------
/.kokoro-autorelease/trigger.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | CURRENT_DIR=$(dirname "${BASH_SOURCE[0]}")
17 | AUTORELEASE_COMMAND=trigger "${CURRENT_DIR}/build.sh"
18 |
--------------------------------------------------------------------------------
/.kokoro/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2018 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -eo pipefail
18 |
19 | cd github/releasetool
20 |
21 | # Disable buffering, so that the logs stream through.
22 | export PYTHONUNBUFFERED=1
23 |
24 | # Run tests
25 | nox -s lint test
26 |
27 | # remove all files, preventing kokoro from trying to sync them.
28 | rm -rf *
29 |
--------------------------------------------------------------------------------
/.kokoro/common.cfg:
--------------------------------------------------------------------------------
1 | # Format: //devtools/kokoro/config/proto/build.proto
2 |
3 | # Download trampoline resources.
4 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
5 |
6 | # Use the trampoline script to run in docker.
7 | build_file: "releasetool/.kokoro/trampoline.sh"
8 |
9 | # Configure the docker image for kokoro-trampoline.
10 | env_vars: {
11 | key: "TRAMPOLINE_IMAGE"
12 | value: "gcr.io/cloud-devrel-kokoro-resources/python-multi:latest"
13 | }
14 |
15 | # Tell the trampoline which build file to use.
16 | env_vars: {
17 | key: "TRAMPOLINE_BUILD_FILE"
18 | value: "github/releasetool/.kokoro/build.sh"
19 | }
20 |
--------------------------------------------------------------------------------
/.kokoro/continuous.cfg:
--------------------------------------------------------------------------------
1 | # Format: //devtools/kokoro/config/proto/build.proto
2 |
--------------------------------------------------------------------------------
/.kokoro/populate-release-secrets.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | set -eo pipefail
17 |
18 | function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;}
19 | function msg { println "$*" >&2 ;}
20 | function println { printf '%s\n' "$(now) $*" ;}
21 |
22 |
23 | SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager"
24 | msg "Creating folder on disk for secrets: ${SECRET_LOCATION}"
25 | mkdir -p ${SECRET_LOCATION}
26 | for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g")
27 | do
28 | msg "Retrieving secret ${key}"
29 | docker run --entrypoint=gcloud \
30 | --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \
31 | gcr.io/google.com/cloudsdktool/cloud-sdk \
32 | secrets versions access latest \
33 | --project cloud-sdk-release-custom-pool \
34 | --secret ${key} > \
35 | "${SECRET_LOCATION}/${key}"
36 | if [[ $? == 0 ]]; then
37 | msg "Secret written to ${SECRET_LOCATION}/${key}"
38 | else
39 | msg "Error retrieving secret ${key}"
40 | fi
41 | done
42 |
--------------------------------------------------------------------------------
/.kokoro/populate-secrets.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google LLC.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | set -eo pipefail
17 |
18 | function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;}
19 | function msg { println "$*" >&2 ;}
20 | function println { printf '%s\n' "$(now) $*" ;}
21 |
22 |
23 | # Populates requested secrets set in SECRET_MANAGER_KEYS from service account:
24 | # kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com
25 | SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager"
26 | msg "Creating folder on disk for secrets: ${SECRET_LOCATION}"
27 | mkdir -p ${SECRET_LOCATION}
28 | for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g")
29 | do
30 | msg "Retrieving secret ${key}"
31 | docker run --entrypoint=gcloud \
32 | --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \
33 | gcr.io/google.com/cloudsdktool/cloud-sdk \
34 | secrets versions access latest \
35 | --project cloud-devrel-kokoro-resources \
36 | --secret ${key} > \
37 | "${SECRET_LOCATION}/${key}"
38 | if [[ $? == 0 ]]; then
39 | msg "Secret written to ${SECRET_LOCATION}/${key}"
40 | else
41 | msg "Error retrieving secret ${key}"
42 | fi
43 | done
44 |
--------------------------------------------------------------------------------
/.kokoro/presubmit.cfg:
--------------------------------------------------------------------------------
1 | # Format: //devtools/kokoro/config/proto/build.proto
2 |
--------------------------------------------------------------------------------
/.kokoro/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2018 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -eo pipefail
18 |
19 | # Move into the package, build the distribution and upload.
20 | cd github/releasetool
21 |
22 | # Enable the publish build reporter
23 | # Note: this installs from source since we're in the releasetool repo. Other projects
24 | # will need to use python3 -m pip install gcp-releasetool
25 | python3 -m pip install --require-hashes -r requirements.txt
26 | python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script
27 |
28 | TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-1")
29 |
30 | # Disable buffering, so that the logs stream through.
31 | export PYTHONUNBUFFERED=1
32 |
33 | python3 setup.py sdist bdist_wheel
34 | python3 -m twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/*
35 |
--------------------------------------------------------------------------------
/.kokoro/release/common.cfg:
--------------------------------------------------------------------------------
1 | # Format: //devtools/kokoro/config/proto/build.proto
2 |
--------------------------------------------------------------------------------
/.kokoro/release/release.cfg:
--------------------------------------------------------------------------------
1 | # Format: //devtools/kokoro/config/proto/build.proto
2 |
3 | # Download trampoline resources.
4 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
5 |
6 | # Use the trampoline script to run in docker.
7 | build_file: "releasetool/.kokoro/trampoline_release.sh"
8 |
9 | env_vars: {
10 | key: "SECRET_MANAGER_KEYS"
11 | value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem"
12 | }
13 |
14 | env_vars: {
15 | key: "TRAMPOLINE_BUILD_FILE"
16 | value: "github/releasetool/.kokoro/release.sh"
17 | }
18 |
19 | # Configure the docker image for kokoro-trampoline.
20 | env_vars: {
21 | key: "TRAMPOLINE_IMAGE"
22 | value: "us-central1-docker.pkg.dev/cloud-sdk-release-custom-pool/release-images/python-multi"
23 | }
24 |
25 | # Fetch PyPI password
26 | before_action {
27 | fetch_keystore {
28 | keystore_resource {
29 | keystore_config_id: 73713
30 | keyname: "google-cloud-pypi-token-keystore-1"
31 | }
32 | }
33 | }
34 |
35 | # Store the packages we uploaded to PyPI. That way, we have a record of exactly
36 | # what we published, which we can use to generate SBOMs and attestations.
37 | action {
38 | define_artifacts {
39 | regex: "github/releasetool/**/*.tar.gz"
40 | strip_prefix: "github/releasetool"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.kokoro/trampoline.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2017 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | set -eo pipefail
16 | # Always run the cleanup script, regardless of the success of bouncing into
17 | # the container.
18 | function cleanup() {
19 | chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
20 | ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
21 | echo "cleanup";
22 | }
23 | trap cleanup EXIT
24 |
25 | $(dirname $0)/populate-secrets.sh # Secret Manager secrets.
26 | python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py"
27 |
--------------------------------------------------------------------------------
/.kokoro/trampoline_release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2017 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | set -eo pipefail
16 | # Always run the cleanup script, regardless of the success of bouncing into
17 | # the container.
18 | function cleanup() {
19 | chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
20 | ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
21 | echo "cleanup";
22 | }
23 | trap cleanup EXIT
24 |
25 | $(dirname $0)/populate-release-secrets.sh # Secret Manager secrets.
26 | TRAMPOLINE_HOST=$(echo "${TRAMPOLINE_IMAGE}" | cut -d/ -f1)
27 | if [[ ! "${TRAMPOLINE_HOST}" =~ "gcr.io" ]]; then
28 | # If you need to specify a host other than gcr.io, you have to run on an update version of gcloud.
29 | echo "TRAMPOLINE_HOST: ${TRAMPOLINE_HOST}"
30 | gcloud components update
31 | gcloud auth configure-docker "${TRAMPOLINE_HOST}"
32 | fi
33 | python3 "${KOKORO_GFILE_DIR}/trampoline_release.py"
34 |
--------------------------------------------------------------------------------
/.trampolinerc:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # This file adds configuration options for trampoline_v2.sh
16 | # See https://github.com/GoogleCloudPlatform/docker-ci-helper#customization
17 |
18 | pass_down_envvars+=(
19 | # Autorelease command for .kokoro-autorelease/build.sh
20 | "AUTORELEASE_COMMAND"
21 | )
22 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project,
4 | and in the interest of fostering an open and welcoming community,
5 | we pledge to respect all people who contribute through reporting issues,
6 | posting feature requests, updating documentation,
7 | submitting pull requests or patches, and other activities.
8 |
9 | We are committed to making participation in this project
10 | a harassment-free experience for everyone,
11 | regardless of level of experience, gender, gender identity and expression,
12 | sexual orientation, disability, personal appearance,
13 | body size, race, ethnicity, age, religion, or nationality.
14 |
15 | Examples of unacceptable behavior by participants include:
16 |
17 | * The use of sexualized language or imagery
18 | * Personal attacks
19 | * Trolling or insulting/derogatory comments
20 | * Public or private harassment
21 | * Publishing other's private information,
22 | such as physical or electronic
23 | addresses, without explicit permission
24 | * Other unethical or unprofessional conduct.
25 |
26 | Project maintainers have the right and responsibility to remove, edit, or reject
27 | comments, commits, code, wiki edits, issues, and other contributions
28 | that are not aligned to this Code of Conduct.
29 | By adopting this Code of Conduct,
30 | project maintainers commit themselves to fairly and consistently
31 | applying these principles to every aspect of managing this project.
32 | Project maintainers who do not follow or enforce the Code of Conduct
33 | may be permanently removed from the project team.
34 |
35 | This code of conduct applies both within project spaces and in public spaces
36 | when an individual is representing the project or its community.
37 |
38 | Instances of abusive, harassing, or otherwise unacceptable behavior
39 | may be reported by opening an issue
40 | or contacting one or more of the project maintainers.
41 |
42 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
43 | available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
44 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | recursive-include releasetool/commands *.sh
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Releasetool (for client libraries)
2 |
3 | This tool helps create releases for cloud client libraries.
4 |
5 | Presently, this works for Python, Node.js, and Ruby. However, it's designed
6 | in such a way that it could easily be used for other languages.
7 |
8 |
9 | ## Installation
10 |
11 | **Requirements:**
12 | - Python 3.8+
13 | - pip
14 |
15 | We recommend following [this guide](https://docs.python-guide.org/starting/installation/#installation-guides) for installing both Python 3 and pip.
16 |
17 |
18 | Install releasetool using pip:
19 | ```
20 | python3 -m pip install gcp-releasetool
21 | ```
22 |
23 | ## Usage
24 |
25 | Packages are published in two phases. First, a PR is created to update
26 | `CHANGELOG.md` and the version number. Second, once the PR is merged the
27 | merge commit is tagged and CI publishes the package.
28 |
29 | To start the process of releasing use `releasetool start` from the directory of
30 | the client you want to publish, for example:
31 |
32 | ```
33 | git clone git@github.com:googleapis/google-cloud-python.git
34 | cd google-cloud-python
35 | cd bigquery
36 | releasetool start
37 | ```
38 | This will create a PR.
39 |
40 | **If the PR has a `autorelease: pending` label:**
41 |
42 | Upon approval and merging,
43 | `autorelease` will pick up the PR and run `releasetool tag` and release the
44 | package to their respective package managers. Autorelease will comment on the release PR with the status of the release. [Example PR](https://github.com/googleapis/nodejs-pubsub/pull/521)
45 |
46 | **Otherwise:**
47 |
48 | Once the PR has been approved and merged, you can run `releasetool tag` from
49 | anywhere in the repository to tag the commit and start CI.
50 |
51 | ```
52 | git fetch origin master
53 | git checkout origin/master
54 | releasetool tag
55 | ```
56 |
57 | ## Authenticating
58 |
59 | When first running `releasetool` you will be prompted for a GitHub token. Make
60 | sure that the token provided has `write:repo_hook` and `public_repo` scopes.
61 |
62 | ### Resetting the GitHub Token
63 |
64 | If you need to change the GitHub API token associated with releasetool, run `releasetool reset-config`. This will delete the existing token. The next time you run `releasetool start` you will be
65 | prompted to enter a new token.
66 |
67 | ## Disclaimer
68 |
69 | This is not an official Google product.
70 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
4 |
5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
6 |
7 | We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
8 |
--------------------------------------------------------------------------------
/autorelease/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleapis/releasetool/4a6e25c4a0cb6b6db1815f578012edb8cb884595/autorelease/__init__.py
--------------------------------------------------------------------------------
/autorelease/__main__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import argparse
16 | import os
17 | import sys
18 |
19 | from autorelease import tag, trigger
20 |
21 | # TODO(busunkim): Fetch magictoken from KMS once KMS setup is complete.
22 | _KEYSTORE_GITHUB_TOKEN_LOCATION = "73713_yoshi-automation-github-key"
23 |
24 |
25 | def _determine_github_token(github_token):
26 | """Automatically use the GitHub token provided by Keystore if needed."""
27 | if github_token is not None:
28 | return github_token
29 |
30 | if "KOKORO_KEYSTORE_DIR" in os.environ:
31 | filename = os.path.join(
32 | os.environ["KOKORO_KEYSTORE_DIR"], _KEYSTORE_GITHUB_TOKEN_LOCATION
33 | )
34 |
35 | if os.path.exists(filename):
36 | with open(filename, "r", encoding="utf-8") as fh:
37 | return fh.read().strip()
38 |
39 | return None
40 |
41 |
42 | def main():
43 | parser = argparse.ArgumentParser()
44 | parser.add_argument("--report")
45 | parser.add_argument("--github-token", default=os.environ.get("GITHUB_TOKEN"))
46 | parser.add_argument(
47 | "--kokoro-credentials", default=os.environ.get("AUTORELEASE_KOKORO_CREDENTIALS")
48 | )
49 | parser.add_argument("--pull", default=None)
50 | parser.add_argument("--release", default=None)
51 | parser.add_argument("--lang", default=None)
52 | parser.add_argument("command")
53 | parser.add_argument("--multi-scm-name")
54 |
55 | args = parser.parse_args()
56 |
57 | args.github_token = _determine_github_token(args.github_token)
58 |
59 | if args.command == "tag":
60 | report = tag.main(args.github_token, args.kokoro_credentials)
61 |
62 | if args.report:
63 | report.write(args.report)
64 |
65 | if report.failures:
66 | sys.exit(2)
67 | else:
68 | return
69 | elif args.command == "trigger":
70 | report = trigger.main(args.github_token, args.kokoro_credentials)
71 |
72 | if args.report:
73 | report.write(args.report)
74 |
75 | if report.failures:
76 | sys.exit(2)
77 | else:
78 | return
79 | elif args.command == "trigger-single":
80 | if args.release:
81 | if not args.lang:
82 | raise Exception("missing required arg --lang")
83 | report = trigger.trigger_for_release(
84 | args.github_token,
85 | args.kokoro_credentials,
86 | args.release,
87 | trigger.to_pysafe_language_name(args.lang),
88 | args.multi_scm_name,
89 | )
90 | if not args.pull:
91 | raise Exception("missing required arg --pull")
92 | else:
93 | report = trigger.trigger_single(
94 | args.github_token,
95 | args.kokoro_credentials,
96 | args.pull,
97 | multi_scm_name=args.multi_scm_name,
98 | )
99 |
100 | if args.report:
101 | report.write(args.report)
102 |
103 | if report.failures:
104 | sys.exit(2)
105 | else:
106 | return
107 | else:
108 | print(f"Unknown command {args.command}.")
109 | sys.exit(1)
110 |
111 |
112 | if __name__ == "__main__":
113 | main()
114 |
--------------------------------------------------------------------------------
/autorelease/common.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import json
16 | import re
17 |
18 | from typing import Dict, Any, Callable
19 | from autorelease.github import GitHub
20 |
21 |
22 | def _determine_language(
23 | fetch_repos_json: Callable[[], str], repo_full_name: str
24 | ) -> str:
25 | python_tools = ["synthtool"]
26 |
27 | if repo_full_name.split("/")[1] in python_tools:
28 | return "python_tool"
29 |
30 | repos = json.loads(fetch_repos_json())["repos"]
31 | for repo in repos:
32 | if repo_full_name == repo["repo"]:
33 | return repo["language"]
34 |
35 | raise Exception("Unable to determine repository language.")
36 |
37 |
38 | def determine_language(gh: GitHub, pull: Dict[str, Any]) -> str:
39 | name = pull["base"]["repo"]["full_name"]
40 |
41 | def fetch_repos_json():
42 | return gh.get_contents("googleapis/sloth", "repos.json")
43 |
44 | return _determine_language(fetch_repos_json, name)
45 |
46 |
47 | """Language names as reported by github."""
48 | _SILVER_LANGUAGE_NAMES = {
49 | "JavaScript": "nodejs",
50 | "TypeScript": "nodejs",
51 | "Python": "python",
52 | "Java": "java",
53 | "PHP": "php",
54 | "Ruby": "ruby",
55 | "Go": "go",
56 | "C#": "dotnet",
57 | "Elixir": "elixer",
58 | }
59 |
60 |
61 | def guess_language(gh: GitHub, repo_full_name: str) -> str:
62 | special_cases = {
63 | # 1 special case inherited from the original determine_language() code.
64 | "googleapis/synthtool": "python_tool",
65 | # 2 more special cases where the most prevalent language is not the same as
66 | # what was declared in the old repos.json.
67 | "GoogleCloudPlatform/cloud-code-samples": "dotnet",
68 | "googleapis/doc-templates": "python",
69 | # special case for cndb testing protos, set to java instead of proto.
70 | "googleapis/cndb-client-testing-protos": "java",
71 | }
72 | special_case = special_cases.get(repo_full_name)
73 | if special_case:
74 | return special_case
75 |
76 | # Does the repo name have a language name in it?
77 | lang_names = {
78 | "cpp",
79 | "dotnet",
80 | "elixir",
81 | "go",
82 | "java",
83 | "nodejs",
84 | "php",
85 | "python",
86 | "python_tool",
87 | "ruby",
88 | }
89 | chunks = set(re.split("/|-", repo_full_name))
90 | x = lang_names.intersection(chunks)
91 | if 1 == len(x):
92 | return x.pop() # Found the language name in the repo name
93 |
94 | # Fetch how many lines of each language are found in the repo.
95 | languages = gh.get_languages(repo_full_name)
96 | ranks = [
97 | (count, lang)
98 | for (lang, count) in languages.items()
99 | # Ignore languages we don't care about, like Shell.
100 | if lang in _SILVER_LANGUAGE_NAMES
101 | ]
102 | ranks.sort(reverse=True)
103 | if ranks:
104 | # Return the most prevalent language in the repo.
105 | return _SILVER_LANGUAGE_NAMES[ranks[0][1]]
106 | else:
107 | raise Exception("Unable to determine repository language.")
108 |
--------------------------------------------------------------------------------
/autorelease/kokoro.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module talks to Kokoro via Pub/Sub messages to devrel-prod.googleplex.com."""
16 |
17 | import base64
18 |
19 | from google.auth.transport import requests
20 | from google.oauth2 import service_account
21 | import google.auth
22 |
23 | from protos import kokoro_api_pb2
24 |
25 |
26 | _DEVREL_PROD_KOKORO_TOPIC = (
27 | "projects/google.com:devrel-library-tracker-prod/topics/kokoro"
28 | )
29 |
30 |
31 | def _send_pubsub_message(
32 | session: requests.AuthorizedSession, topic: str, data: str
33 | ) -> dict:
34 | url = f"https://pubsub.googleapis.com/v1/{topic}:publish"
35 | encoded_data = base64.b64encode(data.encode("utf-8"))
36 |
37 | publish_request = {"messages": [{"data": encoded_data.decode("utf-8")}]}
38 |
39 | resp = session.post(url, json=publish_request)
40 | resp.raise_for_status()
41 |
42 | return resp
43 |
44 |
45 | def _make_build_request(
46 | job_name: str, sha: str, env_vars: dict = None, multi_scm_name: str = ""
47 | ) -> str:
48 | request = kokoro_api_pb2.BuildRequest(
49 | full_job_name=job_name,
50 | )
51 |
52 | # If the job is configured to use multiple SCMs, then we need to send
53 | # the scm_name field as part of the request
54 | if multi_scm_name:
55 | request.multi_scm_revision.github_scm_revision.add(
56 | name=multi_scm_name, commit_sha=sha
57 | )
58 | else:
59 | request.scm_revision.github_scm_revision.commit_sha = sha
60 |
61 | if env_vars:
62 | for key, value in env_vars.items():
63 | request.env_vars[key] = value
64 |
65 | # Transform into a string for TextFormat. See:
66 | # https://sites.google.com/a/google.com/protocol-buffers/user-docs/miscellaneous-howtos/text-format-examples
67 | return str(request)
68 |
69 |
70 | def make_authorized_session(credentials_file: str) -> requests.AuthorizedSession:
71 | """Create a scoped, authorized requests session using a service account
72 |
73 | Args:
74 | credentials_file {str}: Path to service account file
75 |
76 | Returns:
77 | requests.AuthorizedSession: The authorized requests session
78 | """
79 | credentials = service_account.Credentials.from_service_account_file(
80 | credentials_file, scopes=["https://www.googleapis.com/auth/pubsub"]
81 | )
82 | session = requests.AuthorizedSession(credentials)
83 | return session
84 |
85 |
86 | def make_adc_session() -> requests.AuthorizedSession:
87 | """Create a scoped, authorized requests session using ADC
88 |
89 | Returns:
90 | requests.AuthorizedSession: The authorized requests session
91 |
92 | Raises:
93 | DefaultCredentialsError if no credentials found
94 | """
95 | credentials, _ = google.auth.default(
96 | scopes=["https://www.googleapis.com/auth/pubsub"]
97 | )
98 | session = requests.AuthorizedSession(credentials)
99 | return session
100 |
101 |
102 | def trigger_build(
103 | session: requests.AuthorizedSession,
104 | job_name: str,
105 | sha: str,
106 | env_vars: dict = None,
107 | multi_scm_name: str = "",
108 | ):
109 | build_request = _make_build_request(
110 | job_name, sha, env_vars=env_vars, multi_scm_name=multi_scm_name
111 | )
112 | _send_pubsub_message(session, _DEVREL_PROD_KOKORO_TOPIC, build_request)
113 |
--------------------------------------------------------------------------------
/autorelease/report.xml.j2:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% for result in reporter.results %}
4 |
5 |
6 | {% if result.error %}
7 | {{result.output|e}}
8 | {% else %}
9 | {{result.output|e}}
10 | {% endif %}
11 |
12 |
13 | {% endfor %}
14 |
15 |
--------------------------------------------------------------------------------
/autorelease/reporter.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module is used for reporting status via junit XML files that can be
16 | consumed by Kokoro/Sponge."""
17 |
18 | import io
19 | import os
20 |
21 | import attr
22 | import jinja2
23 |
24 | with open(os.path.join(os.path.dirname(__file__), "report.xml.j2"), "r") as fh:
25 | _TEMPLATE = jinja2.Template(fh.read())
26 |
27 |
28 | @attr.s(auto_attribs=True, slots=True)
29 | class Result:
30 | name: str
31 | error: bool = False
32 | skipped: bool = False
33 | _output: io.StringIO = attr.ib(factory=io.StringIO)
34 |
35 | @property
36 | def output(self):
37 | return self._output.getvalue()
38 |
39 | def print(self, *args, **kwargs):
40 | print(*args, **kwargs)
41 | print(*args, file=self._output, **kwargs)
42 |
43 |
44 | class Reporter:
45 | def __init__(self, name):
46 | self.name = name
47 | self.results = []
48 |
49 | @property
50 | def failures(self):
51 | return len([result for result in self.results if result.error])
52 |
53 | @property
54 | def skips(self):
55 | return len([result for result in self.results if result.skipped])
56 |
57 | def add(self, result):
58 | self.results.append(result)
59 |
60 | def render(self):
61 | return _TEMPLATE.render(reporter=self)
62 |
63 | def write(self, filename):
64 | with open(filename, "w") as fh:
65 | fh.write(self.render())
66 |
--------------------------------------------------------------------------------
/autorelease/tag.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module handles automatically running releasetool tag against all pending PRs."""
16 |
17 | import importlib
18 |
19 | from autorelease import common, github, kokoro, reporter
20 | from releasetool.commands.common import TagContext
21 | import releasetool.github
22 |
23 | LANGUAGE_ALLOWLIST = []
24 |
25 | # This is only used in the autorelease/tag workflows.
26 | # Direct, explicit triggering on repos in this list is still possible.
27 | REPO_DENYLIST = []
28 |
29 |
30 | ORGANIZATIONS_TO_SCAN = ["googleapis", "GoogleCloudPlatform"]
31 |
32 |
33 | def run_releasetool_tag(lang: str, gh: github.GitHub, pull: dict) -> TagContext:
34 | """Runs releasetool tag using external config."""
35 | language_module = importlib.import_module(f"releasetool.commands.tag.{lang}")
36 | ctx = TagContext()
37 | ctx.interactive = False
38 | # TODO(busunkim): Use proxy once KMS setup is complete.
39 | ctx.github = releasetool.github.GitHub(gh.token, use_proxy=False)
40 | ctx.token = gh.token
41 | ctx.upstream_repo = pull["base"]["repo"]["full_name"]
42 | ctx.release_pr = pull
43 | return language_module.tag(ctx)
44 |
45 |
46 | def process_issue(
47 | kokoro_session, gh: github.GitHub, issue: dict, result: reporter.Result
48 | ) -> None:
49 | # Reify the "issue" into a full pull request object from github. This
50 | # is necessary because github gives us back an issue object, but it
51 | # doesn't contain all of the PR info.
52 | pull = gh.get_url(issue["pull_request"]["url"])
53 | repo_full_name = pull["base"]["repo"]["full_name"]
54 |
55 | # Determine language.
56 | lang = common.guess_language(gh, repo_full_name)
57 |
58 | # As part of the migration to release-please tagging, cross-reference the
59 | # language against an allowlist to allow migrating language-by-language.
60 | if lang not in LANGUAGE_ALLOWLIST:
61 | result.skipped = True
62 | result.print(f"Language {lang} not in allowlist, skipping.")
63 | return
64 | # As part of the migration to release-please tagging, cross-reference the
65 | # repository against a denylist to allow migrating repo-by-repo.
66 | for denied_repo in REPO_DENYLIST:
67 | if denied_repo == repo_full_name:
68 | result.skipped = True
69 | result.print(f"Repo {denied_repo} in denylist, skipping.")
70 | return
71 |
72 | # Before doing any processing, check to make sure the PR was actually merged.
73 | # "closed" PRs can be merged or just closed without merging.
74 | if not pull.get("merged_at"):
75 | result.skipped = True
76 | result.print("Closed, not merged, skipping.")
77 | # Remove the label so we don't continue processing it.
78 | gh.update_pull_labels(
79 | pull, add=["autorelease: closed"], remove=["autorelease: pending"]
80 | )
81 | return
82 |
83 | # Run releasetool tag for the PR.
84 | ctx = run_releasetool_tag(lang, gh, pull)
85 |
86 | # Trigger Kokoro release build
87 | result.print(f"Triggering {ctx.kokoro_job_name} using {ctx.release_tag}")
88 | if ctx.kokoro_job_name and ctx.release_tag:
89 | kokoro.trigger_build(
90 | kokoro_session,
91 | job_name=ctx.kokoro_job_name,
92 | sha=ctx.release_tag,
93 | env_vars={"AUTORELEASE_PR": pull["html_url"]},
94 | )
95 |
96 |
97 | def main(github_token: str, kokoro_credentials: str) -> reporter.Reporter:
98 | report = reporter.Reporter("autorelease.tag")
99 | # TODO(busunkim): Use proxy once KMS setup is complete.
100 | gh = github.GitHub(github_token, use_proxy=False)
101 |
102 | if kokoro_credentials:
103 | kokoro_session = kokoro.make_authorized_session(kokoro_credentials)
104 | else:
105 | kokoro_session = kokoro.make_adc_session()
106 |
107 | # First, we need to get a list of all pull requests (GitHub calls these "issues")
108 | # that are merged ("closed") and have the label "autorelease: pending".
109 | list_result = reporter.Result("list issues")
110 | report.add(list_result)
111 |
112 | all_issues = []
113 | for org in ORGANIZATIONS_TO_SCAN:
114 | try:
115 | issues = gh.list_org_issues(
116 | org=org,
117 | # Must be merged ("closed").
118 | state="closed",
119 | # Must be labeled with "autorelease: pending"
120 | labels="autorelease: pending",
121 | )
122 |
123 | # Just in case any non-PRs got in here.
124 | issues = [result for result in issues if "pull_request" in result]
125 |
126 | all_issues.extend(issues)
127 |
128 | # Exceptions while getting the list of pull requests constitutes a total failure.
129 | except Exception as exc:
130 | list_result.error = True
131 | list_result.print(exc)
132 |
133 | # Print out our findings as a checkpoint.
134 | list_result.print("Working set:")
135 | for issue in all_issues:
136 | list_result.print(f" * {issue['title']}: {issue['pull_request']['html_url']}")
137 |
138 | # For each pull request, execute releasetool tag for it.
139 | for issue in all_issues:
140 | result = reporter.Result(f"{issue['title']}")
141 | report.add(result)
142 | result.print(
143 | f"Processing {issue['title']}: {issue['pull_request']['html_url']}"
144 | )
145 |
146 | try:
147 | process_issue(kokoro_session, gh, issue, result)
148 | # Failing any one PR is fine, just record it in the log and continue.
149 | except Exception as exc:
150 | result.error = True
151 | result.print(f"{exc!r}")
152 |
153 | return report
154 |
--------------------------------------------------------------------------------
/noxfile.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import nox
16 | import pathlib
17 |
18 | CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
19 |
20 | ALL_PYTHON = ["3.9", "3.10", "3.11", "3.12"]
21 |
22 | @nox.session(python='3.8')
23 | def blacken(session):
24 | session.install('black')
25 | session.run('black', 'autorelease', 'releasetool', 'tests')
26 |
27 |
28 | @nox.session(python='3.8')
29 | def lint(session):
30 | session.install('mypy==0.812', 'flake8', 'black')
31 | session.install('-e', '.')
32 | session.run('black', '--check', 'autorelease', 'releasetool', 'tests')
33 | session.run('flake8', 'autorelease', 'releasetool', 'tests')
34 | session.run(
35 | 'mypy',
36 | '--no-strict-optional',
37 | '--ignore-missing-imports',
38 | 'releasetool')
39 |
40 |
41 | @nox.session(python=ALL_PYTHON)
42 | def test(session):
43 | # Use a constraints file for the specific python runtime version.
44 | # We do this to make sure that we're testing against the lowest
45 | # supported version of a dependency.
46 | session.install("-r", f"{CURRENT_DIRECTORY}/requirements-dev.txt")
47 | constraints_file = f"{CURRENT_DIRECTORY}/testing/constraints-{session.python}.txt"
48 | session.install('-e', '.', "-r", constraints_file)
49 | session.run('pytest', 'tests', *session.posargs)
50 |
--------------------------------------------------------------------------------
/protos/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleapis/releasetool/4a6e25c4a0cb6b6db1815f578012edb8cb884595/protos/__init__.py
--------------------------------------------------------------------------------
/protos/kokoro_api.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Protocol messages and service definition for Kokoro master API.
16 | // This has been heavily modified from the upstream source to just contain
17 | // the fields we need.
18 | // Source: https://cs.corp.google.com/piper///depot/google3/devtools/kokoro/api/proto/kokoro_api.proto
19 |
20 | syntax = "proto2";
21 |
22 | package devtools.kokoro.api.proto;
23 |
24 | // Request for new build of a job.
25 | message BuildRequest {
26 | optional string full_job_name = 1;
27 |
28 | // The specific revision to build. If not set, Jenkins will use whatever
29 | // revision is latest when the build actually executes.
30 | optional ScmRevision scm_revision = 3;
31 |
32 | // Use this for multi scm jobs
33 | optional MultiScmRevision multi_scm_revision = 9;
34 |
35 | // The input file/directory paths for a kokoro build, they should be
36 | // accessible by GoogleFile. Those files will be downloaded to src directory
37 | // and available before build step starts.
38 | repeated string input_file_paths = 4;
39 |
40 | // Build parameters passed to job as environment variable key value pairs.
41 | // Multiple env_vars blocks are allowed. See
42 | // http://cs/piper///depot/google3/devtools/kokoro/config/proto/build.proto?type=cs&q=project:kokoro+file:build.proto+%22env_vars+%3D+6;%22
43 | map env_vars = 6;
44 |
45 | // Keystore Resources to be fetched pre build. These will be downloaded to
46 | // src/keystore directory and will be available before build starts.
47 | // Resource with configID X and name Y will be placed at src/keystore/X_Y.
48 | // optional .kokoro.FetchKeystore fetch_keystore = 7;
49 |
50 | // Kokoro build config parameters key value pairs. The key is the variable in
51 | // the build config and the value is the value that you want to replace the
52 | // variable in the build config (string type only).
53 | // Multiple build_params blocks are allowed. See build_params in
54 | // https://cs.corp.google.com/piper///depot/google3/devtools/kokoro/config/proto/build.proto?dr=C&rcl=206864844&g=0&l=18
55 | map build_params = 12;
56 |
57 | // next index: 13;
58 | }
59 |
60 | // Represents a specific revision in one of the supported SCM systems.
61 | message ScmRevision {
62 | oneof scmrevision {
63 | GithubScmRevision github_scm_revision = 4;
64 | }
65 | }
66 |
67 | // Represents a specific revision for multi-scm.
68 | message MultiScmRevision {
69 | repeated GithubScmRevision github_scm_revision = 4;
70 | }
71 |
72 | // Represents a specific revision in GitHub repository.
73 | message GithubScmRevision {
74 | // Name of the scm that this revision belongs to. This name should be the same
75 | // as multi_scm.github_scm.name
76 | optional string name = 4;
77 |
78 | optional string commit_sha = 1;
79 |
80 | // Pull request number in GitHub. If this field is present, then commit_sha
81 | // will be the commit in the pull request.
82 | optional string pull_request_number = 2;
83 |
84 | // The URL for the change in Github repository. This field will be populated
85 | // in the build result, and will be ignored if it is specified in the query.
86 | optional string repository_url = 3;
87 |
88 | // Owner of the repository. If supplied, this field will override the owner
89 | // field in job.proto.
90 | optional string owner = 5;
91 |
92 | // GitHub repository name. If supplied, this field will override the
93 | // repository field in job.proto.
94 | optional string repository = 6;
95 |
96 | // next index: 7
97 | }
98 |
--------------------------------------------------------------------------------
/protos/kokoro_api_pb2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by the protocol buffer compiler. DO NOT EDIT!
3 | # source: protos/kokoro_api.proto
4 | """Generated protocol buffer code."""
5 | from google.protobuf import descriptor as _descriptor
6 | from google.protobuf import descriptor_pool as _descriptor_pool
7 | from google.protobuf import message as _message
8 | from google.protobuf import reflection as _reflection
9 | from google.protobuf import symbol_database as _symbol_database
10 | # @@protoc_insertion_point(imports)
11 |
12 | _sym_db = _symbol_database.Default()
13 |
14 |
15 |
16 |
17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17protos/kokoro_api.proto\x12\x19\x64\x65vtools.kokoro.api.proto\"\xc2\x03\n\x0c\x42uildRequest\x12\x15\n\rfull_job_name\x18\x01 \x01(\t\x12<\n\x0cscm_revision\x18\x03 \x01(\x0b\x32&.devtools.kokoro.api.proto.ScmRevision\x12G\n\x12multi_scm_revision\x18\t \x01(\x0b\x32+.devtools.kokoro.api.proto.MultiScmRevision\x12\x18\n\x10input_file_paths\x18\x04 \x03(\t\x12\x46\n\x08\x65nv_vars\x18\x06 \x03(\x0b\x32\x34.devtools.kokoro.api.proto.BuildRequest.EnvVarsEntry\x12N\n\x0c\x62uild_params\x18\x0c \x03(\x0b\x32\x38.devtools.kokoro.api.proto.BuildRequest.BuildParamsEntry\x1a.\n\x0c\x45nvVarsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x32\n\x10\x42uildParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"i\n\x0bScmRevision\x12K\n\x13github_scm_revision\x18\x04 \x01(\x0b\x32,.devtools.kokoro.api.proto.GithubScmRevisionH\x00\x42\r\n\x0bscmrevision\"]\n\x10MultiScmRevision\x12I\n\x13github_scm_revision\x18\x04 \x03(\x0b\x32,.devtools.kokoro.api.proto.GithubScmRevision\"\x8d\x01\n\x11GithubScmRevision\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x12\n\ncommit_sha\x18\x01 \x01(\t\x12\x1b\n\x13pull_request_number\x18\x02 \x01(\t\x12\x16\n\x0erepository_url\x18\x03 \x01(\t\x12\r\n\x05owner\x18\x05 \x01(\t\x12\x12\n\nrepository\x18\x06 \x01(\t')
18 |
19 |
20 |
21 | _BUILDREQUEST = DESCRIPTOR.message_types_by_name['BuildRequest']
22 | _BUILDREQUEST_ENVVARSENTRY = _BUILDREQUEST.nested_types_by_name['EnvVarsEntry']
23 | _BUILDREQUEST_BUILDPARAMSENTRY = _BUILDREQUEST.nested_types_by_name['BuildParamsEntry']
24 | _SCMREVISION = DESCRIPTOR.message_types_by_name['ScmRevision']
25 | _MULTISCMREVISION = DESCRIPTOR.message_types_by_name['MultiScmRevision']
26 | _GITHUBSCMREVISION = DESCRIPTOR.message_types_by_name['GithubScmRevision']
27 | BuildRequest = _reflection.GeneratedProtocolMessageType('BuildRequest', (_message.Message,), {
28 |
29 | 'EnvVarsEntry' : _reflection.GeneratedProtocolMessageType('EnvVarsEntry', (_message.Message,), {
30 | 'DESCRIPTOR' : _BUILDREQUEST_ENVVARSENTRY,
31 | '__module__' : 'protos.kokoro_api_pb2'
32 | # @@protoc_insertion_point(class_scope:devtools.kokoro.api.proto.BuildRequest.EnvVarsEntry)
33 | })
34 | ,
35 |
36 | 'BuildParamsEntry' : _reflection.GeneratedProtocolMessageType('BuildParamsEntry', (_message.Message,), {
37 | 'DESCRIPTOR' : _BUILDREQUEST_BUILDPARAMSENTRY,
38 | '__module__' : 'protos.kokoro_api_pb2'
39 | # @@protoc_insertion_point(class_scope:devtools.kokoro.api.proto.BuildRequest.BuildParamsEntry)
40 | })
41 | ,
42 | 'DESCRIPTOR' : _BUILDREQUEST,
43 | '__module__' : 'protos.kokoro_api_pb2'
44 | # @@protoc_insertion_point(class_scope:devtools.kokoro.api.proto.BuildRequest)
45 | })
46 | _sym_db.RegisterMessage(BuildRequest)
47 | _sym_db.RegisterMessage(BuildRequest.EnvVarsEntry)
48 | _sym_db.RegisterMessage(BuildRequest.BuildParamsEntry)
49 |
50 | ScmRevision = _reflection.GeneratedProtocolMessageType('ScmRevision', (_message.Message,), {
51 | 'DESCRIPTOR' : _SCMREVISION,
52 | '__module__' : 'protos.kokoro_api_pb2'
53 | # @@protoc_insertion_point(class_scope:devtools.kokoro.api.proto.ScmRevision)
54 | })
55 | _sym_db.RegisterMessage(ScmRevision)
56 |
57 | MultiScmRevision = _reflection.GeneratedProtocolMessageType('MultiScmRevision', (_message.Message,), {
58 | 'DESCRIPTOR' : _MULTISCMREVISION,
59 | '__module__' : 'protos.kokoro_api_pb2'
60 | # @@protoc_insertion_point(class_scope:devtools.kokoro.api.proto.MultiScmRevision)
61 | })
62 | _sym_db.RegisterMessage(MultiScmRevision)
63 |
64 | GithubScmRevision = _reflection.GeneratedProtocolMessageType('GithubScmRevision', (_message.Message,), {
65 | 'DESCRIPTOR' : _GITHUBSCMREVISION,
66 | '__module__' : 'protos.kokoro_api_pb2'
67 | # @@protoc_insertion_point(class_scope:devtools.kokoro.api.proto.GithubScmRevision)
68 | })
69 | _sym_db.RegisterMessage(GithubScmRevision)
70 |
71 | if _descriptor._USE_C_DESCRIPTORS == False:
72 |
73 | DESCRIPTOR._options = None
74 | _BUILDREQUEST_ENVVARSENTRY._options = None
75 | _BUILDREQUEST_ENVVARSENTRY._serialized_options = b'8\001'
76 | _BUILDREQUEST_BUILDPARAMSENTRY._options = None
77 | _BUILDREQUEST_BUILDPARAMSENTRY._serialized_options = b'8\001'
78 | _BUILDREQUEST._serialized_start=55
79 | _BUILDREQUEST._serialized_end=505
80 | _BUILDREQUEST_ENVVARSENTRY._serialized_start=407
81 | _BUILDREQUEST_ENVVARSENTRY._serialized_end=453
82 | _BUILDREQUEST_BUILDPARAMSENTRY._serialized_start=455
83 | _BUILDREQUEST_BUILDPARAMSENTRY._serialized_end=505
84 | _SCMREVISION._serialized_start=507
85 | _SCMREVISION._serialized_end=612
86 | _MULTISCMREVISION._serialized_start=614
87 | _MULTISCMREVISION._serialized_end=707
88 | _GITHUBSCMREVISION._serialized_start=710
89 | _GITHUBSCMREVISION._serialized_end=851
90 | # @@protoc_insertion_point(module_scope)
91 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | filterwarnings =
3 | # treat all warnings as errors
4 | error
5 | # Remove once https://github.com/protocolbuffers/protobuf/issues/15077 is fixed
6 | ignore:.*custom tp_new.*in Python 3.14:DeprecationWarning
7 | # Remove once https://github.com/dateutil/dateutil/issues/1314 is fixed
8 | ignore:.*datetime.datetime.utcfromtimestamp\(\) is deprecated:DeprecationWarning:dateutil.tz.tz
9 |
--------------------------------------------------------------------------------
/releasetool/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """releasetool helps make releases."""
16 |
17 | __version__ = "1.8.2"
18 |
--------------------------------------------------------------------------------
/releasetool/__main__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import functools
16 | import os
17 |
18 | import click
19 |
20 | import releasetool.secrets
21 | import releasetool.update_check
22 | import releasetool.commands.publish_reporter
23 | import releasetool.commands.start.python
24 | import releasetool.commands.start.python_tool
25 | import releasetool.commands.start.nodejs
26 | import releasetool.commands.start.java
27 | import releasetool.commands.start.ruby
28 | import releasetool.commands.start.go
29 | import releasetool.commands.tag.python
30 | import releasetool.commands.tag.python_tool
31 | import releasetool.commands.tag.nodejs
32 | import releasetool.commands.tag.java
33 | import releasetool.commands.tag.php
34 | import releasetool.commands.tag.ruby
35 | import releasetool.commands.tag.dotnet
36 |
37 |
38 | class _OptionPromptIfNone(click.Option):
39 | """A custom option that only prompts if the default value can't be
40 | determined."""
41 |
42 | _value_key = "_default_val"
43 |
44 | def get_default(self, ctx):
45 | if not hasattr(self, self._value_key):
46 | default = super(_OptionPromptIfNone, self).get_default(ctx)
47 | setattr(self, self._value_key, default)
48 | return getattr(self, self._value_key)
49 |
50 | def prompt_for_value(self, ctx):
51 | default = self.get_default(ctx)
52 |
53 | # only prompt if the default value is None
54 | if default is None:
55 | return super(_OptionPromptIfNone, self).prompt_for_value(ctx)
56 |
57 | return default
58 |
59 |
60 | @click.group(invoke_without_command=True)
61 | @click.pass_context
62 | @click.version_option(message="%(version)s")
63 | def main(ctx):
64 | if ctx.invoked_subcommand is None:
65 | return ctx.invoke(start)
66 |
67 |
68 | def _detect_language():
69 | if os.path.exists("package.json"):
70 | return "nodejs"
71 | elif os.path.exists("setup.py"):
72 | if os.path.exists("releasetool") or os.path.exists("synthtool"):
73 | return "python-tool"
74 | else:
75 | return "python"
76 | elif os.path.exists("Gemfile"):
77 | return "ruby"
78 | elif os.path.exists("pom.xml") or os.path.exists("build.gradle"):
79 | return "java"
80 | elif os.path.exists("global.json"):
81 | return "dotnet"
82 | return None
83 |
84 |
85 | _language_choices = [
86 | "python",
87 | "python-tool",
88 | "nodejs",
89 | "java",
90 | "ruby",
91 | "go",
92 | "php",
93 | "dotnet",
94 | ]
95 |
96 |
97 | def _language_option():
98 | return click.option(
99 | "--language",
100 | prompt=f"Which language ({', '.join(_language_choices)})?",
101 | type=click.Choice(_language_choices),
102 | default=_detect_language,
103 | cls=_OptionPromptIfNone,
104 | )
105 |
106 |
107 | @main.command()
108 | @_language_option()
109 | def start(language):
110 | releasetool.update_check.check_for_updates(
111 | "gcp-releasetool", print=functools.partial(click.secho, fg="magenta")
112 | )
113 |
114 | if language == "python":
115 | return releasetool.commands.start.python.start()
116 | if language == "python-tool":
117 | return releasetool.commands.start.python_tool.start()
118 | if language == "nodejs":
119 | return releasetool.commands.start.nodejs.start()
120 | if language == "java":
121 | return releasetool.commands.start.java.start()
122 | if language == "ruby":
123 | return releasetool.commands.start.ruby.start()
124 | if language == "go":
125 | return releasetool.commands.start.go.start()
126 |
127 |
128 | @main.command()
129 | @_language_option()
130 | def tag(language):
131 | if language == "python":
132 | return releasetool.commands.tag.python.tag()
133 | if language == "python-tool":
134 | return releasetool.commands.tag.python_tool.tag()
135 | if language == "nodejs":
136 | return releasetool.commands.tag.nodejs.tag()
137 | if language == "java":
138 | return releasetool.commands.tag.java.tag()
139 | if language == "php":
140 | return releasetool.commands.tag.php.tag()
141 | if language == "ruby":
142 | return releasetool.commands.tag.ruby.tag()
143 | if language == "dotnet":
144 | return releasetool.commands.tag.dotnet.tag()
145 |
146 |
147 | @main.command(name="reset-config")
148 | def reset_config():
149 | releasetool.secrets.delete_password()
150 |
151 |
152 | @main.command(name="publish-reporter-start")
153 | @click.option("--github_token", envvar="GITHUB_TOKEN", default=None)
154 | @click.option("--pr", envvar="AUTORELEASE_PR", default=None)
155 | @click.option("--app_id_path", envvar="APP_ID_PATH", default=None)
156 | @click.option("--installation_id_path", envvar="INSTALLATION_ID_PATH", default=None)
157 | @click.option("--private_key_path", envvar="GITHUB_PRIVATE_KEY_PATH", default=None)
158 | def publish_reporter_start(
159 | github_token: str,
160 | pr: str,
161 | app_id_path: str,
162 | installation_id_path: str,
163 | private_key_path: str,
164 | ):
165 | if app_id_path:
166 | github_token = github_jwt_dict(
167 | app_id_path, installation_id_path, private_key_path
168 | )
169 | releasetool.commands.publish_reporter.start(github_token, pr)
170 |
171 |
172 | @main.command(name="publish-reporter-finish")
173 | @click.option("--github_token", envvar="GITHUB_TOKEN", default=None)
174 | @click.option("--pr", envvar="AUTORELEASE_PR", default=None)
175 | @click.option("--status", type=bool, default=True)
176 | @click.option("--details", envvar="PUBLISH_DETAILS", default=None)
177 | @click.option("--app_id_path", envvar="APP_ID_PATH", default=None)
178 | @click.option("--installation_id_path", envvar="INSTALLATION_ID_PATH", default=None)
179 | @click.option("--private_key_path", envvar="GITHUB_PRIVATE_KEY_PATH", default=None)
180 | def publish_reporter_finish(
181 | github_token: str,
182 | pr: str,
183 | status: bool,
184 | details: str,
185 | app_id_path: str,
186 | installation_id_path: str,
187 | private_key_path: str,
188 | ):
189 | if app_id_path:
190 | github_token = github_jwt_dict(
191 | app_id_path, installation_id_path, private_key_path
192 | )
193 | releasetool.commands.publish_reporter.finish(github_token, pr, status, details)
194 |
195 |
196 | def github_jwt_dict(app_id_path: str, installation_id_path: str, private_key_path: str):
197 | """An app_id, installation_id, and private_key may be provided, rather
198 | than a github_token. This dictionary of values is passed to publish_reporter
199 | which exchanges them for a JWT."""
200 | return {
201 | "app_id": open(app_id_path, "r").read(),
202 | "installation_id": open(installation_id_path, "r").read(),
203 | "private_key": open(private_key_path, "r").read(),
204 | }
205 |
206 |
207 | @main.command(name="publish-reporter-script")
208 | def publish_reporter_script():
209 | releasetool.commands.publish_reporter.script()
210 |
211 |
212 | if __name__ == "__main__":
213 | main()
214 |
--------------------------------------------------------------------------------
/releasetool/circleci.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from typing import Iterator, Optional
16 | import time
17 | from datetime import datetime
18 |
19 | import requests
20 |
21 | _CIRCLE_ROOT: str = "https://circleci.com/api/v1.1"
22 |
23 |
24 | class CircleCI:
25 | def __init__(self, repository: str, vcs: str = "github") -> None:
26 | self.session: requests.Session = requests.Session()
27 | self.vcs = vcs
28 | self.repo = repository
29 |
30 | def get_latest_build_by_tag(self, tag: str, retries: int = 15) -> Optional[dict]:
31 | url = f"{_CIRCLE_ROOT}/project/{self.vcs}/{self.repo}"
32 |
33 | for retry in range(1, retries):
34 | response = self.session.get(url)
35 | response.raise_for_status()
36 | for build in response.json():
37 | if "branch" in build.keys() and build["vcs_tag"] == tag:
38 | return build
39 | time.sleep(retry)
40 |
41 | return None
42 |
43 | def get_latest_build_by_branch(self, branch_name: str) -> Optional[dict]:
44 | url = f"{_CIRCLE_ROOT}/project/{self.vcs}/{self.repo}"
45 |
46 | response = self.session.get(url)
47 | response.raise_for_status()
48 | for build in response.json():
49 | if "branch" in build.keys() and build["branch"] == branch_name:
50 | return build
51 |
52 | return None
53 |
54 | def get_fresh_build_by_branch(
55 | self, branch_name: str, seconds_fresh: int = 60, retries: int = 15
56 | ) -> Optional[dict]:
57 | """
58 | Find a build that is less than seconds_fresh old. Useful if you
59 | need to find a build that isn't an old run
60 | """
61 | for retry in range(1, retries):
62 | build = self.get_latest_build_by_branch(branch_name)
63 | if not build:
64 | continue
65 | build_queued = build["queued_at"]
66 | queued_time = datetime.strptime(build_queued, "%Y-%m-%dT%H:%M:%S.%fZ")
67 | time_delta = datetime.utcnow() - queued_time
68 | if time_delta.total_seconds() <= seconds_fresh:
69 | return build
70 |
71 | # we either didn't find a build (hasn't been queued) or we
72 | # found a build but it was stale. Wait for new build to be queued.
73 | time.sleep(retry)
74 | return None
75 |
76 | def get_build(self, build_num: str) -> dict:
77 | url = f"{_CIRCLE_ROOT}/project/{self.vcs}/{self.repo}/{build_num}"
78 | response = self.session.get(url)
79 | response.raise_for_status()
80 | return response.json()
81 |
82 | def get_link_to_build(self, build_num: int):
83 | # API vcs and FE vcs are different
84 | vcs_map = {"github": "gh"}
85 | if self.vcs in vcs_map.keys():
86 | url_vcs = vcs_map[self.vcs]
87 | else:
88 | # if we don't have it in the mapping provide it directly.
89 | url_vcs = self.vcs
90 |
91 | # https://circleci.com/gh/GitHubUser/RepositoryName/1234
92 | return f"https://circleci.com/{url_vcs}/{self.repo}/{build_num}"
93 |
94 | def get_build_status_generator(self, build_num: str) -> Iterator[str]:
95 | """
96 | Returns a generator that polls circle for the status of a branch. It
97 | continues to return results until it enters a finished state
98 | """
99 | """
100 | lifecycle_states = [
101 | "queued", "scheduled", "not_run", "not_running", "running",
102 | "finished" ]
103 |
104 | build_status_states = [
105 | "retried", "canceled", "infrastructure_fail", "timedout",
106 | "not_run", "running", "failed", "queued", "scheduled",
107 | "not_running", "no_tests", "fixed", "success" ]
108 | """
109 | build = self.get_build(build_num)
110 | while "lifecycle" in build.keys() and build["lifecycle"] != "finished":
111 | yield build["status"]
112 | time.sleep(10)
113 | build = self.get_build(build_num)
114 | yield build["status"]
115 | return
116 |
--------------------------------------------------------------------------------
/releasetool/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleapis/releasetool/4a6e25c4a0cb6b6db1815f578012edb8cb884595/releasetool/commands/__init__.py
--------------------------------------------------------------------------------
/releasetool/commands/common.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import datetime
16 | from typing import Optional, Sequence, Tuple
17 | from urllib import parse
18 |
19 | import attr
20 | import click
21 | import pyperclip
22 | from dateutil import tz
23 | import requests
24 |
25 | import releasetool.filehelpers
26 | import releasetool.git
27 | import releasetool.github
28 | import releasetool.secrets
29 |
30 |
31 | @attr.s(auto_attribs=True, slots=True)
32 | class BaseContext:
33 | interactive: bool = True
34 |
35 |
36 | @attr.s(auto_attribs=True, slots=True)
37 | class GitHubContext(BaseContext):
38 | github: Optional[releasetool.github.GitHub] = None
39 | origin_user: Optional[str] = None
40 | origin_repo: Optional[str] = None
41 | upstream_name: Optional[str] = None
42 | upstream_repo: Optional[str] = None
43 | package_name: Optional[str] = None
44 | changes: Sequence[str] = ()
45 | release_notes: Optional[str] = None
46 |
47 |
48 | @attr.s(auto_attribs=True, slots=True)
49 | class TagContext(GitHubContext):
50 | release_pr: Optional[dict] = None
51 | release_tag: Optional[str] = None
52 | release_version: Optional[str] = None
53 | github_release: Optional[dict] = None
54 | kokoro_job_name: Optional[str] = None
55 | fusion_url: Optional[str] = None
56 | token: Optional[str] = None
57 |
58 |
59 | def _determine_origin(ctx: GitHubContext) -> None:
60 | remotes = releasetool.git.get_github_remotes()
61 | origin = remotes["origin"]
62 | ctx.origin_user = origin.split("/")[0]
63 | ctx.origin_repo = origin
64 |
65 |
66 | def _determine_upstream(ctx: GitHubContext, owners: Tuple[str, ...]) -> None:
67 | remotes = releasetool.git.get_github_remotes()
68 | repos = {
69 | name: repo for name, repo in remotes.items() if repo.lower().startswith(owners)
70 | }
71 |
72 | if not repos:
73 | raise ValueError("Unable to determine the upstream GitHub repo. :(")
74 |
75 | if len(repos) > 1:
76 | options = "\n".join(f" * {name}: {repo}" for name, repo in repos.items())
77 | choice = click.prompt(
78 | click.style(
79 | f"More than one possible upstream remote was found."
80 | f"\n\n{options}\n\n"
81 | f"Please enter the *name* of one you want to use",
82 | fg="yellow",
83 | ),
84 | default="origin",
85 | )
86 | try:
87 | upstream = choice, repos[choice]
88 | except KeyError:
89 | click.secho(f"No remote named {choice}!", fg="red")
90 | raise click.Abort()
91 | else:
92 | upstream = repos.popitem()
93 |
94 | ctx.upstream_name, ctx.upstream_repo = upstream
95 |
96 |
97 | def setup_github_context(
98 | ctx: GitHubContext,
99 | owners: Tuple[str, ...] = ("googlecloudplatform", "googleapis", "google"),
100 | ) -> None:
101 | click.secho("> Determining GitHub context.", fg="cyan")
102 | github_token = releasetool.secrets.ensure_password(
103 | "github",
104 | "Please provide your GitHub API token with write:repo_hook and "
105 | "public_repo (https://github.com/settings/tokens)",
106 | )
107 |
108 | ctx.github = releasetool.github.GitHub(
109 | releasetool.github.GitHubToken(github_token, "Bearer")
110 | )
111 |
112 | _determine_origin(ctx)
113 | _determine_upstream(ctx, owners)
114 |
115 | click.secho(f"Origin: {ctx.origin_repo}, Upstream: {ctx.upstream_repo}")
116 |
117 | # Compare upstream/master with master
118 | click.secho(f"> Comparing {ctx.upstream_name}/master to master.", fg="cyan")
119 |
120 | if releasetool.git.get_latest_commit(
121 | f"{ctx.upstream_name}/master"
122 | ) == releasetool.git.get_latest_commit("master"):
123 | click.echo(f"master is up to date with {ctx.upstream_name}/master")
124 | else:
125 | click.secho(
126 | f"WARNING: master is not up to date with {ctx.upstream_name}/master",
127 | fg="red",
128 | )
129 | if click.confirm("> Would you like to continue?") is False:
130 | exit()
131 |
132 |
133 | def edit_release_notes(ctx: GitHubContext) -> None:
134 | click.secho("> Opening your editor to finalize release notes.", fg="cyan")
135 | release_notes = (
136 | datetime.datetime.now(datetime.timezone.utc)
137 | .astimezone(tz.gettz("US/Pacific"))
138 | .strftime("%m-%d-%Y %H:%M %Z\n\n")
139 | )
140 | release_notes += "\n".join(f"- {change}" for change in ctx.changes)
141 | release_notes += "\n\n### ".join(
142 | [
143 | "",
144 | "Implementation Changes",
145 | "New Features",
146 | "Dependencies",
147 | "Documentation",
148 | "Internal / Testing Changes",
149 | ]
150 | )
151 | ctx.release_notes = releasetool.filehelpers.open_editor_with_tempfile(
152 | release_notes, "release-notes.md"
153 | ).strip()
154 |
155 |
156 | def release_exists(ctx: TagContext) -> bool:
157 | try:
158 | release = ctx.github.get_release(ctx.upstream_repo, ctx.release_tag)
159 | tag_sha = ctx.github.get_tag_sha(ctx.upstream_repo, ctx.release_tag)
160 | # If a 404 or similar happened, return False as it indicates the release
161 | # doesn't exit.
162 | except requests.HTTPError:
163 | return False
164 |
165 | if release is not None and tag_sha == ctx.release_pr["merge_commit_sha"]:
166 | return True
167 | else:
168 | return False
169 |
170 |
171 | def publish_via_kokoro(ctx: TagContext) -> None:
172 | kokoro_url = "https://fusion2.corp.google.com/ci;prev=s/kokoro/prod"
173 | ctx.fusion_url = f"{kokoro_url}:{parse.quote_plus(ctx.kokoro_job_name)}"
174 |
175 | if ctx.interactive:
176 | pyperclip.copy(ctx.release_tag)
177 |
178 | click.secho(
179 | "> Trigger the Kokoro build with the commitish below to publish to PyPI. The commitish has been copied to the clipboard.",
180 | fg="cyan",
181 | )
182 |
183 | click.secho(f"Kokoro build URL:\t\t{click.style(ctx.fusion_url, underline=True)}")
184 | click.secho(f"Commitish:\t{click.style(ctx.release_tag, bold=True)}")
185 |
186 | if ctx.interactive:
187 | if click.confirm("Would you like to go the Kokoro build page?", default=True):
188 | click.launch(ctx.fusion_url)
189 |
--------------------------------------------------------------------------------
/releasetool/commands/publish_reporter.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Used by publish CI jobs to report status back to GitHub."""
16 |
17 | import os
18 | import importlib
19 | import re
20 | from typing import cast, Tuple, Union
21 | from requests import HTTPError
22 |
23 | import releasetool.github
24 |
25 |
26 | def figure_out_github_token(github_token: str) -> str:
27 | # This script is designed to run in Kokoro. There's several sources where
28 | # the GitHub token could be, and we want to make adding this script easy.
29 | # We'll try the common ones before giving up.
30 |
31 | # A valid github token was passed on the command line, make sure it's
32 | # not a file and just return it.
33 | if github_token is not None:
34 | if os.path.exists(github_token):
35 | with open(github_token, "r", encoding="utf-8") as fh:
36 | return fh.read().strip()
37 | else:
38 | return github_token
39 |
40 | # First, try KeyStore
41 | paths = []
42 | if "KOKORO_KEYSTORE_DIR" in os.environ:
43 | paths.append(
44 | os.path.join(
45 | os.environ["KOKORO_KEYSTORE_DIR"], "73713_releasetool-magictoken"
46 | )
47 | )
48 |
49 | # Second, try gfile resources.
50 | if "KOKORO_GFILE_DIR" in os.environ:
51 | paths.append(
52 | os.path.join(
53 | os.environ["KOKORO_GFILE_DIR"], "yoshi-releasetool-magictoken.txt"
54 | )
55 | )
56 |
57 | for path in paths:
58 | if os.path.exists(path):
59 | with open(path, "r", encoding="utf-8") as fh:
60 | return fh.read().strip()
61 |
62 | return None
63 |
64 |
65 | def extract_pr_details(pr) -> Tuple[str, str, str]:
66 | match = re.match(
67 | r"https://github\.com/(?P.+?)/(?P.+?)/pull/(?P\d+?)$", pr
68 | )
69 |
70 | if not match:
71 | raise ValueError("Not a PR URL.")
72 |
73 | return match.group("owner"), match.group("repo"), match.group("number")
74 |
75 |
76 | def start(github_token_raw: Union[str, dict], pr: str) -> None:
77 | """Reports the start of a publication job to GitHub."""
78 | # If we are passed a dictionary for github_token, assume we are
79 | # retrieveing a JWT, and do not use magic proxy:
80 | use_proxy = True
81 | if type(github_token_raw) is dict:
82 | use_proxy = False
83 | github_token = releasetool.github.GitHubToken.token_from_dict(
84 | cast(dict, github_token_raw)
85 | )
86 | else:
87 | github_token_raw = figure_out_github_token(cast(str, github_token_raw))
88 | github_token = releasetool.github.GitHubToken(github_token_raw, "Bearer")
89 |
90 | if not github_token or not pr:
91 | print("No github token or PR specified to report status to, returning.")
92 | return
93 |
94 | gh = releasetool.github.GitHub(github_token, use_proxy=use_proxy)
95 |
96 | try:
97 | owner, repo, number = extract_pr_details(pr)
98 | except ValueError:
99 | print("Invalid PR number, returning.")
100 | return
101 |
102 | build_url = os.environ.get("CLOUD_LOGGING_URL")
103 |
104 | if not build_url:
105 | kokoro_build_id = os.environ.get("KOKORO_BUILD_ID")
106 |
107 | if kokoro_build_id:
108 | build_url = f"http://sponge/{kokoro_build_id}"
109 |
110 | if build_url:
111 | message = f"The release build has started, the log can be viewed [here]({build_url}). :sunflower:"
112 | else:
113 | message = "The release build has started, but the build log URL could not be determined. :broken_heart:"
114 |
115 | try:
116 | gh.create_pull_request_comment(f"{owner}/{repo}", number, message)
117 | except HTTPError as e:
118 | # wrap exception so we don't show the proxy url
119 | raise Exception(f"Error commenting on PR: {e.response.status_code}")
120 |
121 |
122 | def finish(
123 | github_token_raw: Union[str, dict], pr: str, status: bool, details: str
124 | ) -> None:
125 | """Reports the completion of a publication job to GitHub."""
126 | # If we are passed a dictionary for github_token, assume we are
127 | # retrieveing a JWT, and do not use magic proxy:
128 | use_proxy = True
129 | if type(github_token_raw) is dict:
130 | use_proxy = False
131 | github_token = releasetool.github.GitHubToken.token_from_dict(
132 | cast(dict, github_token_raw)
133 | )
134 | else:
135 | github_token_raw = figure_out_github_token(cast(str, github_token_raw))
136 | github_token = releasetool.github.GitHubToken(github_token_raw, "Bearer")
137 |
138 | if not github_token or not pr:
139 | print("No github token or PR specified to report status to, returning.")
140 | return
141 |
142 | gh = releasetool.github.GitHub(github_token, use_proxy=use_proxy)
143 |
144 | try:
145 | owner, repo, number = extract_pr_details(pr)
146 | except ValueError:
147 | print("Invalid PR number, returning.")
148 | return
149 |
150 | if status:
151 | message = ":egg: You hatched a release! The release build finished successfully! :purple_heart:"
152 | labels = ["autorelease: published"]
153 | else:
154 | message = "The release build failed! Please investigate!"
155 | labels = ["autorelease: failed"]
156 |
157 | if details:
158 | message += f"\n{details}"
159 |
160 | try:
161 | gh.create_pull_request_comment(f"{owner}/{repo}", number, message)
162 | except HTTPError as e:
163 | # wrap exception so we don't show the proxy url
164 | raise Exception(f"Error commenting on PR: {e.response.status_code}")
165 |
166 | try:
167 | pull = gh.get_pull_request(f"{owner}/{repo}", number)
168 | gh.update_pull_labels(pull, add=labels, remove=["autorelease: tagged"])
169 | except HTTPError as e:
170 | # wrap exception so we don't show the proxy url
171 | raise Exception(f"Error updating lables on PR: {e.response.status_code}")
172 |
173 |
174 | def script():
175 | resource = importlib.resources.read_binary(
176 | "releasetool.commands", "publish_reporter.sh"
177 | )
178 | print(resource.decode("utf-8"), flush=True)
179 |
--------------------------------------------------------------------------------
/releasetool/commands/publish_reporter.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2018 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | if [ -f "${KOKORO_GFILE_DIR}/secret_manager/releasetool-publish-reporter-app" ]; then
18 | export APP_ID_PATH="${KOKORO_GFILE_DIR}/secret_manager/releasetool-publish-reporter-app"
19 | if [ -f "${KOKORO_GFILE_DIR}/secret_manager/releasetool-publish-reporter-googleapis-installation" ]; then
20 | export INSTALLATION_ID_PATH="${KOKORO_GFILE_DIR}/secret_manager/releasetool-publish-reporter-googleapis-installation"
21 | elif [ -f "${KOKORO_GFILE_DIR}/secret_manager/releasetool-publish-reporter-googlecloudplatform-installation" ]; then
22 | export INSTALLATION_ID_PATH="${KOKORO_GFILE_DIR}/secret_manager/releasetool-publish-reporter-googlecloudplatform-installation"
23 | else
24 | echo 'could not load GitHub installation id'
25 | fi
26 | export GITHUB_PRIVATE_KEY_PATH="${KOKORO_GFILE_DIR}/secret_manager/releasetool-publish-reporter-pem"
27 | else
28 | echo 'could not load GitHub installation credentials'
29 | fi
30 |
31 | # Install an exit hook to report status.
32 | releasetool_finish_report() {
33 | rv=$?
34 | if [[ $rv == 0 ]]; then
35 | python3 -m releasetool publish-reporter-finish --status yes || true
36 | else
37 | python3 -m releasetool publish-reporter-finish --status no || true
38 | fi
39 | echo "Release status reported."
40 | exit $rv
41 | }
42 |
43 | trap releasetool_finish_report EXIT
44 |
45 | # Report the start of a build
46 | python3 -m releasetool publish-reporter-start || true
47 |
--------------------------------------------------------------------------------
/releasetool/commands/start/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleapis/releasetool/4a6e25c4a0cb6b6db1815f578012edb8cb884595/releasetool/commands/start/__init__.py
--------------------------------------------------------------------------------
/releasetool/commands/start/go.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import datetime
16 | import getpass
17 | import json
18 | import os
19 | import subprocess
20 | import textwrap
21 | from typing import Dict, List, Optional, Sequence
22 |
23 | import attr
24 | import click
25 | from dateutil import tz
26 |
27 | import releasetool.filehelpers
28 | import releasetool.git
29 |
30 |
31 | _CHANGELOG_FILENAME = "CHANGES.md"
32 |
33 | _CHANGELOG_TEMPLATE = """\
34 | # Changes
35 |
36 | """
37 |
38 |
39 | @attr.s(auto_attribs=True, slots=True)
40 | class Context:
41 | module_name: str = None
42 | relative_module_name: str = None # relative to repo root; used for tags
43 | changes: Sequence[str] = ()
44 | release_notes: Optional[str] = None
45 | last_release_version: Optional[str] = None
46 | last_release_committish: Optional[str] = None
47 | release_version: Optional[str] = None
48 | release_branch: Optional[str] = None
49 |
50 |
51 | def determine_module_name(ctx: Context) -> None:
52 | # Get the module name from the go.mod file in the current directory.
53 | click.secho("> Figuring out the module name.", fg="cyan")
54 | info = read_gomod()
55 | if info is None:
56 | click.secho("Looks like we're releasing the repo (no modules).")
57 | else:
58 | ctx.module_name = info["Module"]["Path"]
59 | ctx.relative_module_name = relative_module_name(ctx.module_name)
60 | click.secho(
61 | f"Looks like we're releasing {ctx.module_name} (relative path {ctx.relative_module_name})."
62 | )
63 |
64 |
65 | def read_gomod() -> Optional[dict]:
66 | if os.path.isdir(".git"):
67 | return None
68 | elif os.path.isfile("go.mod"):
69 | output = subprocess.check_output(["go", "mod", "edit", "-json"]).decode("utf-8")
70 | return json.loads(output)
71 | else:
72 | raise ValueError("no go.mod; must release from repo root")
73 |
74 |
75 | def relative_module_name(modname) -> str:
76 | """Returns modname relative to the repo root.
77 |
78 | Assumes modname's go.mod file is in the current directory.
79 | """
80 | dir = os.getcwd()
81 | components: List[str] = []
82 | while dir != "/":
83 | if os.path.isdir(os.path.join(dir, ".git")):
84 | return "/".join(reversed(components))
85 | dir, c = os.path.split(dir)
86 | components.append(c)
87 | raise ValueError("not inside a git repo")
88 |
89 |
90 | def determine_last_release(ctx: Context) -> None:
91 | click.secho("> Figuring out what the last release was.", fg="cyan")
92 | if ctx.relative_module_name is None:
93 | prefix = "v"
94 | else:
95 | prefix = ctx.relative_module_name + "/v"
96 | tags = releasetool.git.list_tags()
97 | candidates = [tag for tag in tags if tag.startswith(prefix)]
98 |
99 | if candidates:
100 | ctx.last_release_committish = candidates[0]
101 | ctx.last_release_version = candidates[0].rsplit("/").pop()[1:]
102 |
103 | else:
104 | click.secho(
105 | f"I couldn't figure out the last release for {ctx.module_name}, "
106 | "so I'm assuming this is the first release. Can you tell me "
107 | "which git rev/sha to start the changelog at?",
108 | fg="yellow",
109 | )
110 | ctx.last_release_committish = click.prompt("Committish")
111 | ctx.last_release_version = "0.0.0"
112 |
113 | click.secho(f"The last release was {ctx.last_release_version}.")
114 |
115 |
116 | def gather_changes(ctx: Context) -> None:
117 | click.secho(f"> Gathering changes since {ctx.last_release_version}", fg="cyan")
118 | ctx.changes = releasetool.git.summary_log(
119 | from_=ctx.last_release_committish, to="origin/master"
120 | )
121 | click.secho(f"Cool, {len(ctx.changes)} changes found.")
122 |
123 |
124 | def determine_release_version(ctx: Context) -> None:
125 | click.secho("> Now it's time to pick a release version!", fg="cyan")
126 | release_notes = textwrap.indent(ctx.release_notes, "\t")
127 | click.secho(f"Here's the release notes you wrote:\n\n{release_notes}\n")
128 |
129 | parsed_version = [int(x) for x in ctx.last_release_version.split(".")]
130 |
131 | if parsed_version == [0, 0, 0]:
132 | ctx.release_version = "0.1.0"
133 | if not click.confirm(f"Release {ctx.release_version}?", default=True):
134 | version = click.prompt("What version should we release?")
135 | ctx.release_version = version
136 | return
137 |
138 | selection = click.prompt(
139 | "Is this a major, minor, or patch update (or enter the new version " "directly)"
140 | )
141 | if selection == "major":
142 | parsed_version[0] += 1
143 | parsed_version[1] = 0
144 | parsed_version[2] = 0
145 | elif selection == "minor":
146 | parsed_version[1] += 1
147 | parsed_version[2] = 0
148 | elif selection == "patch":
149 | parsed_version[2] += 1
150 | else:
151 | ctx.release_version = selection
152 | return
153 |
154 | ctx.release_version = "{}.{}.{}".format(*parsed_version)
155 |
156 | click.secho(f"Got it, releasing {ctx.release_version}.")
157 |
158 |
159 | def create_release_branch(ctx) -> None:
160 | if ctx.module_name is None:
161 | ctx.release_branch = f"release-{ctx.release_version}"
162 | else:
163 | ctx.release_branch = f"release-{ctx.module_name}-{ctx.release_version}"
164 | click.secho(f"> Creating branch {ctx.release_branch}", fg="cyan")
165 | return releasetool.git.checkout_create_branch(ctx.release_branch)
166 |
167 |
168 | def update_changelog(ctx: Context) -> None:
169 | click.secho(f"> Updating {_CHANGELOG_FILENAME}.", fg="cyan")
170 |
171 | if not os.path.exists(_CHANGELOG_FILENAME):
172 | print(f"{_CHANGELOG_FILENAME} does not yet exist. Opening it for creation.")
173 |
174 | releasetool.filehelpers.open_editor_with_content(
175 | _CHANGELOG_FILENAME, _CHANGELOG_TEMPLATE
176 | )
177 |
178 | changelog_entry = (
179 | f"## v{ctx.release_version}" f"\n\n" f"{ctx.release_notes}" f"\n\n"
180 | )
181 | releasetool.filehelpers.insert_before(
182 | _CHANGELOG_FILENAME, changelog_entry, r"^## (.+)$|\Z"
183 | )
184 |
185 |
186 | def create_release_commit(ctx: Context) -> None:
187 | """Create a release commit."""
188 | click.secho("> Comitting changes", fg="cyan")
189 | releasetool.git.commit([_CHANGELOG_FILENAME], f"all: release {ctx.release_version}")
190 |
191 |
192 | def create_release_cl(ctx: Context) -> None:
193 | click.secho("> Creating release CL.", fg="cyan")
194 | revs = click.prompt("reviewers (comma-separated)", default="deklerk,jba")
195 | subprocess.check_output(["git", "codereview", "mail", "-r", revs, "HEAD"])
196 |
197 |
198 | def edit_release_notes(ctx: Context) -> None:
199 | click.secho("> Opening your editor to finalize release notes.", fg="cyan")
200 | release_notes = (
201 | datetime.datetime.now(datetime.timezone.utc)
202 | .astimezone(tz.gettz("US/Pacific"))
203 | .strftime("%m-%d-%Y %H:%M %Z\n\n")
204 | )
205 |
206 | packages: Dict[str, List[str]] = {}
207 | for change in ctx.changes:
208 | try:
209 | package, commit = change.split(":", 1)
210 | except ValueError:
211 | package = "all"
212 | commit = change
213 | commit = commit.strip()
214 | try:
215 | packages[package].append(commit)
216 | except KeyError:
217 | packages[package] = [commit]
218 |
219 | # sort packages alphabetically
220 | sorted_packages = sorted(list(packages.items()), key=lambda x: x[0])
221 | for package, commits in sorted_packages:
222 | commit_list = "\n".join(f" - {commit}" for commit in commits)
223 | release_notes += f"- {package}:\n{commit_list}\n"
224 |
225 | ctx.release_notes = releasetool.filehelpers.open_editor_with_tempfile(
226 | release_notes, "release-notes.md"
227 | ).strip()
228 |
229 |
230 | def start() -> None:
231 | ctx = Context()
232 |
233 | click.secho(f"o/ Hey, {getpass.getuser()}, let's release some stuff!", fg="magenta")
234 |
235 | determine_module_name(ctx)
236 | determine_last_release(ctx)
237 | gather_changes(ctx)
238 | edit_release_notes(ctx)
239 | determine_release_version(ctx)
240 | create_release_branch(ctx)
241 | update_changelog(ctx)
242 | create_release_commit(ctx)
243 | create_release_cl(ctx)
244 |
245 | click.secho("\\o/ All done!", fg="magenta")
246 |
--------------------------------------------------------------------------------
/releasetool/commands/start/nodejs.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import os
17 | import textwrap
18 | from typing import Optional
19 |
20 | import attr
21 | import click
22 |
23 | import releasetool.filehelpers
24 | import releasetool.git
25 | import releasetool.github
26 | import releasetool.secrets
27 | import releasetool.commands.common
28 |
29 |
30 | _CHANGELOG_TEMPLATE = """\
31 | # Changelog
32 |
33 | [npm history][1]
34 |
35 | [1]: https://www.npmjs.com/package/{package_name}?activeTab=versions
36 |
37 | """
38 |
39 |
40 | @attr.s(auto_attribs=True, slots=True)
41 | class Context(releasetool.commands.common.GitHubContext):
42 | last_release_version: Optional[str] = None
43 | last_release_committish: Optional[str] = None
44 | release_version: Optional[str] = None
45 | release_branch: Optional[str] = None
46 | pull_request: Optional[dict] = None
47 |
48 |
49 | def determine_package_name(ctx: Context) -> None:
50 | click.secho("> Figuring out the package name.", fg="cyan")
51 | ctx.package_name = releasetool.filehelpers.extract(
52 | "package.json", r'"name": "(.*?)"'
53 | )
54 | click.secho(f"Looks like we're releasing {ctx.package_name}.")
55 |
56 |
57 | def determine_last_release(ctx: Context) -> None:
58 | click.secho("> Figuring out what the last release was.", fg="cyan")
59 | tags = releasetool.git.list_tags()
60 | candidates = [tag for tag in tags if tag.startswith("v")]
61 |
62 | if candidates:
63 | ctx.last_release_committish = candidates[0]
64 | # strip the leading 'v'
65 | ctx.last_release_version = candidates[0].lstrip("v")
66 |
67 | else:
68 | click.secho(
69 | f"I couldn't figure out the last release for {ctx.package_name}, "
70 | "so I'm assuming this is the first release. Can you tell me "
71 | "which git rev/sha to start the changelog at?",
72 | fg="yellow",
73 | )
74 | ctx.last_release_committish = click.prompt("Committish")
75 | ctx.last_release_version = "0.0.0"
76 |
77 | click.secho(f"The last release was {ctx.last_release_version}.")
78 |
79 |
80 | def gather_changes(ctx: Context) -> None:
81 | click.secho(f"> Gathering changes since {ctx.last_release_version}", fg="cyan")
82 | ctx.changes = releasetool.git.summary_log(
83 | from_=ctx.last_release_committish, to=f"{ctx.upstream_name}/master"
84 | )
85 | ctx.changes = [
86 | ctx.github.link_pull_request(c, ctx.upstream_repo) for c in ctx.changes
87 | ]
88 | click.secho(f"Cool, {len(ctx.changes)} changes found.")
89 |
90 |
91 | def determine_release_version(ctx: Context) -> None:
92 | click.secho("> Now it's time to pick a release version!", fg="cyan")
93 | release_notes = textwrap.indent(ctx.release_notes, "\t")
94 | click.secho(f"Here's the release notes you wrote:\n\n{release_notes}\n")
95 |
96 | parsed_version = [int(x) for x in ctx.last_release_version.split(".")]
97 |
98 | if parsed_version == [0, 0, 0]:
99 | ctx.release_version = "0.1.0"
100 | if not click.confirm(f"Release {ctx.release_version}?", default=True):
101 | version = click.prompt("What version should we release?")
102 | ctx.release_version = version
103 | return
104 |
105 | selection = click.prompt(
106 | "Is this a major, minor, or patch update (or enter the new version " "directly)"
107 | )
108 | if selection == "major":
109 | parsed_version[0] += 1
110 | parsed_version[1] = 0
111 | parsed_version[2] = 0
112 | elif selection == "minor":
113 | parsed_version[1] += 1
114 | parsed_version[2] = 0
115 | elif selection == "patch":
116 | parsed_version[2] += 1
117 | else:
118 | ctx.release_version = selection
119 | return
120 |
121 | ctx.release_version = "{}.{}.{}".format(*parsed_version)
122 |
123 | click.secho(f"Got it, releasing {ctx.release_version}.")
124 |
125 |
126 | def create_release_branch(ctx) -> None:
127 | ctx.release_branch = f"release-v{ctx.release_version}"
128 | click.secho(f"> Creating branch {ctx.release_branch}", fg="cyan")
129 | return releasetool.git.checkout_create_branch(ctx.release_branch)
130 |
131 |
132 | def update_changelog(ctx: Context) -> None:
133 | changelog_filename = "CHANGELOG.md"
134 | click.secho(f"> Updating {changelog_filename}.", fg="cyan")
135 |
136 | if not os.path.exists(changelog_filename):
137 | print(f"{changelog_filename} does not yet exist. Opening it for " "creation.")
138 |
139 | releasetool.filehelpers.open_editor_with_content(
140 | changelog_filename,
141 | _CHANGELOG_TEMPLATE.format(package_name=ctx.package_name),
142 | )
143 |
144 | changelog_entry = (
145 | f"## v{ctx.release_version}" f"\n\n" f"{ctx.release_notes}" f"\n\n"
146 | )
147 | releasetool.filehelpers.insert_before(
148 | changelog_filename, changelog_entry, r"^## (.+)$|\Z"
149 | )
150 |
151 |
152 | def update_package_json(ctx: Context) -> None:
153 | click.secho("> Updating package.json.", fg="cyan")
154 | releasetool.filehelpers.replace(
155 | "package.json", r'"version": "(.+?)"', f'"version": "{ctx.release_version}"'
156 | )
157 |
158 |
159 | def update_samples_package_json(ctx: Context) -> None:
160 | click.secho("> Updating samples/package.json.", fg="cyan")
161 | try:
162 | releasetool.filehelpers.replace(
163 | "samples/package.json",
164 | f'"{ctx.package_name}": "(.+?)"',
165 | f'"{ctx.package_name}": "^{ctx.release_version}"',
166 | )
167 | except FileNotFoundError:
168 | pass
169 |
170 |
171 | def create_release_commit(ctx: Context) -> None:
172 | """Create a release commit."""
173 | click.secho("> Committing changes", fg="cyan")
174 | files = ["CHANGELOG.md", "package.json"]
175 |
176 | # Not all node.js repos have a samples directory
177 | if os.path.exists("samples/package.json"):
178 | files.append("samples/package.json")
179 |
180 | releasetool.git.commit(files, f"Release v{ctx.release_version}")
181 |
182 |
183 | def push_release_branch(ctx: Context) -> None:
184 | click.secho("> Pushing release branch.", fg="cyan")
185 | releasetool.git.push(ctx.release_branch)
186 |
187 |
188 | def create_release_pr(ctx: Context, autorelease: bool = True) -> None:
189 | click.secho("> Creating release pull request.", fg="cyan")
190 |
191 | if ctx.upstream_repo == ctx.origin_repo:
192 | head = ctx.release_branch
193 | else:
194 | head = f"{ctx.origin_user}:{ctx.release_branch}"
195 |
196 | ctx.pull_request = ctx.github.create_pull_request(
197 | ctx.upstream_repo,
198 | head=head,
199 | title=f"Release {ctx.package_name} v{ctx.release_version}",
200 | body="This pull request was generated using releasetool.",
201 | )
202 |
203 | if autorelease:
204 | ctx.github.add_issue_labels(
205 | ctx.upstream_repo, ctx.pull_request["number"], ["autorelease: pending"]
206 | )
207 |
208 | click.secho(f"Pull request is at {ctx.pull_request['html_url']}.")
209 |
210 |
211 | def start() -> None:
212 | ctx = Context()
213 |
214 | click.secho(f"o/ Hey, {getpass.getuser()}, let's release some stuff!", fg="magenta")
215 |
216 | releasetool.commands.common.setup_github_context(ctx)
217 | determine_package_name(ctx)
218 | determine_last_release(ctx)
219 | gather_changes(ctx)
220 | releasetool.commands.common.edit_release_notes(ctx)
221 | determine_release_version(ctx)
222 | create_release_branch(ctx)
223 | update_changelog(ctx)
224 | update_package_json(ctx)
225 | update_samples_package_json(ctx)
226 | create_release_commit(ctx)
227 | push_release_branch(ctx)
228 | # TODO: Confirm?
229 | create_release_pr(ctx)
230 |
231 | click.secho("\\o/ All done!", fg="magenta")
232 |
--------------------------------------------------------------------------------
/releasetool/commands/start/python.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import os
17 | import re
18 | import subprocess
19 | import sys
20 | import textwrap
21 | from typing import Optional, Sequence, Tuple
22 |
23 | import attr
24 | import click
25 |
26 | import releasetool.filehelpers
27 | import releasetool.git
28 | import releasetool.github
29 | import releasetool.secrets
30 | import releasetool.commands.common
31 |
32 |
33 | _CHANGELOG_TEMPLATE = """\
34 | # Changelog
35 |
36 | [PyPI History][1]
37 |
38 | [1]: https://pypi.org/project/DISTRIBUTION NAME/#history
39 |
40 | """
41 |
42 |
43 | @attr.s(auto_attribs=True, slots=True)
44 | class Context(releasetool.commands.common.GitHubContext):
45 | last_release_version: Optional[str] = None
46 | last_release_committish: Optional[str] = None
47 | release_version: Optional[str] = None
48 | release_branch: Optional[str] = None
49 | pull_request: Optional[dict] = None
50 | monorepo: bool = False # true only when releasing from google-cloud-python
51 |
52 |
53 | def determine_package_name(ctx: Context) -> None:
54 | click.secho("> Figuring out the package name.", fg="cyan")
55 | if ctx.monorepo:
56 | ctx.package_name = os.path.basename(os.getcwd())
57 | else:
58 | ctx.package_name = subprocess.check_output(
59 | [sys.executable, "setup.py", "--name"]
60 | ).decode("utf-8")
61 |
62 | click.secho(f"Looks like we're releasing {ctx.package_name}.")
63 |
64 |
65 | def find_last_release_tag(
66 | tags: Sequence[str], package_name: str, monorepo: bool
67 | ) -> Optional[Tuple[str, str]]:
68 | commitish = None
69 | if monorepo:
70 | # tags look like storage-1.2.3
71 | package_names = [package_name, package_name.replace("_", "-")]
72 | candidates = [tag for tag in tags if tag.rsplit("-")[0] in package_names]
73 |
74 | if candidates:
75 | commitish = candidates[0]
76 | version = commitish.rsplit("-").pop()
77 | else:
78 | # tags look like v1.2.3 or 1.2.3
79 | candidates = [tag for tag in tags if re.match(r"v?(\d+\.\d+\.\d+)", tag)]
80 | if candidates:
81 | commitish = candidates[0]
82 | version = commitish.split("v").pop()
83 |
84 | if commitish:
85 | return commitish, version
86 | return None
87 |
88 |
89 | def determine_last_release(ctx: Context) -> None:
90 | click.secho("> Figuring out what the last release was.", fg="cyan")
91 | tags = releasetool.git.list_tags()
92 |
93 | candidate = find_last_release_tag(tags, ctx.package_name, ctx.monorepo)
94 | if candidate is not None:
95 | ctx.last_release_committish = candidate[0]
96 | ctx.last_release_version = candidate[1]
97 |
98 | else:
99 | click.secho(
100 | f"I couldn't figure out the last release for {ctx.package_name}, "
101 | "so I'm assuming this is the first release. Can you tell me "
102 | "which git rev/sha to start the changelog at?",
103 | fg="yellow",
104 | )
105 | ctx.last_release_committish = click.prompt("Committish")
106 | ctx.last_release_version = "0.0.0"
107 |
108 | click.secho(f"The last release was {ctx.last_release_version}.")
109 |
110 |
111 | def gather_changes(ctx: Context) -> None:
112 | click.secho(f"> Gathering changes since {ctx.last_release_version}", fg="cyan")
113 | ctx.changes = releasetool.git.summary_log(
114 | from_=ctx.last_release_committish, to="master"
115 | )
116 | ctx.changes = [
117 | ctx.github.link_pull_request(c, ctx.upstream_repo) for c in ctx.changes
118 | ]
119 | click.secho(f"Cool, {len(ctx.changes)} changes found.")
120 |
121 |
122 | def determine_release_version(ctx: Context) -> None:
123 | click.secho("> Now it's time to pick a release version!", fg="cyan")
124 | release_notes = textwrap.indent(ctx.release_notes, "\t")
125 | click.secho(f"Here's the release notes you wrote:\n\n{release_notes}\n")
126 |
127 | parsed_version = [int(x) for x in ctx.last_release_version.split(".")]
128 |
129 | if parsed_version == [0, 0, 0]:
130 | ctx.release_version = "0.1.0"
131 | if not click.confirm(f"Release {ctx.release_version}?", default=True):
132 | version = click.prompt("What version should we release?")
133 | ctx.release_version = version
134 | return
135 |
136 | selection = click.prompt(
137 | "Is this a major, minor, or patch update (or enter the new version " "directly)"
138 | )
139 | if selection == "major":
140 | parsed_version[0] += 1
141 | parsed_version[1] = 0
142 | parsed_version[2] = 0
143 | elif selection == "minor":
144 | parsed_version[1] += 1
145 | parsed_version[2] = 0
146 | elif selection == "patch":
147 | parsed_version[2] += 1
148 | else:
149 | ctx.release_version = selection
150 | return
151 |
152 | ctx.release_version = "{}.{}.{}".format(*parsed_version)
153 |
154 | click.secho(f"Got it, releasing {ctx.release_version}.")
155 |
156 |
157 | def create_release_branch(ctx) -> None:
158 | if ctx.monorepo:
159 | ctx.release_branch = f"release-{ctx.package_name}-{ctx.release_version}"
160 | else:
161 | ctx.release_branch = f"release-v{ctx.release_version}"
162 | click.secho(f"> Creating branch {ctx.release_branch}", fg="cyan")
163 | return releasetool.git.checkout_create_branch(ctx.release_branch)
164 |
165 |
166 | def update_changelog(ctx: Context) -> None:
167 | changelog_filename = "CHANGELOG.md"
168 | click.secho(f"> Updating {changelog_filename}.", fg="cyan")
169 |
170 | if not os.path.exists(changelog_filename):
171 | print(f"{changelog_filename} does not yet exist. Opening it for " "creation.")
172 |
173 | releasetool.filehelpers.open_editor_with_content(
174 | changelog_filename, _CHANGELOG_TEMPLATE
175 | )
176 |
177 | changelog_entry = f"## {ctx.release_version}" f"\n\n" f"{ctx.release_notes}" f"\n\n"
178 | releasetool.filehelpers.insert_before(
179 | changelog_filename, changelog_entry, r"^## (.+)$|\Z"
180 | )
181 |
182 |
183 | def update_setup_py(ctx: Context) -> None:
184 | click.secho("> Updating setup.py.", fg="cyan")
185 | releasetool.filehelpers.replace(
186 | "setup.py",
187 | r"version\s*=\s*(['\"])(.+?)['\"]",
188 | f"version = \\g<1>{ctx.release_version}\\g<1>",
189 | )
190 |
191 |
192 | def create_release_commit(ctx: Context) -> None:
193 | """Create a release commit."""
194 | click.secho("> Comitting changes", fg="cyan")
195 | if ctx.monorepo:
196 | commit_msg = f"chore({ctx.package_name}): release {ctx.release_version}"
197 | else:
198 | commit_msg = f"chore: release v{ctx.release_version}"
199 | releasetool.git.commit(["CHANGELOG.md", "setup.py"], commit_msg)
200 |
201 |
202 | def push_release_branch(ctx: Context) -> None:
203 | click.secho("> Pushing release branch.", fg="cyan")
204 | releasetool.git.push(ctx.release_branch)
205 |
206 |
207 | def create_release_pr(ctx: Context, autorelease: bool = True) -> None:
208 | click.secho("> Creating release pull request.", fg="cyan")
209 |
210 | if ctx.upstream_repo == ctx.origin_repo:
211 | head = ctx.release_branch
212 | else:
213 | head = f"{ctx.origin_user}:{ctx.release_branch}"
214 |
215 | if ctx.monorepo:
216 | pr_title = f"chore({ctx.package_name}): release {ctx.release_version}"
217 | else:
218 | pr_title = f"chore: Release v{ctx.release_version}"
219 |
220 | ctx.pull_request = ctx.github.create_pull_request(
221 | ctx.upstream_repo,
222 | head=head,
223 | title=pr_title,
224 | body="This pull request was generated using releasetool.",
225 | )
226 |
227 | if autorelease:
228 | ctx.github.add_issue_labels(
229 | ctx.upstream_repo, ctx.pull_request["number"], ["autorelease: pending"]
230 | )
231 |
232 | click.secho(f"Pull request is at {ctx.pull_request['html_url']}.")
233 |
234 |
235 | def start() -> None:
236 | ctx = Context()
237 |
238 | click.secho(f"o/ Hey, {getpass.getuser()}, let's release some stuff!", fg="magenta")
239 |
240 | releasetool.commands.common.setup_github_context(ctx)
241 |
242 | if "google-cloud-python" in ctx.origin_repo:
243 | ctx.monorepo = True
244 |
245 | determine_package_name(ctx)
246 | determine_last_release(ctx)
247 | gather_changes(ctx)
248 | releasetool.commands.common.edit_release_notes(ctx)
249 | determine_release_version(ctx)
250 | create_release_branch(ctx)
251 | update_changelog(ctx)
252 | update_setup_py(ctx)
253 | create_release_commit(ctx)
254 | push_release_branch(ctx)
255 | create_release_pr(ctx)
256 |
257 | click.secho("\\o/ All done!", fg="magenta")
258 |
--------------------------------------------------------------------------------
/releasetool/commands/start/python_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import datetime
17 |
18 | import click
19 | from dateutil import tz
20 |
21 | import releasetool.commands.start.python
22 | from releasetool.commands.start.python import Context
23 |
24 |
25 | def determine_release_version(ctx: Context) -> None:
26 | ctx.release_version = (
27 | datetime.datetime.now(datetime.timezone.utc)
28 | .astimezone(tz.gettz("US/Pacific"))
29 | .strftime("%Y.%m.%d")
30 | )
31 |
32 | if ctx.release_version in ctx.last_release_version:
33 | click.secho(
34 | f"The release version {ctx.release_version} is already used.", fg="red"
35 | )
36 | ctx.release_version = click.prompt("Please input another version: ")
37 |
38 | click.secho(f"Releasing {ctx.release_version}.")
39 |
40 |
41 | def start() -> None:
42 | # Python tools use calver, otherwise the process is the same as python
43 | # libraries.
44 | ctx = Context()
45 |
46 | click.secho(f"o/ Hey, {getpass.getuser()}, let's release some stuff!", fg="magenta")
47 |
48 | releasetool.commands.common.setup_github_context(ctx)
49 | releasetool.commands.start.python.determine_package_name(ctx)
50 | releasetool.commands.start.python.determine_last_release(ctx)
51 | releasetool.commands.start.python.gather_changes(ctx)
52 | releasetool.commands.common.edit_release_notes(ctx)
53 | determine_release_version(ctx)
54 | releasetool.commands.start.python.create_release_branch(ctx)
55 | releasetool.commands.start.python.update_changelog(ctx)
56 | releasetool.commands.start.python.update_setup_py(ctx)
57 | releasetool.commands.start.python.create_release_commit(ctx)
58 | releasetool.commands.start.python.push_release_branch(ctx)
59 | # TODO: Confirm?
60 | releasetool.commands.start.python.create_release_pr(ctx)
61 |
62 | click.secho("\\o/ All done!", fg="magenta")
63 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleapis/releasetool/4a6e25c4a0cb6b6db1815f578012edb8cb884595/releasetool/commands/tag/__init__.py
--------------------------------------------------------------------------------
/releasetool/commands/tag/cpp.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from typing import Union
16 |
17 |
18 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
19 | """Return the Kokoro job name.
20 |
21 | Args:
22 | upstream_repo (str): The GitHub repo in the form of `/`
23 | package_name (str): The name of package to release
24 |
25 | Returns:
26 | The name of the Kokoro job to trigger or None if there is no job to trigger
27 | """
28 | return f"cloud-devrel/client-libraries/cpp/{upstream_repo.rsplit('/', 1)[-1]}/release/publish"
29 |
30 |
31 | def package_name(pull: dict) -> Union[str, None]:
32 | return None
33 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/dotnet.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import re
17 | from typing import Union
18 |
19 | import click
20 |
21 | import releasetool.git
22 | import releasetool.github
23 | import releasetool.secrets
24 | import releasetool.commands.common
25 | from releasetool.commands.common import TagContext
26 |
27 | # We need to match the PR titles we use in google-cloud-dotnet
28 | # Release Google.Maps.Places.V1 version 1.0.0-beta12
29 | # We need to match the PR titles set by Release Please
30 | # chore(main): release Google.CloudEvents.Protobuf 1.7.0-alpha01
31 | RELEASE_LINE_PATTERN = (
32 | r"^((?:- )?R|chore\(main\): r)elease ([^ ]*)( version)? (\d+\.\d+.\d+(-[^ ]*)?)$"
33 | )
34 |
35 |
36 | def determine_release_pr(ctx: TagContext) -> None:
37 | click.secho(
38 | "> Let's figure out which pull request corresponds to your release.", fg="cyan"
39 | )
40 |
41 | pulls = ctx.github.list_pull_requests(ctx.upstream_repo, state="closed")
42 | pulls = [pull for pull in pulls if "release" in pull["title"].lower()][:30]
43 |
44 | click.secho("> Please pick one of the following PRs:\n")
45 | for n, pull in enumerate(pulls, 1):
46 | print(f"\t{n}: {pull['title']} ({pull['number']})")
47 |
48 | pull_idx = click.prompt(
49 | "\nWhich one do you want to tag and release?", type=click.INT
50 | )
51 | ctx.release_pr = pulls[pull_idx - 1]
52 |
53 |
54 | def create_releases(ctx: TagContext) -> None:
55 | click.secho("> Creating the release.")
56 |
57 | commitish = ctx.release_pr["merge_commit_sha"]
58 | title = ctx.release_pr["title"]
59 | body_lines = (ctx.release_pr["body"] or "").splitlines()
60 | all_lines = [title] + body_lines
61 | pr_comment = ""
62 | for line in all_lines:
63 | match = re.search(RELEASE_LINE_PATTERN, line)
64 | if match is not None:
65 | package = match.group(2)
66 | version = match.group(4)
67 | tag = package + "-" + version
68 | ctx.github.create_release(
69 | repository=ctx.upstream_repo,
70 | tag_name=tag,
71 | target_commitish=commitish,
72 | name=f"{package} version {version}",
73 | # TODO: either reformat the message as we do in TagReleases,
74 | # or make sure we create the PR with an "already-formatted"
75 | # body. (The latter is probably simpler, and will make the
76 | # PR easier to read anyway.)
77 | body=ctx.release_pr["body"],
78 | # Versions like "1.0.0-beta01" or "0.9.0" are prerelease
79 | prerelease="-" in version or version.startswith("0."),
80 | )
81 | click.secho(f"Created release for {tag}")
82 | pr_comment = pr_comment + f"- Created release for {tag}\n"
83 |
84 | if pr_comment == "":
85 | raise ValueError("No releases found within pull request")
86 |
87 | ctx.github.create_pull_request_comment(
88 | ctx.upstream_repo, ctx.release_pr["number"], pr_comment
89 | )
90 |
91 | # This isn't a tag, but that's okay - it just needs to be a commitish for
92 | # Kokoro to build against.
93 | ctx.release_tag = commitish
94 |
95 | ctx.kokoro_job_name = kokoro_job_name(ctx.upstream_repo, "")
96 | ctx.github.update_pull_labels(
97 | ctx.release_pr, add=["autorelease: tagged"], remove=["autorelease: pending"]
98 | )
99 | releasetool.commands.common.publish_via_kokoro(ctx)
100 |
101 |
102 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
103 | """Return the Kokoro job name.
104 |
105 | Args:
106 | upstream_repo (str): The GitHub repo in the form of `/`
107 | package_name (str): The name of package to release
108 |
109 | Returns:
110 | The name of the Kokoro job to trigger or None if there is no job to trigger
111 | """
112 | repo_short_name = upstream_repo.split("/")[-1]
113 | if repo_short_name in (
114 | "dotnet-spanner-entity-framework",
115 | "google-cloudevents-dotnet",
116 | ):
117 | return (
118 | f"cloud-libraries-dotnet/{repo_short_name}/rbe_windows_releases/autorelease"
119 | )
120 | else:
121 | return f"cloud-sharp/{repo_short_name}/gcp_windows/autorelease"
122 |
123 |
124 | def package_name(pull: dict) -> Union[str, None]:
125 | return None
126 |
127 |
128 | # Note: unlike other languages, the .NET libraries may need multiple
129 | # tags for a single release PR, usually for dependent APIs, e.g.
130 | # Google.Cloud.Spanner.Data depending on Google.Cloud.Spanner.V1.
131 | # We create multiple releases in the create_releases function, and set
132 | # ctx.release_tag to the commit we've tagged (as all tags will use the same commit).
133 | def tag(ctx: TagContext = None) -> TagContext:
134 | if not ctx:
135 | ctx = TagContext()
136 |
137 | if ctx.interactive:
138 | click.secho(f"o/ Hey, {getpass.getuser()}, let's tag a release!", fg="magenta")
139 |
140 | if ctx.github is None:
141 | releasetool.commands.common.setup_github_context(ctx)
142 |
143 | if ctx.release_pr is None:
144 | determine_release_pr(ctx)
145 |
146 | create_releases(ctx)
147 |
148 | return ctx
149 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/go.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from typing import Union
16 |
17 |
18 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
19 | """Return the Kokoro job name.
20 |
21 | Args:
22 | upstream_repo (str): The GitHub repo in the form of `/`
23 | package_name (str): The name of package to release
24 |
25 | Returns:
26 | The name of the Kokoro job to trigger or None if there is no job to trigger
27 | """
28 | return (
29 | f"cloud-devrel/client-libraries/go/{upstream_repo.rsplit('/', 1)[-1]}/release"
30 | )
31 |
32 |
33 | def package_name(pull: dict) -> Union[str, None]:
34 | return None
35 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/java.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import re
17 | import subprocess
18 | import tempfile
19 | import click
20 | from typing import List, Union
21 |
22 | import releasetool.circleci
23 | import releasetool.git
24 | import releasetool.github
25 | import releasetool.secrets
26 | import releasetool.commands.common
27 | from releasetool.commands.common import TagContext
28 |
29 |
30 | def _parse_release_tag(output: str) -> str:
31 | match = re.search("creating release (v.*)", output)
32 | if match:
33 | return match[1]
34 | return None
35 |
36 |
37 | """A list of repositories that have a different kokoro job location.
38 | """
39 | # Standard Java Framework repos in the GoogleCloudPlatform org
40 | java_framework_release_pool_repos: List[str] = [
41 | "google-cloud-spanner-hibernate",
42 | "spring-cloud-gcp",
43 | "cloud-spanner-r2dbc",
44 | ]
45 | # App engine plugin repos in the GoogleCloudPlatform org
46 | java_appengine_plugins_repos: List[str] = ["appengine-plugins"]
47 | functions_framework_java_packages: List[str] = [
48 | "functions-framework-api",
49 | "java-function-invoker",
50 | "function-maven-plugin",
51 | ]
52 |
53 |
54 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
55 | """Return the Kokoro job name.
56 |
57 | Args:
58 | upstream_repo (str): The GitHub repo in the form of `/`
59 | package_name (str): The name of package to release
60 |
61 | Returns:
62 | The name of the Kokoro job to trigger or None if there is no job to trigger
63 | """
64 | repo_short_name = upstream_repo.split("/")[-1]
65 |
66 | if repo_short_name in java_framework_release_pool_repos:
67 | return f"cloud-java-frameworks/{repo_short_name}/stage"
68 |
69 | if repo_short_name in java_appengine_plugins_repos:
70 | return "appengine-plugins-core/gcp_ubuntu/stage"
71 |
72 | if (
73 | repo_short_name == "functions-framework-java"
74 | and package_name in functions_framework_java_packages
75 | ):
76 | return f"functions-framework/java/{package_name}/release"
77 |
78 | else:
79 | return f"cloud-devrel/client-libraries/java/{repo_short_name}/release/stage"
80 |
81 |
82 | def package_name(pull: dict) -> Union[str, None]:
83 | if pull.__contains__("title"):
84 | title = pull["title"]
85 | match = re.search(".* release (.*) [0-9].*", title)
86 | if match:
87 | return match[1]
88 | return None
89 |
90 |
91 | def tag(ctx: TagContext = None) -> TagContext:
92 | if not ctx:
93 | ctx = TagContext()
94 |
95 | if ctx.interactive:
96 | click.secho(f"o/ Hey, {getpass.getuser()}, let's tag a release!", fg="magenta")
97 |
98 | if ctx.github is None:
99 | releasetool.commands.common.setup_github_context(ctx)
100 |
101 | # delegate releaase tagging to release-please
102 | default_branch = ctx.release_pr["base"]["ref"]
103 | repo = ctx.release_pr["base"]["repo"]["full_name"]
104 |
105 | with tempfile.NamedTemporaryFile("w+t", delete=False) as fp:
106 | fp.write(ctx.token)
107 | token_file = fp.name
108 |
109 | output = subprocess.check_output(
110 | [
111 | "npx",
112 | "release-please",
113 | "github-release",
114 | f"--token={token_file}",
115 | f"--default-branch={default_branch}",
116 | "--release-type=java-yoshi",
117 | "--bump-minor-pre-major=true",
118 | f"--repo-url={repo}",
119 | "--package-name=",
120 | "--debug",
121 | ]
122 | )
123 |
124 | ctx.release_tag = _parse_release_tag(output.decode("utf-8"))
125 | ctx.kokoro_job_name = kokoro_job_name(ctx.upstream_repo, ctx.package_name)
126 |
127 | if ctx.interactive:
128 | click.secho("\\o/ All done!", fg="magenta")
129 |
130 | return ctx
131 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/nodejs.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import re
17 | from typing import Union
18 |
19 | import click
20 |
21 | import releasetool.circleci
22 | import releasetool.git
23 | import releasetool.github
24 | import releasetool.secrets
25 | import releasetool.commands.common
26 | from releasetool.commands.common import TagContext
27 | import subprocess
28 | import tempfile
29 |
30 | # Repos that have their publication process handled by GitHub actions:
31 | manifest_release = [
32 | "googleapis/google-api-nodejs-client",
33 | "googleapis/repo-automation-bots",
34 | ]
35 |
36 |
37 | def determine_release_pr(ctx: TagContext) -> None:
38 | click.secho(
39 | "> Let's figure out which pull request corresponds to your release.", fg="cyan"
40 | )
41 |
42 | pulls = ctx.github.list_pull_requests(ctx.upstream_repo, state="closed")
43 | pulls = [pull for pull in pulls if "release" in pull["title"].lower()][:30]
44 |
45 | click.secho("> Please pick one of the following PRs:\n")
46 | for n, pull in enumerate(pulls, 1):
47 | print(f"\t{n}: {pull['title']} ({pull['number']})")
48 |
49 | pull_idx = click.prompt(
50 | "\nWhich one do you want to tag and release?", type=click.INT
51 | )
52 |
53 | ctx.release_pr = pulls[pull_idx - 1]
54 |
55 |
56 | def determine_release_tag(ctx: TagContext) -> None:
57 | click.secho("> Determining what the release tag should be.", fg="cyan")
58 | head_ref = ctx.release_pr["head"]["ref"]
59 | match = re.match("release-(.+)", head_ref)
60 |
61 | if match is not None:
62 | ctx.release_tag = match.group(1)
63 | else:
64 | click.secho(
65 | "I couldn't determine what the release tag should be from the PR's"
66 | f"head ref {head_ref}.",
67 | fg="red",
68 | )
69 | ctx.release_tag = click.prompt(
70 | "What should the release tag be (for example, storage-1.2.3)?"
71 | )
72 |
73 | click.secho(f"Release tag is {ctx.release_tag}.")
74 |
75 |
76 | def determine_package_version(ctx: TagContext) -> None:
77 | click.secho("> Determining the package version.", fg="cyan")
78 | match = re.match(r"(?Pv?\d+\.\d+\.\d+)", ctx.release_tag)
79 | ctx.release_version = match.group("version")
80 | click.secho(f"package version: {ctx.release_version}.")
81 |
82 |
83 | def get_release_notes(ctx: TagContext) -> None:
84 | click.secho("> Grabbing the release notes.")
85 | changelog = ctx.github.get_contents(
86 | ctx.upstream_repo, "CHANGELOG.md", ref=ctx.release_pr["merge_commit_sha"]
87 | ).decode("utf-8")
88 |
89 | _get_latest_release_notes(ctx, changelog)
90 |
91 |
92 | def _get_latest_release_notes(ctx: TagContext, changelog: str):
93 | # the 'v' prefix is not used in the conventional-changelog templates
94 | # used in automated CHANGELOG generation:
95 | version = re.sub(r"^v", "", ctx.release_version)
96 | match = re.search(
97 | rf"## v?\[?{version}[^\n]*\n(?P.+?)(\n##\s|\n### \[?[0-9]+\.|\Z)",
98 | changelog,
99 | re.DOTALL | re.MULTILINE,
100 | )
101 | if match is not None:
102 | ctx.release_notes = match.group("notes").strip()
103 | else:
104 | ctx.release_notes = ""
105 |
106 |
107 | def create_release(ctx: TagContext) -> None:
108 | click.secho("> Creating the release.")
109 |
110 | if ctx.upstream_repo in manifest_release:
111 | # delegate releaase tagging to release-please
112 | default_branch = ctx.release_pr["base"]["ref"]
113 | repo = ctx.release_pr["base"]["repo"]["full_name"]
114 |
115 | with tempfile.NamedTemporaryFile("w+t", delete=False) as fp:
116 | fp.write(ctx.token)
117 | token_file = fp.name
118 |
119 | subprocess.check_output(
120 | [
121 | # TODO(sofisl): remove pinning to a specific version
122 | # once we've tested:
123 | "npx",
124 | "release-please",
125 | "manifest-release",
126 | f"--token={token_file}",
127 | f"--default-branch={default_branch}",
128 | f"--repo-url={repo}",
129 | "--debug",
130 | ]
131 | )
132 | else:
133 | # TODO(sofisl): move the non-manifest release to release-please too
134 | # for consistency:
135 | ctx.github_release = ctx.github.create_release(
136 | repository=ctx.upstream_repo,
137 | tag_name=ctx.release_version,
138 | target_commitish=ctx.release_pr["merge_commit_sha"],
139 | name=f"{ctx.release_version}",
140 | body=ctx.release_notes,
141 | )
142 |
143 | release_location_string = f"Release is at {ctx.github_release['html_url']}"
144 | click.secho(release_location_string)
145 | click.secho("CI will handle publishing the package to npm.")
146 |
147 | ctx.github.create_pull_request_comment(
148 | ctx.upstream_repo, ctx.release_pr["number"], release_location_string
149 | )
150 |
151 | ctx.github.update_pull_labels(
152 | ctx.release_pr, add=["autorelease: tagged"], remove=["autorelease: pending"]
153 | )
154 |
155 |
156 | def wait_on_circle(ctx: TagContext) -> None:
157 | circle = releasetool.circleci.CircleCI(repository=ctx.upstream_repo)
158 | click.secho("> Waiting for CircleCI to queue a release build")
159 | tag_name = ctx.release_version
160 | fresh_build = circle.get_latest_build_by_tag(tag_name)
161 | if fresh_build:
162 | click.secho(f"CircleCI Build: {fresh_build['build_url']}")
163 | click.secho("> Monitoring CircleCI for completion of release")
164 | click.secho("")
165 | for state in circle.get_build_status_generator(fresh_build["build_num"]):
166 | click.secho(f"CircleCI Build State: {state}\r", nl=False)
167 | else:
168 | click.secho(f"CircleCI Build not found for tag {tag_name}...")
169 |
170 |
171 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
172 | """Return the Kokoro job name.
173 |
174 | Args:
175 | upstream_repo (str): The GitHub repo in the form of `/`
176 | package_name (str): The name of package to release
177 |
178 | Returns:
179 | The name of the Kokoro job to trigger or None if there is no job to trigger
180 | """
181 | return f"cloud-devrel/client-libraries/nodejs/release/{upstream_repo}/publish"
182 |
183 |
184 | def package_name(pull: dict) -> Union[str, None]:
185 | return None
186 |
187 |
188 | def tag(ctx: TagContext = None) -> TagContext:
189 | if not ctx:
190 | ctx = TagContext()
191 |
192 | if ctx.interactive:
193 | click.secho(f"o/ Hey, {getpass.getuser()}, let's tag a release!", fg="magenta")
194 |
195 | if ctx.github is None:
196 | releasetool.commands.common.setup_github_context(ctx)
197 |
198 | if ctx.release_pr is None:
199 | determine_release_pr(ctx)
200 |
201 | # If using manifest release, the manifest releaser determines
202 | # release tag, version, and release notes:
203 | if ctx.upstream_repo not in manifest_release:
204 | determine_release_tag(ctx)
205 | determine_package_version(ctx)
206 | # If the release already exists, don't do anything
207 | if releasetool.commands.common.release_exists(ctx):
208 | click.secho(f"{ctx.release_tag} already exists.", fg="magenta")
209 | return ctx
210 | get_release_notes(ctx)
211 | else:
212 | # If using mono-release strategy, fallback to using sha from
213 | # time of merge:
214 | ctx.release_tag = ctx.release_pr["merge_commit_sha"]
215 |
216 | create_release(ctx)
217 | ctx.kokoro_job_name = kokoro_job_name(ctx.upstream_repo, ctx.package_name)
218 | releasetool.commands.common.publish_via_kokoro(ctx)
219 |
220 | if ctx.interactive:
221 | click.secho("\\o/ All done!", fg="magenta")
222 |
223 | return ctx
224 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/php.py:
--------------------------------------------------------------------------------
1 | import getpass
2 | from typing import Union
3 |
4 | import click
5 |
6 | import releasetool.commands.tag.nodejs
7 | from releasetool.commands.common import TagContext
8 |
9 |
10 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
11 | """Return the Kokoro job name.
12 |
13 | Args:
14 | upstream_repo (str): The GitHub repo in the form of `/`
15 | package_name (str): The name of package to release
16 |
17 | Returns:
18 | The name of the Kokoro job to trigger or None if there is no job to trigger
19 | """
20 | repo_short_name = upstream_repo.split("/")[-1]
21 |
22 | if repo_short_name == "google-cloud-php":
23 | return "cloud-devrel/client-libraries/php/google-cloud-php/docs/docs"
24 | else:
25 | return f"cloud-devrel/client-libraries/php/{upstream_repo}/release"
26 |
27 |
28 | def package_name(pull: dict) -> Union[str, None]:
29 | return None
30 |
31 |
32 | def tag(ctx: TagContext = None) -> TagContext:
33 | # PHP just needs a release to be tagged on GitHub.
34 | # Tagging logic is the same as NodeJs.
35 | if not ctx:
36 | ctx = TagContext()
37 |
38 | if ctx.interactive:
39 | click.secho(f"o/ Hey, {getpass.getuser()}, let's tag a release!", fg="magenta")
40 |
41 | if ctx.github is None:
42 | releasetool.commands.common.setup_github_context(ctx)
43 |
44 | if ctx.release_pr is None:
45 | releasetool.commands.tag.nodejs.determine_release_pr(ctx)
46 |
47 | releasetool.commands.tag.nodejs.determine_release_tag(ctx)
48 | releasetool.commands.tag.nodejs.determine_package_version(ctx)
49 |
50 | # If the release already exists, don't do anything
51 | if releasetool.commands.common.release_exists(ctx):
52 | click.secho(f"{ctx.release_tag} already exists.", fg="magenta")
53 | return ctx
54 |
55 | releasetool.commands.tag.nodejs.get_release_notes(ctx)
56 | releasetool.commands.tag.nodejs.create_release(ctx)
57 |
58 | if ctx.interactive:
59 | click.secho("\\o/ All done!", fg="magenta")
60 |
61 | return ctx
62 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/python.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import re
17 | from typing import List, Union
18 |
19 | import click
20 |
21 | import releasetool.circleci
22 | import releasetool.git
23 | import releasetool.github
24 | import releasetool.secrets
25 | import releasetool.commands.common
26 | from releasetool.commands.common import TagContext
27 |
28 |
29 | def determine_release_pr(ctx: TagContext) -> None:
30 | click.secho(
31 | "> Let's figure out which pull request corresponds to your release.", fg="cyan"
32 | )
33 |
34 | pulls = ctx.github.list_pull_requests(ctx.upstream_repo, state="closed")
35 | pulls = [pull for pull in pulls if "release" in pull["title"].lower()][:30]
36 |
37 | click.secho("> Please pick one of the following PRs:\n")
38 | for n, pull in enumerate(pulls, 1):
39 | print(f"\t{n}: {pull['title']} ({pull['number']})")
40 |
41 | pull_idx = click.prompt(
42 | "\nWhich one do you want to tag and release?", type=click.INT
43 | )
44 |
45 | ctx.release_pr = pulls[pull_idx - 1]
46 |
47 |
48 | def determine_release_tag(ctx: TagContext) -> None:
49 | click.secho("> Determining what the release tag should be.", fg="cyan")
50 | head_ref = ctx.release_pr["head"]["ref"]
51 |
52 | # try release-please v13 pull requests
53 | title = ctx.release_pr["title"]
54 | match = re.match("chore\\(.*\\): release (\\d+\\.\\d+\\.\\d+.*)", title)
55 | if match is not None:
56 | ctx.release_tag = f"v{match.group(1)}"
57 | return
58 |
59 | match = re.match("release-(.+)", head_ref)
60 |
61 | if match is not None:
62 | ctx.release_tag = match.group(1)
63 | else:
64 | print(
65 | "I couldn't determine what the release tag should be from the PR's"
66 | f"head ref {head_ref}."
67 | )
68 | ctx.release_tag = click.prompt(
69 | "What should the release tag be (for example, v1.2.3)?"
70 | )
71 |
72 | click.secho(f"Release tag is {ctx.release_tag}.")
73 |
74 |
75 | def determine_package_version(ctx: TagContext) -> None:
76 | click.secho("> Determining the package version.", fg="cyan")
77 | # strip the leading 'v' from the tag
78 | ctx.release_version = re.sub(r"^v", "", ctx.release_tag)
79 | click.secho(f"Package version: {ctx.release_version}.")
80 |
81 |
82 | def get_release_notes(ctx: TagContext) -> None:
83 | click.secho("> Grabbing the release notes.")
84 |
85 | changelog_path = "CHANGELOG.md"
86 | changelog = ctx.github.get_contents(
87 | ctx.upstream_repo, changelog_path, ref=ctx.release_pr["merge_commit_sha"]
88 | ).decode("utf-8")
89 |
90 | _get_latest_release_notes(ctx, changelog)
91 |
92 |
93 | def _get_latest_release_notes(ctx: TagContext, changelog: str):
94 | match = re.search(
95 | rf"## v?\[?{ctx.release_version}[^\n]*\n(?P.+?)(\n##\s|\n### \[?[0-9]+\.|\Z)",
96 | changelog,
97 | re.DOTALL | re.MULTILINE,
98 | )
99 | if match is not None:
100 | ctx.release_notes = match.group("notes").strip()
101 | else:
102 | ctx.release_notes = ""
103 |
104 |
105 | def create_release(ctx: TagContext) -> None:
106 | click.secho("> Creating the release.")
107 |
108 | release_name = f"v{ctx.release_version}"
109 | ctx.github_release = ctx.github.create_release(
110 | repository=ctx.upstream_repo,
111 | tag_name=ctx.release_tag,
112 | target_commitish=ctx.release_pr["merge_commit_sha"],
113 | name=release_name,
114 | body=ctx.release_notes,
115 | )
116 |
117 | release_location_string = f"Release is at {ctx.github_release['html_url']}"
118 | click.secho(release_location_string)
119 |
120 | ctx.github.create_pull_request_comment(
121 | ctx.upstream_repo, ctx.release_pr["number"], release_location_string
122 | )
123 |
124 | ctx.github.update_pull_labels(
125 | ctx.release_pr, add=["autorelease: tagged"], remove=["autorelease: pending"]
126 | )
127 |
128 |
129 | """A list of repositories that have been migrated to use the new KOKORO VM
130 | pool dedicated to releasing client libraries.
131 | """
132 | release_pool_repos: List[str] = ["googleapis/python-apigee-connect"]
133 |
134 |
135 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
136 | """Return the Kokoro job name.
137 |
138 | Args:
139 | upstream_repo (str): The GitHub repo in the form of `/`
140 | package_name (str): The name of package to release
141 |
142 | Returns:
143 | The name of the Kokoro job to trigger or None if there is no job to trigger
144 | """
145 | if upstream_repo in release_pool_repos:
146 | return f"cloud-devrel/client-libraries/release/python/{upstream_repo}/release"
147 | else:
148 | return f"cloud-devrel/client-libraries/python/{upstream_repo}/release/release"
149 |
150 |
151 | def package_name(pull: dict) -> Union[str, None]:
152 | return None
153 |
154 |
155 | def tag(ctx: TagContext = None) -> TagContext:
156 | if not ctx:
157 | ctx = TagContext()
158 |
159 | if ctx.interactive:
160 | click.secho(f"o/ Hey, {getpass.getuser()}, let's tag a release!", fg="magenta")
161 |
162 | if ctx.github is None:
163 | releasetool.commands.common.setup_github_context(ctx)
164 |
165 | if ctx.release_pr is None:
166 | determine_release_pr(ctx)
167 |
168 | determine_release_tag(ctx)
169 | determine_package_version(ctx)
170 |
171 | # If the release already exists, don't do anything
172 | if releasetool.commands.common.release_exists(ctx):
173 | click.secho(f"{ctx.release_tag} already exists.", fg="magenta")
174 | return ctx
175 |
176 | get_release_notes(ctx)
177 |
178 | create_release(ctx)
179 |
180 | ctx.kokoro_job_name = kokoro_job_name(ctx.upstream_repo, ctx.package_name)
181 | releasetool.commands.common.publish_via_kokoro(ctx)
182 |
183 | if ctx.interactive:
184 | click.secho("\\o/ All done!", fg="magenta")
185 |
186 | return ctx
187 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/python_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import re
17 | from typing import Union
18 |
19 | import click
20 |
21 | import releasetool.commands.common
22 | from releasetool.commands.common import TagContext
23 | from releasetool.commands.tag import python
24 |
25 |
26 | def get_release_notes(ctx: TagContext) -> None:
27 | click.secho("> Grabbing the release notes.")
28 | changelog = ctx.github.get_contents(
29 | ctx.upstream_repo, "CHANGELOG.md", ref=ctx.release_pr["merge_commit_sha"]
30 | ).decode("utf-8")
31 |
32 | match = re.search(
33 | rf"## {ctx.release_version}\n(?P.+?)(\n##\s|\Z)",
34 | changelog,
35 | re.DOTALL | re.MULTILINE,
36 | )
37 | if match is not None:
38 | ctx.release_notes = match.group("notes").strip()
39 | else:
40 | ctx.release_notes = ""
41 |
42 |
43 | def create_release(ctx: TagContext) -> None:
44 | click.secho("> Creating the release.")
45 |
46 | ctx.github_release = ctx.github.create_release(
47 | repository=ctx.upstream_repo,
48 | tag_name=ctx.release_tag,
49 | target_commitish=ctx.release_pr["merge_commit_sha"],
50 | name=f"{ctx.package_name} {ctx.release_version}",
51 | body=ctx.release_notes,
52 | )
53 |
54 | release_location_string = f"Release is at {ctx.github_release['html_url']}"
55 | click.secho(release_location_string)
56 |
57 | ctx.github.create_pull_request_comment(
58 | ctx.upstream_repo, ctx.release_pr["number"], release_location_string
59 | )
60 |
61 | ctx.github.update_pull_labels(
62 | ctx.release_pr, add=["autorelease: tagged"], remove=["autorelease: pending"]
63 | )
64 |
65 |
66 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
67 | """Return the Kokoro job name.
68 |
69 | Args:
70 | upstream_repo (str): The GitHub repo in the form of `/`
71 | package_name (str): The name of package to release
72 |
73 | Returns:
74 | The name of the Kokoro job to trigger or None if there is no job to trigger
75 | """
76 | return f"cloud-devrel/client-libraries/{package_name}/release"
77 |
78 |
79 | def package_name(pull: dict) -> Union[str, None]:
80 | return None
81 |
82 |
83 | def tag(ctx: TagContext = None) -> TagContext:
84 | if not ctx:
85 | ctx = TagContext()
86 |
87 | if ctx.interactive:
88 | click.secho(f"o/ Hey, {getpass.getuser()}, let's tag a release!", fg="magenta")
89 |
90 | if ctx.github is None:
91 | releasetool.commands.common.setup_github_context(ctx)
92 |
93 | if ctx.release_pr is None:
94 | python.determine_release_pr(ctx)
95 |
96 | python.determine_release_tag(ctx)
97 | python.determine_package_version(ctx)
98 |
99 | # If the release already exists, don't do anything
100 | if releasetool.commands.common.release_exists(ctx):
101 | click.secho(f"{ctx.release_tag} already exists.", fg="magenta")
102 | return ctx
103 |
104 | get_release_notes(ctx)
105 |
106 | create_release(ctx)
107 |
108 | ctx.kokoro_job_name = kokoro_job_name(ctx.upstream_repo, ctx.package_name)
109 | releasetool.commands.common.publish_via_kokoro(ctx)
110 |
111 | if ctx.interactive:
112 | click.secho("\\o/ All done!", fg="magenta")
113 |
114 | return ctx
115 |
--------------------------------------------------------------------------------
/releasetool/commands/tag/ruby.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import getpass
16 | import re
17 | from typing import Union
18 |
19 | import click
20 | from requests import HTTPError
21 |
22 | import releasetool.circleci
23 | import releasetool.git
24 | import releasetool.github
25 | import releasetool.secrets
26 | import releasetool.commands.common
27 | from releasetool.commands.common import TagContext
28 |
29 |
30 | # Standard Ruby repos in the googleapis org
31 | RUBY_CLIENT_REPOS = [
32 | "common-protos-ruby",
33 | "gapic-generator-ruby",
34 | "google-api-ruby-client",
35 | "google-auth-library-ruby",
36 | "google-cloud-ruby",
37 | "google-cloudevents-ruby",
38 | "opentelemetry-operations-ruby",
39 | "ruby-cloud-env",
40 | "ruby-core-libraries",
41 | "ruby-spanner",
42 | "ruby-spanner-activerecord",
43 | "ruby-style",
44 | "signet",
45 | ]
46 |
47 | # Standard Ruby repos in the GoogleCloudPlatform org
48 | RUBY_CLOUD_REPOS = [
49 | "appengine-ruby",
50 | "functions-framework-ruby",
51 | "serverless-exec-ruby",
52 | ]
53 |
54 | # Standard Ruby monorepos with gems located in subdirectories
55 | RUBY_MONO_REPOS = [
56 | "common-protos-ruby",
57 | "opentelemetry-operations-ruby",
58 | "gapic-generator-ruby",
59 | "google-api-ruby-client",
60 | "google-cloud-ruby",
61 | "ruby-core-libraries",
62 | "ruby-spanner",
63 | ]
64 |
65 |
66 | def determine_release_pr(ctx: TagContext) -> None:
67 | click.secho(
68 | "> Let's figure out which pull request corresponds to your release.", fg="cyan"
69 | )
70 |
71 | pulls = ctx.github.list_pull_requests(ctx.upstream_repo, state="closed")
72 | pulls = [pull for pull in pulls if "release" in pull["title"].lower()][:30]
73 |
74 | click.secho("> Please pick one of the following PRs:\n")
75 | for n, pull in enumerate(pulls, 1):
76 | print(f"\t{n}: {pull['title']} ({pull['number']})")
77 |
78 | pull_idx = click.prompt(
79 | "\nWhich one do you want to tag and release?", type=click.INT
80 | )
81 |
82 | ctx.release_pr = pulls[pull_idx - 1]
83 |
84 |
85 | def determine_release_tag(ctx: TagContext) -> None:
86 | click.secho("> Determining the release tag.", fg="cyan")
87 | head_ref = ctx.release_pr["head"]["ref"]
88 | click.secho(f"PR head ref is {head_ref}")
89 | match = re.match(r"release-(.+)-v(\d+\.\d+\.\d+)", head_ref)
90 | rp13_match = re.match(r"release-please--branches--(.+)--components--(.+)", head_ref)
91 | title_match = re.match(
92 | r"chore\(.+\): release (.+) (\d+\.\d+\.\d+)", ctx.release_pr["title"]
93 | )
94 |
95 | if match is not None:
96 | ctx.package_name = match.group(1)
97 | ctx.release_version = match.group(2)
98 | ctx.release_tag = f"{ctx.package_name}/v{ctx.release_version}"
99 | elif rp13_match is not None and title_match is not None:
100 | ctx.package_name = title_match.group(1)
101 | ctx.release_version = title_match.group(2)
102 | ctx.release_tag = f"{ctx.package_name}/v{ctx.release_version}"
103 |
104 | if ctx.release_tag is None:
105 | click.secho(
106 | "I couldn't determine what the release tag should be from the PR's "
107 | f"head ref {head_ref}.",
108 | fg="red",
109 | )
110 | ctx.release_tag = click.prompt(
111 | "What should the release tag be (for example, google-cloud-storage/v1.2.3)?"
112 | )
113 |
114 | click.secho(f"Package name is {ctx.package_name}")
115 | click.secho(f"Package version is {ctx.release_version}")
116 | click.secho(f"Release tag is {ctx.release_tag}")
117 |
118 |
119 | def determine_package_name_and_version(ctx: TagContext) -> None:
120 | click.secho(
121 | "> Determining the package name and version from your release tag.", fg="cyan"
122 | )
123 | match = re.match(r"^([a-z0-9-_]+)\/v(\d+.\d+.\d+)$", ctx.release_tag)
124 | ctx.package_name = match.group(1)
125 | ctx.release_version = match.group(2)
126 |
127 |
128 | def get_release_notes(ctx: TagContext) -> None:
129 | click.secho("> Grabbing the release notes.", fg="cyan")
130 | changelog_file = "CHANGELOG.md"
131 | repo_name = ctx.upstream_repo.split("/")[-1]
132 | for name in RUBY_MONO_REPOS:
133 | if name == repo_name:
134 | changelog_file = f"{ctx.package_name}/CHANGELOG.md"
135 | changelog = ctx.github.get_contents(
136 | ctx.upstream_repo, changelog_file, ref=ctx.release_pr["merge_commit_sha"]
137 | ).decode("utf-8")
138 |
139 | match = re.search(
140 | rf"^### {ctx.release_version} \/ \d\d\d\d-\d\d-\d\d\n(?P.+?)(\n###\s|\Z)",
141 | changelog,
142 | re.DOTALL | re.MULTILINE,
143 | )
144 | v13_match = re.search(
145 | rf"^### {ctx.release_version} \(\d\d\d\d-\d\d-\d\d\)\n(?P.+?)(\n###\s|\Z)",
146 | changelog,
147 | re.DOTALL | re.MULTILINE,
148 | )
149 | if match is not None:
150 | ctx.release_notes = match.group("notes").strip()
151 | elif v13_match is not None:
152 | ctx.release_notes = v13_match.group("notes").strip()
153 | else:
154 | ctx.release_notes = ""
155 |
156 | click.secho(f"Here's the release notes:\n\n{ctx.release_notes}\n")
157 |
158 |
159 | def create_release(ctx: TagContext) -> None:
160 | click.secho("> Creating the release.")
161 | click.secho(f"pkg={ctx.package_name} pr={ctx.release_pr['number']}")
162 |
163 | ctx.github_release = ctx.github.create_release(
164 | repository=ctx.upstream_repo,
165 | tag_name=ctx.release_tag,
166 | target_commitish=ctx.release_pr["merge_commit_sha"],
167 | name=f"Release {ctx.package_name} {ctx.release_version}",
168 | body=ctx.release_notes,
169 | )
170 |
171 | release_location_string = f"Release is at {ctx.github_release['html_url']}"
172 | click.secho(release_location_string)
173 | click.secho("CI will handle publishing the package to Rubygems.")
174 |
175 | ctx.github.create_pull_request_comment(
176 | ctx.upstream_repo, ctx.release_pr["number"], release_location_string
177 | )
178 |
179 | ctx.github.update_pull_labels(
180 | ctx.release_pr, add=["autorelease: tagged"], remove=["autorelease: pending"]
181 | )
182 |
183 |
184 | def kokoro_job_name(upstream_repo: str, package_name: str) -> Union[str, None]:
185 | """Return the Kokoro job name.
186 |
187 | Args:
188 | upstream_repo (str): The GitHub repo in the form of `/`
189 | package_name (str): The name of package to release
190 |
191 | Returns:
192 | The name of the Kokoro job to trigger or None if there is no job to trigger
193 | """
194 |
195 | repo_name = upstream_repo.split("/")[-1]
196 | for name in RUBY_CLIENT_REPOS:
197 | if name == repo_name:
198 | return f"cloud-devrel/client-libraries/{name}/release"
199 | for name in RUBY_CLOUD_REPOS:
200 | if name == repo_name:
201 | return f"cloud-devrel/ruby/{name}/release"
202 |
203 | return f"cloud-devrel/client-libraries/{package_name}/release"
204 |
205 |
206 | def package_name(pull: dict) -> Union[str, None]:
207 | head_ref = pull["head"]["ref"]
208 | click.secho(f"PR head ref is {head_ref}")
209 | match = re.match(r"release-(.+)-v(\d+\.\d+\.\d+)", head_ref)
210 | if match is None:
211 | return None
212 |
213 | return match.group(1)
214 |
215 |
216 | def tag(ctx: TagContext = None) -> TagContext:
217 | if not ctx:
218 | ctx = TagContext()
219 |
220 | if ctx.interactive:
221 | click.secho(
222 | f"o/ Hey, {getpass.getuser()}, let's tag a Ruby release!", fg="magenta"
223 | )
224 |
225 | if ctx.github is None:
226 | releasetool.commands.common.setup_github_context(ctx)
227 |
228 | if ctx.release_pr is None:
229 | determine_release_pr(ctx)
230 |
231 | determine_release_tag(ctx)
232 | determine_package_name_and_version(ctx)
233 |
234 | # If the release already exists, don't do anything
235 | if releasetool.commands.common.release_exists(ctx):
236 | click.secho(f"{ctx.release_tag} already exists.", fg="magenta")
237 | return ctx
238 |
239 | get_release_notes(ctx)
240 |
241 | create_release(ctx)
242 | ctx.kokoro_job_name = kokoro_job_name(ctx.upstream_repo, ctx.package_name)
243 |
244 | releasetool.commands.common.publish_via_kokoro(ctx)
245 |
246 | if ctx.interactive:
247 | click.secho("\\o/ All done!", fg="magenta")
248 |
249 | branch = ctx.release_pr["head"]["ref"]
250 |
251 | try:
252 | ctx.github.delete_branch(repository=ctx.upstream_repo, branch=branch)
253 | click.secho(f"Deleted branch {branch}")
254 | # If user has already deleted the branch, this will fail.
255 | except HTTPError as exc:
256 | if exc.response.status_code != 422:
257 | click.secho(f"{exc!r}")
258 |
259 | return ctx
260 |
--------------------------------------------------------------------------------
/releasetool/filehelpers.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import re
17 | import tempfile
18 | from typing import Optional
19 |
20 | import click
21 |
22 |
23 | def open_editor(filename: str, return_contents: bool = False) -> Optional[str]:
24 | click.edit(filename=filename)
25 |
26 | if return_contents:
27 | with open(filename, "r") as fh:
28 | return fh.read()
29 | else:
30 | return None
31 |
32 |
33 | def open_editor_with_content(
34 | filename: str, contents: str, return_contents: bool = False
35 | ) -> Optional[str]:
36 | with open(filename, "w") as fh:
37 | fh.write(contents)
38 |
39 | return open_editor(filename, return_contents=return_contents)
40 |
41 |
42 | def open_editor_with_tempfile(contents: str, suffix: str = ".txt") -> Optional[str]:
43 | handle, filename = tempfile.mkstemp(suffix)
44 | os.close(handle)
45 |
46 | content = open_editor_with_content(filename, contents, return_contents=True)
47 |
48 | os.remove(filename)
49 |
50 | return content
51 |
52 |
53 | def insert_before(
54 | filename: str, new_content: str, expr: str, separator: str = "\n"
55 | ) -> None:
56 | with open(filename, "r+") as fh:
57 | if not new_content.endswith(separator):
58 | new_content += separator
59 |
60 | content = fh.read()
61 | match = re.search(expr, content, re.MULTILINE)
62 |
63 | if not match:
64 | return
65 |
66 | position = match.start()
67 |
68 | output = content[:position] + new_content + content[position:]
69 |
70 | fh.seek(0)
71 | fh.write(output)
72 |
73 |
74 | def replace(filename: str, expr: str, replacement: str) -> None:
75 | with open(filename, "r+") as fh:
76 | content = fh.read()
77 |
78 | content = re.sub(expr, replacement, content)
79 |
80 | fh.seek(0)
81 | fh.write(content)
82 | fh.truncate()
83 |
84 |
85 | def extract(filename: str, expr: str) -> str:
86 | with open(filename, "r") as fh:
87 | content = fh.read()
88 |
89 | matches = re.search(expr, content)
90 | return matches.group(1)
91 |
--------------------------------------------------------------------------------
/releasetool/git.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import re
16 | import subprocess
17 | from typing import Dict, Sequence
18 |
19 |
20 | def list_tags() -> Sequence[str]:
21 | subprocess.check_output(["git", "fetch", "--tags"])
22 | output = subprocess.check_output(
23 | ["git", "tag", "--list", "--sort=-creatordate"]
24 | ).decode("utf-8")
25 | tags = output.split("\n")
26 |
27 | return tags
28 |
29 |
30 | def get_latest_commit(branch: str) -> str:
31 | commit = subprocess.check_output(
32 | ["git", "log", "-1", branch, "--pretty=%H"]
33 | ).decode("utf-8")
34 | return commit
35 |
36 |
37 | def summary_log(
38 | from_: str, to: str = "master", where: str = ".", format: str = "%s"
39 | ) -> Sequence[str]:
40 | output = subprocess.check_output(
41 | ["git", "log", f"--format={format}", f"{from_}..{to}", where]
42 | ).decode("utf-8")
43 | commits = output.strip().split("\n")
44 | return commits
45 |
46 |
47 | def log(from_: str, to: str = "master", where: str = ".") -> Sequence[str]:
48 | return subprocess.check_output(["git", "log", f"{from_}..{to}", where]).decode(
49 | "utf-8"
50 | )
51 |
52 |
53 | def diff(from_: str, to: str = "master", where: str = ".") -> Sequence[str]:
54 | return subprocess.check_output(
55 | ["git", "diff", f"{from_}..{to}", "--", where]
56 | ).decode("utf-8")
57 |
58 |
59 | def checkout_create_branch(branch_name: str, base: str = "master") -> None:
60 | subprocess.check_output(["git", "checkout", "-b", branch_name, base])
61 |
62 |
63 | def checkout_branch(branch_name: str) -> None:
64 | subprocess.check_output(["git", "checkout", branch_name])
65 |
66 |
67 | def commit(files: Sequence[str], message: str) -> None:
68 | """Create a release commit."""
69 | subprocess.check_output(["git", "add"] + list(files))
70 | subprocess.check_output(["git", "commit", "-m", message])
71 |
72 |
73 | def push(branch: str, remote: str = "origin") -> None:
74 | """Push the release branch to the remote."""
75 | subprocess.check_output(["git", "push", "-u", remote, branch])
76 |
77 |
78 | def get_config() -> Dict[str, str]:
79 | output = subprocess.check_output(["git", "config", "--list"]).decode("utf-8")
80 |
81 | lines = [line for line in output.split("\n") if line]
82 | pairs = [line.split("=", 1) for line in lines]
83 | config = {key: value for key, value in pairs}
84 |
85 | return config
86 |
87 |
88 | def get_remotes() -> Dict[str, str]:
89 | config = get_config()
90 |
91 | remote_names = []
92 |
93 | for key in config.keys():
94 | match = re.match(r"remote\.(?P.+?)\.url", key)
95 | if match:
96 | remote_names.append(match.group("name"))
97 |
98 | remotes = {name: config[f"remote.{name}.url"] for name in remote_names}
99 | return remotes
100 |
101 |
102 | def get_github_remotes() -> Dict[str, str]:
103 | """Returns a dictionary mapping remote names to the appropriate github
104 | owner/repo string."""
105 | remotes = get_remotes()
106 |
107 | github_repos = {}
108 |
109 | for name, url in remotes.items():
110 | # Match SSH or HTTP URLs, like:
111 | # git@github.com:GoogleCloudPlatform/google-cloud-python.git
112 | # https://github.com/GoogleCloudPlatform/google-cloud-python.git
113 | match = re.match(r"^git@github.com:(?P.+?)(\.git$|$)", url)
114 | if match:
115 | github_repos[name] = match.group("name")
116 | continue
117 |
118 | match = re.match(r"^https://(.+?)?github.com/(?P.+?)(\.git$|$)", url)
119 | if match:
120 | github_repos[name] = match.group("name")
121 | continue
122 |
123 | return github_repos
124 |
125 |
126 | def current_branch() -> str:
127 | """Returns the name of the current working branch."""
128 | return (
129 | subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
130 | .strip()
131 | .decode("utf-8")
132 | )
133 |
--------------------------------------------------------------------------------
/releasetool/secrets.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import click
16 | import keyring
17 |
18 | _SERVICE = "com.google.cloud.devrel.releasetool"
19 |
20 |
21 | def get_password(name):
22 | return keyring.get_password(_SERVICE, name)
23 |
24 |
25 | def set_password(name, password):
26 | """Ensure we have a github username and token."""
27 | keyring.set_password(_SERVICE, "github", password)
28 |
29 |
30 | def delete_password():
31 | keyring.delete_password(_SERVICE, "github")
32 |
33 |
34 | def ensure_password(name, prompt):
35 | password = get_password(name)
36 |
37 | if not password:
38 | password = click.prompt(prompt)
39 | set_password(name, password)
40 |
41 | return password
42 |
--------------------------------------------------------------------------------
/releasetool/update_check.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import time
16 | import pathlib
17 |
18 | import packaging.version
19 | import requests
20 |
21 | import importlib.metadata as metadata
22 |
23 |
24 | def _get_pypi_version(package_name: str) -> str:
25 | r = requests.get(f"https://pypi.org/pypi/{package_name}/json")
26 | r.raise_for_status()
27 |
28 | return r.json()["info"]["version"]
29 |
30 |
31 | def _only_once_pls(package_name: str) -> bool:
32 | flag = pathlib.Path.home() / ".cache" / f"update-check-{package_name}"
33 |
34 | if not flag.exists():
35 | flag.parent.mkdir(parents=True, exist_ok=True)
36 | flag.touch()
37 | return True
38 |
39 | last_check = flag.stat().st_mtime
40 | one_day_in_seconds = 60 * 60 * 24
41 |
42 | if last_check < time.time() - one_day_in_seconds:
43 | flag.touch()
44 | return True
45 | else:
46 | return False
47 |
48 |
49 | def check_for_updates(package_name: str, print=print) -> None:
50 | if not _only_once_pls(package_name):
51 | return
52 |
53 | current_version = packaging.version.Version(
54 | metadata.distribution(package_name).version
55 | )
56 |
57 | pypi_version = packaging.version.Version(_get_pypi_version(package_name))
58 |
59 | if current_version >= pypi_version:
60 | return
61 |
62 | print(
63 | f"{package_name} has a newer version available. Current version is "
64 | f"{current_version}, newest is {pypi_version}. Run `python3 -m pip "
65 | f"install --upgrade {package_name}` to update."
66 | )
67 |
--------------------------------------------------------------------------------
/requirements-dev.in:
--------------------------------------------------------------------------------
1 | nox==2023.4.22
2 | pytest==7.4.4
3 | requests_mock==1.11.0
4 |
--------------------------------------------------------------------------------
/requirements.in:
--------------------------------------------------------------------------------
1 | attrs>=20.1.0
2 | bleach==4.1.0
3 | cachetools==4.2.4
4 | docutils==0.18.1
5 | click>=8.0.4, <8.1.0
6 | cryptography>=42
7 | google-auth>=2.22.0
8 | jaraco.classes==3.2.1
9 | jeepney==0.7.1
10 | jinja2==3.1.4
11 | keyring==21.8.0
12 | markupsafe==2.0.1
13 | protobuf>=4.21.6
14 | packaging>=20.0
15 | pyjwt>=2.0.0
16 | pyperclip>=1.8.0
17 | python-dateutil>=2.8.1
18 | readme-renderer==34.0
19 | requests>=2.31.0
20 | rfc3986==1.5.0
21 | twine==3.8.0
22 | wheel
23 | zipp==3.19.1
24 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import setuptools
16 |
17 | name = 'gcp-releasetool'
18 | description = ''
19 | version = "2.6.0"
20 | release_status = 'Development Status :: 3 - Alpha'
21 | dependencies = [
22 | "requests>=2.31.0",
23 | "attrs>=20.1.0",
24 | "click>=8.0.4, <8.1.0",
25 | "cryptography>=42",
26 | "google-auth>=2.22.0",
27 | "jinja2>=3.1.3",
28 | "keyring>=21.8.0",
29 | "packaging>=20.0",
30 | "protobuf>=4.21.6",
31 | "pyjwt>=2.0.0",
32 | "pyperclip>=1.8.0",
33 | "python-dateutil>=2.8.1",
34 | ]
35 |
36 | packages = setuptools.find_packages()
37 | scripts = [
38 | 'releasetool=releasetool.__main__:main'
39 | ]
40 |
41 |
42 | setuptools.setup(
43 | name=name,
44 | version=version,
45 | description=description,
46 | author='Google LLC',
47 | author_email='theaflowers@google.com',
48 | license='Apache 2.0',
49 | url='',
50 | classifiers=[
51 | release_status,
52 | 'Intended Audience :: Developers',
53 | 'License :: OSI Approved :: Apache Software License',
54 | 'Programming Language :: Python',
55 | 'Programming Language :: Python :: 3.8',
56 | 'Programming Language :: Python :: 3.9',
57 | 'Programming Language :: Python :: 3.10',
58 | 'Programming Language :: Python :: 3.11',
59 | 'Programming Language :: Python :: 3.12',
60 | 'Operating System :: OS Independent',
61 | 'Topic :: Internet',
62 | ],
63 | platforms='Posix; MacOS X; Windows',
64 | packages=packages,
65 | python_requires='>=3.8',
66 | install_requires=dependencies,
67 | include_package_data=True,
68 | zip_safe=False,
69 | entry_points={
70 | 'console_scripts': scripts,
71 | },
72 | package_data={
73 | 'autorelease': ['*.j2']
74 | },
75 | )
76 |
--------------------------------------------------------------------------------
/testing/constraints-3.10.txt:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
--------------------------------------------------------------------------------
/testing/constraints-3.11.txt:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
--------------------------------------------------------------------------------
/testing/constraints-3.12.txt:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
--------------------------------------------------------------------------------
/testing/constraints-3.8.txt:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | # This constraints file is used to check that lower bounds
15 | # are correct in setup.py
16 | # List all library dependencies and extras in this file.
17 | # Pin the version to the lower bound.
18 | # e.g., if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0dev",
19 | # Then this file should have google-cloud-foo==1.14.0
20 | attrs==20.1.0
21 | google-auth==2.22.0
22 | jinja2==3.1.4
23 | keyring==21.8.0
24 | packaging==20.0
25 | pyjwt==2.0.0
26 | pyperclip==1.8.0
27 | python-dateutil==2.8.1
28 | requests==2.32.2
29 | click==8.0.4
30 | cryptography==43.0.1
31 | protobuf==4.21.6
32 | importlib-metadata==4.8.3
33 |
--------------------------------------------------------------------------------
/testing/constraints-3.9.txt:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleapis/releasetool/4a6e25c4a0cb6b6db1815f578012edb8cb884595/tests/__init__.py
--------------------------------------------------------------------------------
/tests/commands/start/test_python.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from unittest import mock
16 |
17 | import pytest
18 |
19 |
20 | @pytest.fixture
21 | def mut():
22 | from releasetool.commands.start import python
23 |
24 | return python
25 |
26 |
27 | @pytest.mark.parametrize(
28 | "setup_py_contents,release_version,expected",
29 | [
30 | ("version = '1.0.0'\n", "1.1.0", "version = '1.1.0'\n"),
31 | ('version = "1.0.0"\n', "1.1.0", 'version = "1.1.0"\n'),
32 | ],
33 | )
34 | def test_update_setup_py_sets_version(
35 | mut, setup_py_contents, release_version, expected
36 | ):
37 | context = mut.Context()
38 | context.release_version = release_version
39 |
40 | with mock.patch(
41 | "builtins.open", mock.mock_open(read_data=setup_py_contents)
42 | ) as mock_open:
43 | mut.update_setup_py(context)
44 | mock_file = mock_open()
45 | mock_file.write.assert_called_once_with(expected)
46 |
47 |
48 | @pytest.mark.parametrize(
49 | "tags,package_name,expected",
50 | [
51 | (
52 | ["bonustag", "bigquery-1.3.0", "bigquery-1.2.0", "bigquery-1.0.0"],
53 | "bigquery",
54 | "bigquery-1.3.0",
55 | ),
56 | (
57 | [
58 | "bonustag",
59 | "bigquery-1.3.0",
60 | "bigquery-1.2.0",
61 | "bigquery_storage-0.2.0",
62 | "bigquery-1.0.0",
63 | "bigquery_datatransfer-0.3.0",
64 | "bigquery_datatransfer-0.2.0",
65 | "bigquery_storage-0.1.1",
66 | "bigquery_datatransfer-0.1.1",
67 | "bigquery_storage-0.1.0",
68 | ],
69 | "bigquery_storage",
70 | "bigquery_storage-0.2.0",
71 | ),
72 | (
73 | [
74 | "bonustag",
75 | "bigquery_datatransfer-0.3.0",
76 | "bigquery_storage-0.2.0",
77 | "bigquery-1.3.0",
78 | "bigquery-1.2.0",
79 | "bigquery-1.0.0",
80 | "bigquery_datatransfer-0.2.0",
81 | "bigquery_datatransfer-0.1.1",
82 | "bigquery_storage-0.1.1",
83 | "bigquery_storage-0.1.0",
84 | ],
85 | "bigquery",
86 | "bigquery-1.3.0",
87 | ),
88 | (["mypackage-1.0.0", "bonustag", "mypackage-0.9.0"], "myotherpackage", None),
89 | ],
90 | )
91 | def find_last_release_tag(mut, tags, package_name, expected):
92 | candidate = mut.find_last_release_tag(tags, package_name)
93 | assert candidate == expected
94 |
--------------------------------------------------------------------------------
/tests/commands/start/test_ruby.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import pytest
16 |
17 |
18 | @pytest.fixture
19 | def mut():
20 | from releasetool.commands.start import ruby
21 |
22 | return ruby
23 |
24 |
25 | def test_determine_last_release(mut):
26 | context = mut.Context()
27 | context.tags = ["google-cloud-spanner/v1.1.1", "google-cloud/v2.2.2"]
28 | context.package_name = "google-cloud"
29 |
30 | mut.determine_last_release(context)
31 | assert context.last_release_committish == "google-cloud/v2.2.2"
32 | assert context.last_release_version == "2.2.2"
33 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_cpp.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from releasetool.commands.tag.cpp import kokoro_job_name, package_name
16 |
17 |
18 | def test_kokoro_job_name():
19 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
20 | assert job_name == "cloud-devrel/client-libraries/cpp/upstream-repo/release/publish"
21 |
22 |
23 | def test_package_name():
24 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
25 | assert name is None
26 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_dotnet.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020, Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import pytest
16 | import re
17 |
18 | from releasetool.commands.tag.dotnet import (
19 | RELEASE_LINE_PATTERN,
20 | kokoro_job_name,
21 | package_name,
22 | )
23 |
24 |
25 | release_triggering_lines = [
26 | ("Release Google.LongRunning version 1.2.3", "Google.LongRunning", "1.2.3"),
27 | (
28 | "Release Google.LongRunning version 1.2.3-beta01",
29 | "Google.LongRunning",
30 | "1.2.3-beta01",
31 | ),
32 | ("- Release Google.LongRunning version 1.2.3", "Google.LongRunning", "1.2.3"),
33 | ]
34 |
35 | non_release_triggering_lines = [
36 | ("Release new version of all OsLogin packages"),
37 | ("Release all OsLogin packages version 1.2.3"),
38 | ("Release Google.LongRunning version 1.0"),
39 | ("Release Google.LongRunning version 1.2.3 and 1.2.4"),
40 | ]
41 |
42 |
43 | @pytest.mark.parametrize("line,package,version", release_triggering_lines)
44 | def test_release_line_regex_matching(line, package, version):
45 | """
46 | The regex can extract a well-formatted package and version
47 | """
48 | match = re.search(RELEASE_LINE_PATTERN, line)
49 | assert match is not None
50 | assert match.group(2) == package
51 | assert match.group(4) == version
52 |
53 |
54 | @pytest.mark.parametrize("line", non_release_triggering_lines)
55 | def test_release_line_regex_not_matching(line):
56 | """
57 | The regex is strict enough not to match other lines.
58 | """
59 | match = re.search(RELEASE_LINE_PATTERN, line)
60 | assert match is None
61 |
62 |
63 | def test_kokoro_job_name():
64 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
65 | assert job_name == "cloud-sharp/upstream-repo/gcp_windows/autorelease"
66 |
67 |
68 | def test_package_name():
69 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
70 | assert name is None
71 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_go.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from releasetool.commands.tag.go import kokoro_job_name, package_name
16 |
17 |
18 | def test_kokoro_job_name():
19 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
20 | assert job_name == "cloud-devrel/client-libraries/go/upstream-repo/release"
21 |
22 |
23 | def test_package_name():
24 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
25 | assert name is None
26 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_java.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from releasetool.commands.tag.java import (
16 | _parse_release_tag,
17 | kokoro_job_name,
18 | package_name,
19 | )
20 |
21 | RELEASE_PLEASE_OUTPUT = """
22 | ✔ creating release v1.20.0
23 | ✔ Created release: https://github.com/googleapis/java-bigtable/releases/tag/v1.20.0.
24 | ✔ adding comment to https://github.com/googleapis/java-bigtable/issue/610
25 | ✔ adding label autorelease: tagged to https://github.com/googleapis/java-bigtable/pull/610
26 | ✔ removing label autorelease: pending from 610
27 | """
28 |
29 |
30 | def test_releasetool_release_tag():
31 | expected = "v1.20.0"
32 | assert _parse_release_tag(RELEASE_PLEASE_OUTPUT) == expected
33 |
34 |
35 | def test_kokoro_job_name():
36 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
37 | assert job_name == "cloud-devrel/client-libraries/java/upstream-repo/release/stage"
38 |
39 |
40 | def test_package_name():
41 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
42 | assert name is None
43 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_nodejs.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from releasetool.commands.tag.nodejs import (
16 | _get_latest_release_notes,
17 | kokoro_job_name,
18 | package_name,
19 | )
20 | from releasetool.commands.common import TagContext
21 |
22 | fixture_old_style_changelog = """
23 | # Changelog
24 |
25 | [npm history][1]
26 |
27 | [1]: https://www.npmjs.com/package/dialogflow?activeTab=versions
28 |
29 | ## v0.8.2
30 |
31 | 03-13-2019 16:30 PDT
32 |
33 | ### Bug Fixes
34 | - fix: throw on invalid credentials ([#281](https://github.com/googleapis/nodejs-dialogflow/pull/281))
35 |
36 | ## v0.8.1
37 |
38 | 01-28-2019 13:24 PST
39 |
40 | ### Documentation
41 | - fix(docs): dialogflow inn't published under @google-cloud scope ([#266](https://github.com/googleapis/nodejs-dialogflow/pull/266))
42 |
43 | ## v0.8.0
44 | """
45 |
46 | fixture_new_and_old_style_changelog = """
47 | # Changelog
48 |
49 | [npm history][1]
50 |
51 | [1]: https://www.npmjs.com/package/@google-cloud/os-login?activeTab=versions
52 |
53 | ### [0.3.3](https://www.github.com/googleapis/nodejs-os-login/compare/v0.3.2...v0.3.3) (2019-04-30)
54 |
55 |
56 | ### Bug Fixes
57 |
58 | * include 'x-goog-request-params' header in requests ([#167](https://www.github.com/googleapis/nodejs-os-login/issues/167)) ([074051d](https://www.github.com/googleapis/nodejs-os-login/commit/074051d))
59 |
60 | ## v0.3.2
61 |
62 | 03-18-2019 13:47 PDT
63 |
64 | ### Implementation Changes
65 | - refactor: update json import paths ([#156](https://github.com/googleapis/nodejs-os-login/pull/156))
66 | - fix: throw on invalid credentials
67 | """
68 |
69 | fixture_new_style_changelog = """
70 | # Change Log
71 |
72 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
73 |
74 | ## [2.0.0](https://www.github.com/bcoe/examples-conventional-commits/compare/v1.3.0...v2.0.0) (2019-04-29)
75 |
76 |
77 | ### Features
78 |
79 | * added the most amazing feature ever ([42f90e2](https://www.github.com/bcoe/examples-conventional-commits/commit/42f90e2))
80 | * adds a fancy new feature ([c46bfa3](https://www.github.com/bcoe/examples-conventional-commits/commit/c46bfa3))
81 |
82 |
83 | ### BREAKING CHANGES
84 |
85 | * this fancy new feature breaks things
86 | * disclaimer breaks everything
87 |
88 | ## [1.3.0](https://github.com/bcoe/examples-conventional-commits/compare/v1.2.1...v1.3.0) (2018-11-03)
89 | """
90 |
91 | fixture_new_style_patch = """
92 | # Changelog
93 |
94 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
95 |
96 | ## [5.0.0](https://www.github.com/bcoe/c8/compare/v4.1.5...v5.0.0) (2019-05-20)
97 |
98 |
99 | ### ⚠ BREAKING CHANGES
100 |
101 | * temp directory now defaults to setting for report directory
102 |
103 | ### Features
104 |
105 | * default temp directory to report directory ([#102](https://www.github.com/bcoe/c8/issues/102)) ([8602f4a](https://www.github.com/bcoe/c8/commit/8602f4a))
106 | * load .nycrc/.nycrc.json to simplify migration ([#100](https://www.github.com/bcoe/c8/issues/100)) ([bd7484f](https://www.github.com/bcoe/c8/commit/bd7484f))
107 |
108 | ### [4.1.5](https://github.com/bcoe/c8/compare/v4.1.4...v4.1.5) (2019-05-11)
109 |
110 |
111 | ### Bug Fixes
112 |
113 | * exit with code 1 when report output fails ([#92](https://github.com/bcoe/c8/issues/92)) ([a27b694](https://github.com/bcoe/c8/commit/a27b694))
114 | * remove the unmaintained mkdirp dependency ([#91](https://github.com/bcoe/c8/issues/91)) ([a465b65](https://github.com/bcoe/c8/commit/a465b65))
115 |
116 |
117 |
118 | ## [4.1.4](https://github.com/bcoe/c8/compare/v4.1.3...v4.1.4) (2019-05-03)
119 |
120 |
121 | ### Bug Fixes
122 |
123 | * we were not exiting with 1 if mkdir failed ([#89](https://github.com/bcoe/c8/issues/89)) ([fb02ed6](https://github.com/bcoe/c8/commit/fb02ed6))
124 | """
125 |
126 |
127 | def test_old_style_release_notes():
128 | """
129 | Our old CHANGELOG template does not make the version header a link and
130 | always uses H2 headers.
131 | """
132 | expected = """03-13-2019 16:30 PDT
133 |
134 | ### Bug Fixes
135 | - fix: throw on invalid credentials ([#281](https://github.com/googleapis/nodejs-dialogflow/pull/281))"""
136 | ctx = TagContext(release_version="v0.8.2")
137 | _get_latest_release_notes(ctx, fixture_old_style_changelog)
138 | assert ctx.release_notes == expected
139 |
140 |
141 | def test_new_style_release_notes_patch():
142 | """
143 | In the conventional-commits template (see: https://github.com/conventional-changelog/conventional-changelog),
144 | patches are an H3 header and are linked to the underlying issue that created the release.
145 | """
146 | expected = """### Bug Fixes
147 |
148 | * include 'x-goog-request-params' header in requests ([#167](https://www.github.com/googleapis/nodejs-os-login/issues/167)) ([074051d](https://www.github.com/googleapis/nodejs-os-login/commit/074051d))"""
149 | ctx = TagContext(release_version="v0.3.3")
150 | _get_latest_release_notes(ctx, fixture_new_and_old_style_changelog)
151 | assert ctx.release_notes == expected
152 |
153 |
154 | def test_new_style_release_notes_breaking():
155 | """
156 | in the conventional-commits template, features/breaking-changes use an H2 header.
157 | """
158 | expected = """### Features
159 |
160 | * added the most amazing feature ever ([42f90e2](https://www.github.com/bcoe/examples-conventional-commits/commit/42f90e2))
161 | * adds a fancy new feature ([c46bfa3](https://www.github.com/bcoe/examples-conventional-commits/commit/c46bfa3))
162 |
163 |
164 | ### BREAKING CHANGES
165 |
166 | * this fancy new feature breaks things
167 | * disclaimer breaks everything"""
168 | ctx = TagContext(release_version="v2.0.0")
169 | _get_latest_release_notes(ctx, fixture_new_style_changelog)
170 | assert ctx.release_notes == expected
171 |
172 |
173 | def test_extracts_appropriate_release_notes_when_prior_release_is_patch():
174 | """
175 | see: https://github.com/googleapis/release-please/issues/140
176 | """
177 | ctx = TagContext(release_version="v5.0.0")
178 | _get_latest_release_notes(ctx, fixture_new_style_patch)
179 | expected = """### ⚠ BREAKING CHANGES
180 |
181 | * temp directory now defaults to setting for report directory
182 |
183 | ### Features
184 |
185 | * default temp directory to report directory ([#102](https://www.github.com/bcoe/c8/issues/102)) ([8602f4a](https://www.github.com/bcoe/c8/commit/8602f4a))
186 | * load .nycrc/.nycrc.json to simplify migration ([#100](https://www.github.com/bcoe/c8/issues/100)) ([bd7484f](https://www.github.com/bcoe/c8/commit/bd7484f))"""
187 | assert ctx.release_notes == expected
188 |
189 |
190 | def test_kokoro_job_name():
191 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
192 | assert (
193 | job_name
194 | == "cloud-devrel/client-libraries/nodejs/release/upstream-owner/upstream-repo/publish"
195 | )
196 |
197 |
198 | def test_package_name():
199 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
200 | assert name is None
201 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_php.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from releasetool.commands.tag.php import kokoro_job_name, package_name
16 |
17 |
18 | def test_kokoro_job_name():
19 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
20 | assert (
21 | job_name
22 | == "cloud-devrel/client-libraries/php/upstream-owner/upstream-repo/release"
23 | )
24 | job_name = kokoro_job_name("upstream-owner/google-cloud-php", "some-package-name")
25 | assert job_name == "cloud-devrel/client-libraries/php/google-cloud-php/docs/docs"
26 |
27 |
28 | def test_package_name():
29 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
30 | assert name is None
31 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_python.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from releasetool.commands.tag.python import kokoro_job_name, package_name
16 |
17 |
18 | def test_kokoro_job_name():
19 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
20 | assert (
21 | job_name
22 | == "cloud-devrel/client-libraries/python/upstream-owner/upstream-repo/release/release"
23 | )
24 |
25 |
26 | def test_package_name():
27 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
28 | assert name is None
29 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_python_tool.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from releasetool.commands.tag.python_tool import kokoro_job_name, package_name
16 |
17 |
18 | def test_kokoro_job_name():
19 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
20 | assert job_name == "cloud-devrel/client-libraries/some-package-name/release"
21 |
22 |
23 | def test_package_name():
24 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
25 | assert name is None
26 |
--------------------------------------------------------------------------------
/tests/commands/tag/test_tag_ruby.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from unittest import mock
16 | from releasetool.github import GitHub
17 | from releasetool.commands.common import TagContext
18 | from releasetool.commands.tag.ruby import (
19 | kokoro_job_name,
20 | package_name,
21 | get_release_notes,
22 | )
23 |
24 |
25 | def test_kokoro_job_name():
26 | job_name = kokoro_job_name("upstream-owner/upstream-repo", "some-package-name")
27 | assert job_name == "cloud-devrel/client-libraries/some-package-name/release"
28 |
29 |
30 | def test_kokoro_job_name_gapic():
31 | job_name = kokoro_job_name(
32 | "googleapis/google-cloud-ruby", "google-cloud-video-intelligence"
33 | )
34 | assert job_name == "cloud-devrel/client-libraries/google-cloud-ruby/release"
35 |
36 |
37 | def test_kokoro_job_name_functions_framework():
38 | job_name = kokoro_job_name(
39 | "GoogleCloud/functions-framework-ruby", "functions_framework"
40 | )
41 | assert job_name == "cloud-devrel/ruby/functions-framework-ruby/release"
42 |
43 |
44 | def test_kokoro_job_name_apiary():
45 | job_name = kokoro_job_name("googleapis/google-api-ruby-client", "youtube")
46 | assert job_name == "cloud-devrel/client-libraries/google-api-ruby-client/release"
47 |
48 |
49 | def test_kokoro_job_name_adapter():
50 | job_name = kokoro_job_name(
51 | "googleapis/ruby-spanner-activerecord", "ruby-spanner-activerecord"
52 | )
53 | assert job_name == "cloud-devrel/client-libraries/ruby-spanner-activerecord/release"
54 |
55 |
56 | def test_package_name():
57 | name = package_name({"head": {"ref": "release-storage-v1.2.3"}})
58 | assert name == "storage"
59 |
60 |
61 | def test_get_release_notes_monorepo():
62 | ctx = TagContext()
63 | ctx.package_name = "google-cloud-spanner"
64 | ctx.upstream_name = "origin"
65 | ctx.upstream_repo = "googleapis/ruby-spanner"
66 | ctx.release_version = "1.2.3"
67 | ctx.release_pr = {"merge_commit_sha": "abc123"}
68 | ctx.github = mock.Mock(autospec=GitHub)
69 |
70 | contents = "### 1.2.3 (2022-12-08)\n\n#### Features\n\n* something\n"
71 | ctx.github.get_contents.return_value = contents.encode("utf-8")
72 |
73 | get_release_notes(ctx)
74 |
75 | assert ctx.release_notes == "#### Features\n\n* something"
76 |
77 | ctx.github.get_contents.assert_called_once_with(
78 | "googleapis/ruby-spanner", "google-cloud-spanner/CHANGELOG.md", ref="abc123"
79 | )
80 |
81 |
82 | def test_get_release_notes_non_monorepo():
83 | ctx = TagContext()
84 | ctx.package_name = "ruby-spanner-activerecord"
85 | ctx.upstream_name = "origin"
86 | ctx.upstream_repo = "googleapis/ruby-spanner-activerecord"
87 | ctx.release_version = "1.2.3"
88 | ctx.release_pr = {"merge_commit_sha": "abc123"}
89 | ctx.github = mock.Mock(autospec=GitHub)
90 |
91 | contents = "### 1.2.3 (2022-12-08)\n\n#### Features\n\n* something\n"
92 | ctx.github.get_contents.return_value = contents.encode("utf-8")
93 |
94 | get_release_notes(ctx)
95 |
96 | assert ctx.release_notes == "#### Features\n\n* something"
97 |
98 | ctx.github.get_contents.assert_called_once_with(
99 | "googleapis/ruby-spanner-activerecord", "CHANGELOG.md", ref="abc123"
100 | )
101 |
--------------------------------------------------------------------------------
/tests/test_autorelease_tag.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import requests_mock
16 | from unittest.mock import patch, Mock
17 |
18 | from autorelease import tag
19 |
20 |
21 | @patch("autorelease.tag.process_issue")
22 | @patch("autorelease.github.GitHub.list_org_issues")
23 | @patch("autorelease.kokoro.make_authorized_session")
24 | def test_no_issues(make_authorized_session, list_org_issues, process_issue):
25 | list_org_issues.return_value = []
26 |
27 | tag.main("github-token", "kokoro-credentials")
28 | make_authorized_session.assert_called_once()
29 | process_issue.assert_not_called()
30 |
31 |
32 | @patch("autorelease.tag.process_issue")
33 | @patch("autorelease.github.GitHub.list_org_issues")
34 | @patch("autorelease.kokoro.make_authorized_session")
35 | def test_processes_issues(make_authorized_session, list_org_issues, process_issue):
36 | pr1 = {
37 | "base": {"ref": "abc123", "repo": {"full_name": "googleapis/java-asset"}},
38 | "pull_request": {"html_url": "https://github.com/googleapis/java-asset"},
39 | "title": "chore: release 1.2.3",
40 | }
41 | pr2 = {
42 | "base": {"ref": "def456", "repo": {"full_name": "googleapis/nodejs-container"}},
43 | "pull_request": {"html_url": "https://github.com/nodejs/java-container"},
44 | "title": "chore: release 1.0.0",
45 | }
46 | list_org_issues.side_effect = [[pr1, pr2]]
47 | tag.main("github-token", "kokoro-credentials")
48 | list_org_issues.assert_any_call(
49 | org="googleapis", state="closed", labels="autorelease: pending"
50 | )
51 | list_org_issues.assert_any_call(
52 | org="GoogleCloudPlatform", state="closed", labels="autorelease: pending"
53 | )
54 | assert process_issue.call_count == 2
55 |
56 |
57 | @patch("releasetool.commands.tag.java.tag")
58 | def test_run_releasetool_tag_delegates(tag_mock):
59 | github = Mock()
60 | github.token = "github-token"
61 | context = Mock()
62 | tag_mock.return_value = context
63 | pull = {"base": {"ref": "abc123", "repo": {"full_name": "googleapis/java-asset"}}}
64 | ctx = tag.run_releasetool_tag("java", github, pull)
65 | assert ctx == context
66 |
67 |
68 | @patch("autorelease.tag.LANGUAGE_ALLOWLIST", ["java"])
69 | @patch("autorelease.tag.run_releasetool_tag")
70 | def test_process_issue_skips_non_merged(run_releasetool_tag):
71 | github = Mock()
72 | github.update_pull_labels = Mock()
73 | github.get_url.return_value = {
74 | "merged_at": None,
75 | "base": {"repo": {"full_name": "googleapis/java-asset"}},
76 | }
77 | issue = {
78 | "pull_request": {"url": "https://api.github.com/googleapis/java-asset/pull/5"}
79 | }
80 | tag.process_issue(Mock(), github, issue, Mock())
81 | github.update_pull_labels.assert_called_once()
82 | run_releasetool_tag.assert_not_called()
83 |
84 |
85 | @patch("autorelease.tag.LANGUAGE_ALLOWLIST", ["java"])
86 | @patch("autorelease.kokoro.trigger_build")
87 | @patch("autorelease.tag.run_releasetool_tag")
88 | def test_process_issue_triggers_kokoro(run_releasetool_tag, trigger_build):
89 | github = Mock()
90 | github.get_url.return_value = {
91 | "merged_at": "2021-01-01T09:00:00.000Z",
92 | "base": {"repo": {"full_name": "googleapis/java-asset"}},
93 | "html_url": "https://github.com/googleapis/java-asset/pulls/5",
94 | }
95 | context = Mock()
96 | context.kokoro_job_name = "kokoro-job-name"
97 | context.release_tag = "v1.2.3"
98 | run_releasetool_tag.return_value = context
99 | issue = {
100 | "pull_request": {"url": "https://api.github.com/googleapis/java-asset/pull/5"},
101 | "merged_at": "2021-01-01T09:00:00.000Z",
102 | }
103 | tag.process_issue(Mock(), github, issue, Mock())
104 | run_releasetool_tag.assert_called_once()
105 | trigger_build.assert_called_once()
106 |
107 |
108 | @patch("autorelease.tag.LANGUAGE_ALLOWLIST", ["java"])
109 | @patch("autorelease.kokoro.trigger_build")
110 | @patch("autorelease.tag.run_releasetool_tag")
111 | def test_process_issue_skips_kokoro_if_no_job_name(run_releasetool_tag, trigger_build):
112 | github = Mock()
113 | github.get_url.return_value = {
114 | "merged_at": "2021-01-01T09:00:00.000Z",
115 | "base": {"repo": {"full_name": "googleapis/java-asset"}},
116 | "html_url": "https://github.com/googleapis/java-asset/pulls/5",
117 | }
118 | context = Mock()
119 | context.kokoro_job_name = None
120 | context.release_tag = None
121 | run_releasetool_tag.return_value = context
122 | issue = {
123 | "pull_request": {"url": "https://api.github.com/googleapis/java-asset/pull/5"},
124 | "merged_at": "2021-01-01T09:00:00.000Z",
125 | }
126 | tag.process_issue(Mock(), github, issue, Mock())
127 | run_releasetool_tag.assert_called_once()
128 | trigger_build.assert_not_called()
129 |
130 |
131 | @patch("autorelease.tag.LANGUAGE_ALLOWLIST", ["java"])
132 | @patch("autorelease.tag.run_releasetool_tag")
133 | @patch("autorelease.github.GitHub.list_org_issues")
134 | @patch("autorelease.kokoro.make_authorized_session")
135 | def test_respects_allowlist(
136 | make_authorized_session, list_org_issues, run_releasetool_tag
137 | ):
138 | pr1 = {
139 | "base": {"ref": "abc123", "repo": {"full_name": "googleapis/java-asset"}},
140 | "pull_request": {
141 | "url": "https://api.github.com/repos/googleapis/java-asset/pull/123",
142 | "html_url": "https://github.com/googleapis/java-asset",
143 | },
144 | "title": "chore: release 1.2.3",
145 | }
146 | pr2 = {
147 | "base": {"ref": "def456", "repo": {"full_name": "googleapis/nodejs-container"}},
148 | "pull_request": {
149 | "url": "https://api.github.com/repos/googleapis/nodejs-container/pull/234",
150 | "html_url": "https://github.com/nodejs/nodejs-container",
151 | },
152 | "title": "chore: release 1.0.0",
153 | }
154 | list_org_issues.side_effect = [[pr1, pr2]]
155 | with requests_mock.Mocker() as m:
156 | m.get(
157 | "https://api.github.com/repos/googleapis/java-asset/pull/123",
158 | json={
159 | "merged_at": "2021-01-01T09:00:00.000Z",
160 | "base": {"repo": {"full_name": "googleapis/java-asset"}},
161 | },
162 | )
163 | m.get(
164 | "https://api.github.com/repos/googleapis/nodejs-container/pull/234",
165 | json={
166 | "merged_at": "2021-01-01T09:00:00.000Z",
167 | "base": {"repo": {"full_name": "googleapis/nodejs-container"}},
168 | },
169 | )
170 | tag.main("github-token", "kokoro-credentials")
171 | list_org_issues.assert_any_call(
172 | org="googleapis", state="closed", labels="autorelease: pending"
173 | )
174 | list_org_issues.assert_any_call(
175 | org="GoogleCloudPlatform", state="closed", labels="autorelease: pending"
176 | )
177 | assert run_releasetool_tag.call_count == 1
178 |
--------------------------------------------------------------------------------
/tests/test_dummy.py:
--------------------------------------------------------------------------------
1 | def test_dummy():
2 | assert True
3 |
--------------------------------------------------------------------------------
/tests/test_github.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | from releasetool import github
17 | import pathlib
18 | import requests_mock
19 |
20 |
21 | def test_app_credentials():
22 | with requests_mock.Mocker() as m:
23 | m.post(
24 | "https://api.github.com/app/installations/my-installation-id/access_tokens",
25 | status_code=201,
26 | json={
27 | "token": "remote-access-token",
28 | },
29 | )
30 |
31 | private_key = (
32 | pathlib.Path(__file__).parent / "testdata" / "fake-private-key.pem"
33 | ).read_text()
34 | token = github.get_installation_access_token(
35 | "my-app-id", "my-installation-id", private_key
36 | )
37 | assert token == "remote-access-token"
38 |
--------------------------------------------------------------------------------
/tests/test_publish_reporter.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import pytest
17 | import unittest
18 | from unittest.mock import patch
19 | import releasetool.commands.publish_reporter
20 |
21 |
22 | class PublishReporter(unittest.TestCase):
23 | def test_publish_reporter_start_devrel_api_key(self):
24 | with patch.dict(os.environ, {"KOKORO_KEYSTORE_DIR": "./"}):
25 | with pytest.raises(Exception) as err:
26 | releasetool.commands.publish_reporter.start(
27 | "abc123", "http://example.com"
28 | )
29 | assert "magic github proxy api key is required" in str(err.value)
30 |
31 | def test_publish_reporter_finish_devrel_api_key(self):
32 | with patch.dict(os.environ, {"KOKORO_KEYSTORE_DIR": "./"}):
33 | with pytest.raises(Exception) as err:
34 | releasetool.commands.publish_reporter.finish(
35 | "abc123", "http://example.com", True, ""
36 | )
37 | assert "magic github proxy api key is required" in str(err.value)
38 |
39 | # TODO: use requests_mock to flesh out more thorough tests
40 | # see: https://github.com/googleapis/synthtool/blob/8cf6d2834ad14318e64429c3b94f6443ae83daf9/tests/test_language_java.py#L69-L76
41 |
--------------------------------------------------------------------------------
/tests/testdata/fake-private-key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIICWgIBAAKBgGCmk6xUrFM3e8qS3gph31HusC/JmSf7+7+sG/8POpg/8gzBVq2m
3 | zcEGubHJWwWZHKxrDVKkyZc3G3u7XSD1+N+hh+LDf1Kt4JccDV5OJXZ6tCedjH6Z
4 | Ty1qX8nxb6SF8GZctypTzA5lZk2VVVeR5ficnKDGCngReUOYXCmXoRplAgMBAAEC
5 | gYBWE0QlD+vA2QL4cEArQureTxK+HG63+2RDWYY9a1Slzx1EWtNVJ97Kb7DlMwxL
6 | OgcdTuG4nmWitENXuI/CEQ2pEKNmUAKMqorhSHqL5mFJi7Oe5m8guNqM4ClvJlCS
7 | UKgj6v6B7uEPDsMEojvNllJElyBcw2ld4Ji6VN4LxsleKQJBALjICFD514yYdjtg
8 | 3n8gJpZI9vIFTIwDnLcClIzZAfJwXU1hyaY7jVQKif9VOjUDZr3DGek8tPNeqIVb
9 | Xa9aH4MCQQCF5uK0YSk1yxBBzzoS+5fAAkNI0qYwBNPAoFhwZ+TT0y0S3enRI+m6
10 | 5iYKOwef7E2QVYXUEOhvUIiKUAQFBxH3AkBRcxr3VqnEt4+mLNTmhG194Tu5Aszz
11 | CsSRhvmj/CP3kcAO1APm2mk5mkup2Q+HPrCTBOTvAmtgu2DdJ6DsInWxAkBptNi5
12 | n45h6hm+ajKlc7rbmK23WpxZgiYMhkjrDAmoc6i8oTWJpjlJE5FqOCmPxYOB8xIA
13 | VQy5e7Eex4Y01d0HAkA9c6tb9jFxFHQdcSSM02UuZAjA+YiboO5znl4FCC6lpgBd
14 | irnvYZ5sD2Ba6baUW2/IqQwuTL/t1QJpqbwwuJi5
15 | -----END RSA PRIVATE KEY-----
--------------------------------------------------------------------------------