Skip to content
This repository was archived by the owner on Jun 21, 2024. It is now read-only.

Commit a8a668c

Browse files
committed
update_submodule_versions: add new action
This action helps automate updates to repository submodules. When the action is run, it will: 1. Read information about the submodules from .gitmodules file 2. For each submodule: 1. Check if there are new release tags in upstream repository 2. If yes, create a branch where the update will be performed 3. Update the submodule commit 4. Optionally, update idf_component.yml file with the new version 5. Push the branch to Github repository 6. Open a pull request
1 parent 8e0c4e4 commit a8a668c

File tree

7 files changed

+749
-0
lines changed

7 files changed

+749
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM python:3.10-bullseye
2+
3+
ENV LANG C.UTF-8
4+
ENV LC_ALL C.UTF-8
5+
6+
COPY requirements.txt /tmp/
7+
8+
RUN apt-get update && \
9+
apt-get upgrade -y && \
10+
apt-get install -y git && \
11+
pip3 install --upgrade pip && \
12+
pip3 install -r /tmp/requirements.txt
13+
14+
COPY entrypoint.sh /
15+
COPY update_submodule_versions.py /
16+
17+
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Update repository submodules action
2+
3+
This action helps automate updates to submodules of a repository. It is similar to Dependabot's submodule update functionality, with a few extra features:
4+
5+
1. Configuration of this action, specific to each submodule, is stored along with the rest of submodule information in `.gitmodules` file.
6+
2. The action updates the submodule to the latest tag matching a certain pattern on a given branch.
7+
3. The action can optionally update idf_component.yml file to the version matching the upstream version.
8+
9+
## Configuration
10+
11+
This action reads configuration from custom options in `.gitmodules` file. Here is an example:
12+
```
13+
[submodule "fmt/fmt"]
14+
path = fmt/fmt
15+
url = https://github.com/fmtlib/fmt.git
16+
autoupdate = true
17+
autoupdate-branch = master
18+
autoupdate-tag-glob = [0-9]*.[0-9]*.[0-9]*
19+
autoupdate-include-lightweight = true
20+
autoupdate-manifest = fmt/idf_component.yml
21+
autoupdate-ver-regex = ([0-9]+).([0-9]+).([0-9]+)
22+
```
23+
24+
25+
| Option | Possible values | Default | Explanation |
26+
|--------------------------------|---------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------|
27+
| autoupdate | `true`, `false` | `false` | Whether to update this submodule or not |
28+
| autoupdate-branch | string | | Name of the submodule branch where to look for the new tags. Required if autoupdate=true. |
29+
| autoupdate-tag-glob | Git glob expression | | Glob pattern (as used by 'git describe --match') to use when looking for tags. Required if autoupdate=true. |
30+
| autoupdate-include-lightweight | `true`, `false` | `false` | Whether to include lightweight (not annotated) tags. |
31+
| autoupdate-manifest | path relative to Git repository | | If specified, sets the name of the idf_component.yml file where the version should be updated. |
32+
| autoupdate-ver-regex | regular expression | | Regular expression to extract major, minor, patch version numbers from the Git tag. Required if autoupdate-manifest is set. |
33+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: "Update submodules"
2+
description: "Make PRs to update submodules to new release tags"
3+
inputs:
4+
repo-token:
5+
description: "Github API token (for opening PRs)"
6+
required: true
7+
git-author-name:
8+
description: "Commit author name"
9+
required: true
10+
git-author-email:
11+
description: "Commit author email"
12+
required: true
13+
runs:
14+
using: "docker"
15+
image: "Dockerfile"
16+
env:
17+
GITHUB_TOKEN: ${{ inputs.repo-token }}
18+
GIT_AUTHOR_NAME: ${{ inputs.git-author-name }}
19+
GIT_AUTHOR_EMAIL: ${{ inputs.git-author-email }}
20+
GIT_COMMITTER_NAME: ${{ inputs.git-author-name }}
21+
GIT_COMMITTER_EMAIL: ${{ inputs.git-author-email }}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
git config --global --add safe.directory "*"
6+
7+
/usr/local/bin/python3 /update_submodule_versions.py \
8+
--repo ${GITHUB_WORKSPACE} \
9+
--open-github-pr-in ${GITHUB_REPOSITORY} \
10+
11+
12+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
GitPython==3.1.29
2+
ruamel.yaml==0.17.21
3+
PyGithub==1.58.1
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: Apache-2.0
3+
import tempfile
4+
import textwrap
5+
import unittest
6+
7+
from git import Repo, Commit
8+
9+
from update_submodule_versions import *
10+
11+
12+
class UpdateSubmoduleVersionsTest(unittest.TestCase):
13+
def setUp(self) -> None:
14+
# create the repo for a dependency
15+
self.dependency_dir = Path(tempfile.mkdtemp())
16+
self.dependency_repo = Repo.init(self.dependency_dir)
17+
18+
# add a file and make the first commit
19+
dependency_readme_file = self.dependency_dir / "README.md"
20+
dependency_readme_file.write_text("This is a dependency\n")
21+
self.dependency_repo.index.add([dependency_readme_file.name])
22+
dep_commit = self.dependency_repo.index.commit(
23+
"initial commit of the dependency"
24+
)
25+
self.dependency_repo.create_head("main", commit=dep_commit.hexsha)
26+
27+
# create the "project" repo where the submodule will be added
28+
self.project_dir = Path(tempfile.mkdtemp())
29+
self.project_repo = Repo.init(self.project_dir.absolute())
30+
31+
# add the dependency as a submodule and commit it
32+
self.submodule = self.project_repo.create_submodule(
33+
"dependency", "dependency", url=self.dependency_dir, branch="main"
34+
)
35+
self.project_repo.index.commit("added a dependency as a submodule")
36+
37+
self.addCleanup(self.dependency_dir)
38+
self.addCleanup(self.project_dir)
39+
40+
def create_commit(self, repo: Repo, filename: str, commit_msg: str) -> Commit:
41+
"""Make a commit in the given repo, creating an empty file"""
42+
file_path = Path(repo.working_tree_dir) / filename
43+
file_path.touch()
44+
repo.index.add([filename])
45+
return repo.index.commit(message=commit_msg)
46+
47+
def tag_dependency(self, tag_name: str) -> Commit:
48+
"""Make a commit in the dependency and tag it with the given name"""
49+
dep_commit = self.create_commit(
50+
self.dependency_repo, f"release_{tag_name}.md", f"Release {tag_name}"
51+
)
52+
self.dependency_repo.create_tag(
53+
tag_name, dep_commit.hexsha, message=f"Release {tag_name}"
54+
)
55+
return dep_commit
56+
57+
def update_dependency_submodule_to(self, commit: Commit, commit_msg: str):
58+
submodule = self.project_repo.submodule("dependency")
59+
submodule.binsha = commit.binsha
60+
submodule.update()
61+
self.project_repo.index.add([submodule])
62+
self.project_repo.index.commit(commit_msg)
63+
64+
def test_can_update_manually(self):
65+
"""This is just a test to check that the setUp and above functions work okay"""
66+
self.create_commit(self.dependency_repo, "1.txt", "Added 1.txt")
67+
submodule_commit = self.tag_dependency("v1.0")
68+
self.update_dependency_submodule_to(
69+
submodule_commit, "update submodule to v1.0"
70+
)
71+
self.assertTrue((self.project_dir / "dependency" / "1.txt").exists())
72+
self.assertEqual(
73+
"v1.0",
74+
self.project_repo.git.submodule("--quiet foreach git describe".split()),
75+
)
76+
77+
def test_find_latest_remote_tag(self):
78+
"""Check that find_latest_remote_tag function finds the tagged commit"""
79+
80+
# Create a tag, check that it is found on the right commit
81+
first_commit = self.create_commit(self.dependency_repo, "1.txt", "Added 1.txt")
82+
self.create_commit(self.dependency_repo, "2.txt", "Added 2.txt")
83+
v2_release_commit = self.tag_dependency("v2.0")
84+
self.create_commit(self.dependency_repo, "3.txt", "Added 3.txt")
85+
tag_found = find_latest_remote_tag(self.submodule, "main", "v*")
86+
self.assertEqual(v2_release_commit.hexsha, tag_found.commit.hexsha)
87+
88+
# Create a tag on an older commit, check that the most recent tag
89+
# (in branch sequential order) is found, not the most recent one
90+
# in chronological order
91+
self.dependency_repo.create_tag(
92+
"v1.0", first_commit.hexsha, message=f"Release v1.0"
93+
)
94+
tag_found = find_latest_remote_tag(self.submodule, "main", "v*")
95+
self.assertEqual(v2_release_commit.hexsha, tag_found.commit.hexsha)
96+
97+
# Check that the wildcard is respected, by looking specifically for v1* tags
98+
tag_found = find_latest_remote_tag(self.submodule, "main", "v1*")
99+
self.assertEqual(first_commit.hexsha, tag_found.commit.hexsha)
100+
101+
# Create a newer tag on another branch, check that it is not found
102+
self.dependency_repo.create_head(
103+
"release/v2.0", commit=v2_release_commit.hexsha
104+
)
105+
self.dependency_repo.git.checkout("release/v2.0")
106+
self.create_commit(self.dependency_repo, "2_1.txt", "Added 2_1.txt")
107+
v2_1_release_commit = self.tag_dependency("v2.1")
108+
109+
tag_found = find_latest_remote_tag(self.submodule, "main", "v*")
110+
self.assertEqual(v2_release_commit.hexsha, tag_found.commit.hexsha)
111+
112+
# But the newest tag should be found if we specify the release branch
113+
tag_found = find_latest_remote_tag(self.submodule, "release/v2.0", "v*")
114+
self.assertEqual(v2_1_release_commit.hexsha, tag_found.commit.hexsha)
115+
116+
117+
class VersionFromTagTest(unittest.TestCase):
118+
def test_version_from_tag(self):
119+
self.assertEqual(
120+
IdfComponentVersion(1, 2, 3),
121+
get_version_from_tag("v1.2.3", DEFAULT_TAG_VERSION_REGEX),
122+
)
123+
self.assertEqual(
124+
IdfComponentVersion(1, 2, 3),
125+
get_version_from_tag("1.2.3", DEFAULT_TAG_VERSION_REGEX),
126+
)
127+
self.assertEqual(
128+
IdfComponentVersion(1, 2, 0),
129+
get_version_from_tag("1.2", DEFAULT_TAG_VERSION_REGEX),
130+
)
131+
self.assertEqual(
132+
IdfComponentVersion(2, 4, 9),
133+
get_version_from_tag("R_2_4_9", r"R_(\d+)_(\d+)_(\d+)"),
134+
)
135+
136+
with self.assertRaises(ValueError):
137+
get_version_from_tag("v1.2.3-rc1", DEFAULT_TAG_VERSION_REGEX)
138+
with self.assertRaises(ValueError):
139+
get_version_from_tag("qa-test-v1.2.3", DEFAULT_TAG_VERSION_REGEX)
140+
with self.assertRaises(ValueError):
141+
get_version_from_tag("v1.2.3.4", DEFAULT_TAG_VERSION_REGEX)
142+
with self.assertRaises(ValueError):
143+
get_version_from_tag("v1", DEFAULT_TAG_VERSION_REGEX)
144+
145+
146+
class UpdateIDFComponentYMLVersionTest(unittest.TestCase):
147+
def update_manifest(self, orig_yaml: str, new_ver: IdfComponentVersion):
148+
with tempfile.NamedTemporaryFile("a+") as manifest_file:
149+
manifest_file.write(orig_yaml)
150+
manifest_file.flush()
151+
update_idf_component_yml_version(Path(manifest_file.name), new_ver)
152+
manifest_file.seek(0)
153+
return manifest_file.read()
154+
155+
def test_update_manifest_version(self):
156+
self.assertEqual(
157+
textwrap.dedent(
158+
"""
159+
# this is a comment
160+
version: "2.0.1"
161+
"""
162+
),
163+
self.update_manifest(
164+
textwrap.dedent(
165+
"""
166+
# this is a comment
167+
version: "1.2.0"
168+
"""
169+
),
170+
IdfComponentVersion(2, 0, 1),
171+
),
172+
)
173+
174+
self.assertEqual(
175+
textwrap.dedent(
176+
"""
177+
repository: "https://github.com/espressif/idf-extra-components.git"
178+
version: "2.0.2"
179+
"""
180+
),
181+
self.update_manifest(
182+
textwrap.dedent(
183+
"""
184+
repository: "https://github.com/espressif/idf-extra-components.git"
185+
version: "2.0.1~1"
186+
"""
187+
),
188+
IdfComponentVersion(2, 0, 2),
189+
),
190+
)
191+
192+
self.assertEqual(
193+
textwrap.dedent(
194+
"""
195+
repository: "https://github.com/espressif/idf-extra-components.git"
196+
version: "4.3.1"
197+
"""
198+
),
199+
self.update_manifest(
200+
textwrap.dedent(
201+
"""
202+
repository: "https://github.com/espressif/idf-extra-components.git"
203+
version: "4.3.1~1-rc.1"
204+
"""
205+
),
206+
IdfComponentVersion(4, 3, 1),
207+
),
208+
)
209+
210+
with self.assertRaises(ValueError):
211+
self.update_manifest(
212+
textwrap.dedent(
213+
"""
214+
repository: "https://github.com/espressif/idf-extra-components.git"
215+
# no version tag
216+
"""
217+
),
218+
IdfComponentVersion(1, 0, 0),
219+
)
220+
221+
with self.assertRaises(ValueError):
222+
self.update_manifest(
223+
textwrap.dedent(
224+
"""
225+
version: "0.1.0"
226+
repository: "https://github.com/espressif/idf-extra-components.git"
227+
version: "0.1.1"
228+
"""
229+
),
230+
IdfComponentVersion(1, 0, 0),
231+
)
232+
233+
self.assertEqual(
234+
textwrap.dedent(
235+
"""
236+
# version: "1.0.0"
237+
version: "2.0.1"
238+
"""
239+
),
240+
self.update_manifest(
241+
textwrap.dedent(
242+
"""
243+
# version: "1.0.0"
244+
version: "1.2.0"
245+
"""
246+
),
247+
IdfComponentVersion(2, 0, 1),
248+
),
249+
)
250+
251+
self.assertEqual(
252+
textwrap.dedent(
253+
"""
254+
repository: "https://github.com/espressif/idf-extra-components.git"
255+
version: "2.0.1" # trailing comment
256+
"""
257+
),
258+
self.update_manifest(
259+
textwrap.dedent(
260+
"""
261+
repository: "https://github.com/espressif/idf-extra-components.git"
262+
version: "1.2.0" # trailing comment
263+
"""
264+
),
265+
IdfComponentVersion(2, 0, 1),
266+
),
267+
)
268+
269+
# check that we add a newline in case version is on the last line and
270+
# the line was missing a newline
271+
self.assertEqual(
272+
textwrap.dedent(
273+
"""
274+
repository: "https://github.com/espressif/idf-extra-components.git"
275+
version: "2.0.1" # no newline
276+
"""
277+
),
278+
self.update_manifest(
279+
textwrap.dedent(
280+
"""
281+
repository: "https://github.com/espressif/idf-extra-components.git"
282+
version: "1.2.0" # no newline"""
283+
),
284+
IdfComponentVersion(2, 0, 1),
285+
),
286+
)
287+
288+
289+
if __name__ == "__main__":
290+
unittest.main()

0 commit comments

Comments
 (0)