Skip to content

Commit a3846ae

Browse files
committed
[GO] support go workspaces and project with multiple go project defined
1 parent 9d163ce commit a3846ae

3 files changed

Lines changed: 192 additions & 12 deletions

File tree

continuous_delivery_scripts/plugins/golang.py

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#
55
"""Plugin for Golang projects."""
66

7+
import json
78
import logging
89
import os
910
import shutil
@@ -143,26 +144,86 @@ def _call_goreleaser_check(version: str) -> None:
143144
check_call(_generate_goreleaser_check_command_list(), cwd=ROOT_DIR, env=env)
144145

145146

146-
def _determine_go_module_tag(version: str) -> Optional[str]:
147-
"""Determines go module for tagging.
148-
149-
See https://golang.org/ref/mod#vcs-version.
150-
and https://github.com/golang/go/wiki/Modules#should-i-have-multiple-modules-in-a-single-repository.
151-
"""
152-
module = ""
147+
def _determine_go_module_tag_for_directory(module_directory: Path, version: str) -> Optional[str]:
153148
try:
154-
module = str(SRC_DIR.relative_to(ROOT_DIR))
149+
module = str(module_directory.relative_to(ROOT_DIR))
155150
except ValueError:
156151
try:
157-
module = str(ROOT_DIR.relative_to(SRC_DIR))
152+
module = str(ROOT_DIR.relative_to(module_directory))
158153
except ValueError as exception:
159154
logger.warning(exception)
155+
return None
160156
if module == "." or len(module) == 0:
161157
return None
162-
module = module.rstrip("/")
158+
module = module.replace("\\", "/").rstrip("/")
163159
return f"{module}/{version}"
164160

165161

162+
def _find_go_work_files() -> List[Path]:
163+
go_work_files: List[Path] = []
164+
for go_work_file in [SRC_DIR.joinpath("go.work"), ROOT_DIR.joinpath("go.work")]:
165+
if go_work_file.exists() and go_work_file not in go_work_files:
166+
go_work_files.append(go_work_file)
167+
return go_work_files
168+
169+
170+
def _determine_go_work_module_directories_from_json(go_work_file: Path) -> List[Path]:
171+
"""Determine module directories from `go work edit -json` output.
172+
173+
`go.work` lists all workspace modules that should be released together.
174+
See https://go.dev/ref/mod#workspaces and https://pkg.go.dev/cmd/go#hdr-Edit_workspace_file.
175+
"""
176+
go_work_root = go_work_file.parent
177+
go_work = json.loads(check_output(["go", "work", "edit", "-json"], cwd=go_work_root, encoding="utf8"))
178+
module_directories: List[Path] = []
179+
for use_definition in go_work.get("Use", []):
180+
disk_path = use_definition.get("DiskPath") or use_definition.get("Path")
181+
if not disk_path:
182+
continue
183+
module_directory = Path(str(disk_path))
184+
module_directories.append(
185+
module_directory if module_directory.is_absolute() else go_work_root.joinpath(module_directory)
186+
)
187+
return module_directories
188+
189+
190+
def _determine_go_work_module_directories() -> List[Path]:
191+
"""Determine module directories declared in `go.work`.
192+
193+
`go.work` lists all workspace modules that should be released together.
194+
See https://go.dev/ref/mod#workspaces.
195+
"""
196+
module_directories: List[Path] = []
197+
for go_work_file in _find_go_work_files():
198+
module_directories.extend(_determine_go_work_module_directories_from_json(go_work_file))
199+
return list(dict.fromkeys(module_directories))
200+
201+
202+
def _determine_go_subproject_directories() -> List[Path]:
203+
if not SRC_DIR.exists():
204+
return []
205+
return [go_mod_file.parent for go_mod_file in SRC_DIR.rglob("go.mod")]
206+
207+
208+
def _determine_go_module_tag(version: str) -> List[str]:
209+
"""Determine all go module tags for release.
210+
211+
See https://golang.org/ref/mod#vcs-version, https://go.dev/ref/mod#workspaces,
212+
and https://github.com/golang/go/wiki/Modules/a549b3e4b7ad6be6e7d11c37ef247bb2279c8146#faqs--multi-module-repositories.
213+
"""
214+
module_directories = [SRC_DIR]
215+
go_work_module_directories = _determine_go_work_module_directories()
216+
if go_work_module_directories:
217+
module_directories.extend(go_work_module_directories)
218+
else:
219+
module_directories.extend(_determine_go_subproject_directories())
220+
221+
tags = [
222+
_determine_go_module_tag_for_directory(module_directory, version) for module_directory in module_directories
223+
]
224+
return list(dict.fromkeys([tag for tag in tags if tag]))
225+
226+
166227
class Go(BaseLanguage):
167228
"""Specific actions for a Golang project."""
168229

@@ -224,8 +285,7 @@ def should_clean_before_packaging(self) -> bool:
224285
def tag_release(self, git: GitWrapper, version: str, shortcuts: Dict[str, bool]) -> None:
225286
"""Tags release commit."""
226287
super().tag_release(git, version, shortcuts)
227-
go_tag = _determine_go_module_tag(self.get_version_tag(version))
228-
if go_tag:
288+
for go_tag in _determine_go_module_tag(self.get_version_tag(version)):
229289
git.create_tag(go_tag, message=f"Golang module release: {go_tag}")
230290

231291
def _call_goreleaser_release(self, version: str) -> None:

news/20260604124611.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `[GO]` support go workspaces and project with multiple go project defined

