Skip to content

Commit 0fa1bd7

Browse files
authored
Add workflow to automatically update iOS and Android dependencies. (#629)
This workflow will: - Check out the repo and create a branch - Update the iOS and/or Android dependencies in that branch - Push the branch to GitHub - Create a PR on the branch - Trigger integration tests to run on the PR (via label) For now, to use, go to "Actions", select the "Update Android and iOS dependencies" workflow, and use workflow_dispatch to run the workflow. In most cases, the default inputs should be fine.
1 parent 3c21909 commit 0fa1bd7

File tree

5 files changed

+258
-4
lines changed

5 files changed

+258
-4
lines changed

.github/workflows/lint.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ jobs:
2222
uses: actions/setup-python@v2
2323
with:
2424
python-version: 3.7
25+
- name: Install prerequisites
26+
run: |
27+
cd firebase
28+
python scripts/gha/install_prereqs_desktop.py
2529
- name: Install prerequisites
2630
run: |
2731
python3 -m pip install unidiff
Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,137 @@
1-
name: Update Android and iOS dependencies (placeholder)
1+
name: Update Android and iOS dependencies
22
on:
33
workflow_dispatch:
44
inputs:
5+
updateAndroid:
6+
description: 'update Android dependencies?'
7+
default: 1
8+
updateiOS:
9+
description: 'update iOS dependencies?'
10+
default: 1
11+
triggerTests:
12+
description: 'trigger tests on PR?'
13+
default: 1
14+
baseBranch:
15+
description: 'create the new branch from this base'
16+
default: 'main'
17+
18+
env:
19+
branchPrefix: "workflow/auto-update-deps-"
20+
triggerTestsLabel: "tests-requested: quick"
521

622
jobs:
723
update_dependencies:
8-
name: update-dependencies
24+
name: update-deps
925
runs-on: ubuntu-latest
1026
steps:
11-
- name: Placeholder step
27+
- name: Get token for firebase-workflow-trigger
28+
uses: tibdex/github-app-token@v1
29+
id: generate-token
30+
with:
31+
app_id: ${{ secrets.WORKFLOW_TRIGGER_APP_ID }}
32+
private_key: ${{ secrets.WORKFLOW_TRIGGER_APP_PRIVATE_KEY }}
33+
34+
- name: Setup python
35+
uses: actions/setup-python@v2
36+
with:
37+
python-version: 3.7
38+
39+
- name: Check out base branch
40+
uses: actions/[email protected]
41+
with:
42+
fetch-depth: 0
43+
ref: ${{ github.event.inputs.baseBranch }}
44+
45+
- name: Install prerequisites
46+
run: |
47+
python scripts/gha/install_prereqs_desktop.py
48+
python -m pip install requests
49+
50+
- name: Name new branch
51+
run: |
52+
date_str=$(date "+%Y%m%d-%H%M%S")
53+
echo "NEW_BRANCH=${{env.branchPrefix}}${{github.run_number}}-${date_str}" >> $GITHUB_ENV
54+
55+
- name: Create new branch
1256
run: |
13-
true
57+
git remote update
58+
git checkout -b "${NEW_BRANCH}"
59+
echo "UPDATE_LOGFILE=update_log.txt" >> $GITHUB_ENV
60+
61+
- name: Run update script
62+
run: |
63+
if [[ ${{ github.event.inputs.updateiOS }} -eq 1 ]]; then
64+
if [[ ${{ github.event.inputs.updateAndroid }} -eq 1 ]]; then
65+
# Update both
66+
echo "Updating all dependencies"
67+
python scripts/update_android_ios_dependencies.py --logfile=${UPDATE_LOGFILE}
68+
echo "CHOSEN_DEPS=mobile" >> $GITHUB_ENV
69+
else
70+
# Update iOS only
71+
echo "Updating iOS dependencies only"
72+
python scripts/update_android_ios_dependencies.py --skip_android --logfile=${UPDATE_LOGFILE}
73+
echo "CHOSEN_DEPS=iOS" >> $GITHUB_ENV
74+
fi
75+
# iOS: Update Firestore external version to match Firestore Cocoapod version.
76+
firestore_version=$(grep "pod 'Firebase/Firestore'" ios_pod/Podfile | sed "s/.*'\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)'.*/\1/")
77+
sed -i~ "s/^set(version [^)]*)/set(version CocoaPods-${firestore_version})/i" cmake/external/firestore.cmake
78+
elif [[ ${{ github.event.inputs.updateAndroid }} -eq 1 ]]; then
79+
# Update Android only
80+
echo "Updating Android dependencies only"
81+
python scripts/update_android_ios_dependencies.py --skip_ios --logfile=${UPDATE_LOGFILE}
82+
echo "CHOSEN_DEPS=Android" >> $GITHUB_ENV
83+
else
84+
echo "::error ::Neither Android nor iOS selected. Exiting."
85+
exit 1
86+
fi
87+
88+
- name: Push branch if there are changes
89+
id: push-branch
90+
run: |
91+
if ! git update-index --refresh; then
92+
# Do a bit of post-processing on the update log to split it by platform.
93+
UPDATE_LOGFILE_PROCESSED=update_log_processed.txt
94+
if grep -q ^Android: "${UPDATE_LOGFILE}"; then
95+
echo "### Android" >> "${UPDATE_LOGFILE_PROCESSED}"
96+
echo "" >> "${UPDATE_LOGFILE_PROCESSED}"
97+
sed 's/^Android: /- /' ${UPDATE_LOGFILE} >> ${UPDATE_LOGFILE_PROCESSED}
98+
echo "" >> "${UPDATE_LOGFILE_PROCESSED}"
99+
fi
100+
if grep -q ^iOS: "${UPDATE_LOGFILE}"; then
101+
echo "### iOS" >> "${UPDATE_LOGFILE_PROCESSED}"
102+
echo "" >> "${UPDATE_LOGFILE_PROCESSED}"
103+
sed 's/^iOS: /- /' ${UPDATE_LOGFILE} >> ${UPDATE_LOGFILE_PROCESSED}
104+
echo "" >> "${UPDATE_LOGFILE_PROCESSED}"
105+
fi
106+
107+
date_str=$(date "+%a %b %d %Y")
108+
commit_title="Update ${CHOSEN_DEPS} dependencies - ${date_str}"
109+
commit_body="$(cat ${UPDATE_LOGFILE_PROCESSED})
110+
111+
> Created by [${{github.workflow}} workflow]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
112+
git config user.email "[email protected]"
113+
git config user.name "firebase-workflow-trigger-bot"
114+
git config core.commentChar "%" # so we can use # in git commit messages
115+
git commit -a -m "${commit_title}
116+
117+
${commit_body}"
118+
echo "::set-output name=branch_pushed::1"
119+
# Show changes in git log
120+
git diff
121+
# Push branch
122+
git push --set-upstream origin "${NEW_BRANCH}"
123+
# Create pull request
124+
pr_number=$(python scripts/gha/create_pull_request.py --token ${{ steps.generate-token.outputs.token }} --head "${NEW_BRANCH}" --base "${{ github.event.inputs.baseBranch }}" --title "${commit_title}" --body "${commit_body}")
125+
echo "::set-output name=created_pr_number::${pr_number}"
126+
else
127+
echo "::warning ::No changes detected, won't create pull request."
128+
echo "::set-output name=branch_pushed::0"
129+
fi
130+
131+
- name: Set test trigger label.
132+
uses: actions-ecosystem/action-add-labels@v1
133+
if: ${{ github.event.inputs.triggerTests == 1 && steps.push-branch.outputs.branch_pushed == 1 }}
134+
with:
135+
github_token: ${{ steps.generate-token.outputs.token }}
136+
number: ${{ steps.push-branch.outputs.created_pr_number }}
137+
labels: "${{ env.triggerTestsLabel }}"

scripts/gha/create_pull_request.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
# 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+
"""A utility to create pull requests.
16+
17+
USAGE:
18+
python scripts/gha/create_pull_request.py \
19+
--token ${{github.token}} \
20+
--head pr_branch \
21+
--base main \
22+
--title 'Title of the pull request' \
23+
[--body 'Body text for the pull request']
24+
25+
Creates the pull request, and outputs the new PR number to stdout.
26+
"""
27+
28+
import datetime
29+
import shutil
30+
31+
from absl import app
32+
from absl import flags
33+
from absl import logging
34+
35+
import github
36+
37+
FLAGS = flags.FLAGS
38+
_DEFAULT_MESSAGE = "Creating pull request."
39+
40+
flags.DEFINE_string(
41+
"token", None,
42+
"github.token: A token to authenticate on your repository.")
43+
44+
flags.DEFINE_string(
45+
"head", None,
46+
"Head branch name.")
47+
48+
flags.DEFINE_string(
49+
"base", "main",
50+
"Base branch name.")
51+
52+
flags.DEFINE_string(
53+
"title", None,
54+
"Title for the pull request.")
55+
56+
flags.DEFINE_string(
57+
"body", "",
58+
"Body text for the pull request.")
59+
60+
def main(argv):
61+
if len(argv) > 1:
62+
raise app.UsageError("Too many command-line arguments.")
63+
if github.create_pull_request(FLAGS.token, FLAGS.head, FLAGS.base, FLAGS.title, FLAGS.body, True):
64+
# Find the most recent pull_request with the given base and head, that's ours.
65+
pull_requests = github.list_pull_requests(FLAGS.token, "open", FLAGS.head, FLAGS.base)
66+
print(pull_requests[0]['number'])
67+
else:
68+
exit(1)
69+
70+
71+
if __name__ == "__main__":
72+
flags.mark_flag_as_required("token")
73+
flags.mark_flag_as_required("head")
74+
flags.mark_flag_as_required("title")
75+
app.run(main)

scripts/gha/github.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,39 @@ def workflow_dispatch(token, workflow_id, ref, inputs):
211211
with requests.post(url, headers=headers, data=json.dumps(data),
212212
stream=True, timeout=TIMEOUT) as response:
213213
logging.info("workflow_dispatch: %s response: %s", url, response)
214+
215+
216+
def create_pull_request(token, head, base, title, body, maintainer_can_modify):
217+
"""https://docs.github.com/en/rest/reference/pulls#create-a-pull-request"""
218+
url = f'{FIREBASE_URL}/pulls'
219+
headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'}
220+
data = {'head': head, 'base': base, 'title': title, 'body': body,
221+
'maintainer_can_modify': maintainer_can_modify}
222+
with requests.post(url, headers=headers, data=json.dumps(data),
223+
stream=True, timeout=TIMEOUT) as response:
224+
logging.info("create_pull_request: %s response: %s", head, response)
225+
return True if response.status_code == 201 else False
226+
227+
def list_pull_requests(token, state, head, base):
228+
"""https://docs.github.com/en/rest/reference/pulls#list-pull-requests"""
229+
url = f'{FIREBASE_URL}/pulls'
230+
headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'}
231+
page = 1
232+
per_page = 100
233+
results = []
234+
keep_going = True
235+
while keep_going:
236+
params = {'per_page': per_page, 'page': page}
237+
if state: params.update({'state': state})
238+
if head: params.update({'head': head})
239+
if base: params.update({'base': base})
240+
page = page + 1
241+
keep_going = False
242+
with requests_retry_session().get(url, headers=headers, params=params,
243+
stream=True, timeout=TIMEOUT) as response:
244+
logging.info("get_reviews: %s response: %s", url, response)
245+
results = results + response.json()
246+
# If exactly per_page results were retrieved, read the next page.
247+
keep_going = (len(response.json()) == per_page)
248+
return results
249+

scripts/update_android_ios_dependencies.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
--depfiles
6363
--readmefiles
6464
65+
Log updated version numbers to a text file:
66+
--logfile=my_log_filename.txt
67+
6568
These "files" flags can take a list of paths (files and directories).
6669
If directories are provided, they are scanned for known file types.
6770
"""
@@ -321,6 +324,7 @@ def modify_pod_file(pod_file, pod_version_map, dryrun=True):
321324
dryrun (bool, optional): Just print the substitutions.
322325
Do not write to file. Defaults to True.
323326
"""
327+
global logfile_lines
324328
to_update = False
325329
existing_lines = []
326330
with open(pod_file, "r") as podfile:
@@ -345,6 +349,7 @@ def modify_pod_file(pod_file, pod_version_map, dryrun=True):
345349
substituted_pairs.append((line, substituted_line))
346350
existing_lines[idx] = substituted_line
347351
to_update = True
352+
logfile_lines.add('iOS: %s → %s' % (pod_name, latest_version))
348353

349354
if to_update:
350355
print('Updating contents of {0}'.format(pod_file))
@@ -491,6 +496,7 @@ def modify_dependency_file(dependency_filepath, version_map, dryrun=True):
491496
dryrun (bool, optional): Just print the substitutions.
492497
Do not write to file. Defaults to True.
493498
"""
499+
global logfile_lines
494500
logging.debug('Reading dependency file: {0}'.format(dependency_filepath))
495501
lines = None
496502
with open(dependency_filepath, "r") as dependency_file:
@@ -523,6 +529,9 @@ def replace_dependency(m):
523529
if substituted_line != line:
524530
substituted_pairs.append((line, substituted_line))
525531
to_update = True
532+
log_match = re.search(RE_GENERIC_DEPENDENCY_MODULE, line)
533+
log_pkg = log_match.group('pkg').replace('-', '_').replace(':', '.')
534+
logfile_lines.add('Android: %s → %s' % (log_pkg, version_map[log_pkg]))
526535

527536
if to_update:
528537
print('Updating contents of {0}'.format(dependency_filepath))
@@ -705,6 +714,7 @@ def parse_cmdline_args():
705714
default=('release_build_files/readme.md',),
706715
help= 'List of release readme markdown files or directories'
707716
'containing them.')
717+
parser.add_argument('--logfile', help='Log to text file')
708718

709719
args = parser.parse_args()
710720

@@ -725,6 +735,7 @@ def parse_cmdline_args():
725735
logging.getLogger(__name__)
726736
return args
727737

738+
logfile_lines = set()
728739

729740
def main():
730741
args = parse_cmdline_args()
@@ -761,5 +772,9 @@ def main():
761772
for gradle_file in gradle_files:
762773
modify_gradle_file(gradle_file, latest_android_versions_map, args.dryrun)
763774

775+
if args.logfile:
776+
with open(args.logfile, 'w') as logfile_file:
777+
logfile_file.write("\n".join(sorted(list(logfile_lines))) + "\n")
778+
764779
if __name__ == '__main__':
765780
main()

0 commit comments

Comments
 (0)