tests/plugin/test_golang.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#
2+
# Copyright (C) 2020-2026 Arm Limited or its affiliates and Contributors. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
import shutil
6+
from pathlib import Path
7+
from unittest import TestCase, mock
8+
from unittest import skipUnless
9+
10+
from continuous_delivery_scripts.plugins import golang
11+
from continuous_delivery_scripts.utils.filesystem_helpers import TemporaryDirectory
12+
13+
GO_AVAILABLE = shutil.which("go") is not None
14+
15+
16+
class TestGoModuleTags(TestCase):
17+
def test_determine_go_module_tag_keeps_current_module(self):
18+
with TemporaryDirectory() as temp_dir:
19+
root = Path(temp_dir)
20+
source_dir = root.joinpath("src")
21+
source_dir.mkdir()
22+
23+
with mock.patch.object(golang, "ROOT_DIR", root), mock.patch.object(golang, "SRC_DIR", source_dir):
24+
tags = golang._determine_go_module_tag("v1.2.3")
25+
26+
self.assertEqual(tags, ["src/v1.2.3"])
27+
28+
def test_determine_go_module_tag_reads_go_work_modules(self):
29+
with TemporaryDirectory() as temp_dir:
30+
root = Path(temp_dir)
31+
source_dir = root.joinpath("src")
32+
source_dir.mkdir()
33+
root.joinpath("go.work").touch()
34+
35+
with (
36+
mock.patch.object(golang, "ROOT_DIR", root),
37+
mock.patch.object(golang, "SRC_DIR", source_dir),
38+
mock.patch.object(
39+
golang,
40+
"check_output",
41+
return_value='{"Use": [{"DiskPath": "./app1"}, {"DiskPath": "./nested/app2"}]}',
42+
),
43+
):
44+
tags = golang._determine_go_module_tag("v1.2.3")
45+
46+
self.assertEqual(tags, ["src/v1.2.3", "app1/v1.2.3", "nested/app2/v1.2.3"])
47+
48+
def test_determine_go_module_tag_reads_go_work_from_source_dir(self):
49+
with TemporaryDirectory() as temp_dir:
50+
root = Path(temp_dir)
51+
source_dir = root.joinpath("src")
52+
source_dir.mkdir()
53+
source_dir.joinpath("go.work").touch()
54+
55+
with (
56+
mock.patch.object(golang, "ROOT_DIR", root),
57+
mock.patch.object(golang, "SRC_DIR", source_dir),
58+
mock.patch.object(
59+
golang,
60+
"check_output",
61+
return_value='{"Use": [{"DiskPath": "./app1"}, {"DiskPath": "./nested/app2"}]}',
62+
),
63+
):
64+
tags = golang._determine_go_module_tag("v1.2.3")
65+
66+
self.assertEqual(tags, ["src/v1.2.3", "src/app1/v1.2.3", "src/nested/app2/v1.2.3"])
67+
68+
def test_determine_go_module_tag_reads_multiple_go_work_files(self):
69+
with TemporaryDirectory() as temp_dir:
70+
root = Path(temp_dir)
71+
source_dir = root.joinpath("src")
72+
source_dir.mkdir()
73+
root.joinpath("go.work").touch()
74+
source_dir.joinpath("go.work").touch()
75+
76+
def check_output_side_effect(command, cwd=None, encoding=None):
77+
if Path(str(cwd)) == source_dir:
78+
return '{"Use": [{"DiskPath": "./app1"}]}'
79+
if Path(str(cwd)) == root:
80+
return '{"Use": [{"DiskPath": "./shared"}, {"DiskPath": "./root-app"}]}'
81+
raise AssertionError(f"Unexpected cwd: {cwd}")
82+
83+
with (
84+
mock.patch.object(golang, "ROOT_DIR", root),
85+
mock.patch.object(golang, "SRC_DIR", source_dir),
86+
mock.patch.object(golang, "check_output", side_effect=check_output_side_effect),
87+
):
88+
tags = golang._determine_go_module_tag("v1.2.3")
89+
90+
self.assertEqual(tags, ["src/v1.2.3", "src/app1/v1.2.3", "shared/v1.2.3", "root-app/v1.2.3"])
91+
92+
def test_determine_go_module_tag_reads_nested_go_mod_projects(self):
93+
with TemporaryDirectory() as temp_dir:
94+
root = Path(temp_dir)
95+
source_dir = root.joinpath("src")
96+
source_dir.mkdir()
97+
source_dir.joinpath("go.mod").write_text("module example.com/src\n", encoding="utf8")
98+
source_dir.joinpath("service-a").mkdir()
99+
source_dir.joinpath("service-a", "go.mod").write_text("module example.com/service-a\n", encoding="utf8")
100+
source_dir.joinpath("service-b").mkdir()
101+
source_dir.joinpath("service-b", "go.mod").write_text("module example.com/service-b\n", encoding="utf8")
102+
103+
with mock.patch.object(golang, "ROOT_DIR", root), mock.patch.object(golang, "SRC_DIR", source_dir):
104+
tags = golang._determine_go_module_tag("v1.2.3")
105+
106+
self.assertEqual(tags, ["src/v1.2.3", "src/service-a/v1.2.3", "src/service-b/v1.2.3"])
107+
108+
109+
@skipUnless(GO_AVAILABLE, "go command is required for this integration test")
110+
class TestGoWorkIntegration(TestCase):
111+
def test_determine_go_work_module_directories_from_json_with_go(self):
112+
with TemporaryDirectory() as temp_dir:
113+
root = Path(temp_dir)
114+
root.joinpath("go.work").write_text("go 1.22\n\nuse ./app1\n", encoding="utf8")
115+
root.joinpath("app1").mkdir()
116+
117+
directories = golang._determine_go_work_module_directories_from_json(root.joinpath("go.work"))
118+
119+
self.assertEqual(directories, [root.joinpath("app1")])

0 commit comments

Comments
 (0)