|
|
1
|
+#!/usr/bin/env python3
|
|
|
2
|
+import argparse
|
|
|
3
|
+import json
|
|
|
4
|
+import logging
|
|
|
5
|
+import os
|
|
|
6
|
+import sys
|
|
|
7
|
+import urllib.request
|
|
|
8
|
+
|
|
|
9
|
+logger = logging.getLogger(__name__)
|
|
|
10
|
+
|
|
|
11
|
+GITLAB_URL = "https://gitlab.torproject.org"
|
|
|
12
|
+# The project ID of the tor-browser-bundle-testsuite repository
|
|
|
13
|
+GITLAB_PROJECT_ID = "409"
|
|
|
14
|
+# We are running the pipeline on the main branch.
|
|
|
15
|
+GITLAB_REF = "main"
|
|
|
16
|
+
|
|
|
17
|
+SUPPORTED_ARCHITECTURES_PER_PLATFORM = {
|
|
|
18
|
+ "linux": ["x86_64"],
|
|
|
19
|
+ "windows": ["x86_64"],
|
|
|
20
|
+ "macos": ["x86_64"],
|
|
|
21
|
+ "android": ["x86_64"],
|
|
|
22
|
+}
|
|
|
23
|
+
|
|
|
24
|
+
|
|
|
25
|
+def setup_logging() -> None:
|
|
|
26
|
+ logging.basicConfig(
|
|
|
27
|
+ level=logging.INFO,
|
|
|
28
|
+ format="%(levelname)s: %(message)s",
|
|
|
29
|
+ # IMPORTANT!
|
|
|
30
|
+ #
|
|
|
31
|
+ # Perl captures this script's stdout and, if it is non-empty, stores it
|
|
|
32
|
+ # as `gitlab_pipeline_url` for the current build step. We need to keep logs
|
|
|
33
|
+ # on stderr and only print to stdout when its the resulting pipeline URL.
|
|
|
34
|
+ stream=sys.stderr,
|
|
|
35
|
+ )
|
|
|
36
|
+
|
|
|
37
|
+
|
|
|
38
|
+def parse_args() -> argparse.Namespace:
|
|
|
39
|
+ parser = argparse.ArgumentParser(
|
|
|
40
|
+ description="Post-build trigger hook for triggering GitLab CI pipelines."
|
|
|
41
|
+ )
|
|
|
42
|
+ parser.add_argument(
|
|
|
43
|
+ "--step-name",
|
|
|
44
|
+ required=True,
|
|
|
45
|
+ help="""
|
|
|
46
|
+ Name of the build step that just finished.
|
|
|
47
|
+ List of possible values can be found in
|
|
|
48
|
+ TBBTestSuite/TestSuite/TorBrowserBuild.pm (set_tests)
|
|
|
49
|
+ """,
|
|
|
50
|
+ )
|
|
|
51
|
+ parser.add_argument(
|
|
|
52
|
+ "--publish-url",
|
|
|
53
|
+ required=True,
|
|
|
54
|
+ help="URL where build artifacts are published for the build artifacts.",
|
|
|
55
|
+ )
|
|
|
56
|
+ parser.add_argument(
|
|
|
57
|
+ "--publish-dir",
|
|
|
58
|
+ required=True,
|
|
|
59
|
+ help="Subdirectory within the publish URL where build artifacts are located.",
|
|
|
60
|
+ )
|
|
|
61
|
+ parser.add_argument(
|
|
|
62
|
+ "--dry-run",
|
|
|
63
|
+ action="store_true",
|
|
|
64
|
+ help="Print the equivalent curl command instead of triggering the pipeline.",
|
|
|
65
|
+ )
|
|
|
66
|
+ return parser.parse_args()
|
|
|
67
|
+
|
|
|
68
|
+
|
|
|
69
|
+def build_inputs(step_name: str, publish_url: str, publish_dir: str) -> dict[str, str] | None:
|
|
|
70
|
+ # Add the architecture as padding, to address the macos case which doesn't
|
|
|
71
|
+ # have architecture in the step name since it is a universal build.
|
|
|
72
|
+ browser, channel, platform, architecture = (step_name.split("-") + ["x86_64"])[:4]
|
|
|
73
|
+ if channel != "nightly":
|
|
|
74
|
+ logger.info("This script only knows how to handle nightly builds. Skipping.")
|
|
|
75
|
+ return None
|
|
|
76
|
+ if (
|
|
|
77
|
+ platform not in SUPPORTED_ARCHITECTURES_PER_PLATFORM
|
|
|
78
|
+ or architecture not in SUPPORTED_ARCHITECTURES_PER_PLATFORM[platform]
|
|
|
79
|
+ ):
|
|
|
80
|
+ logger.info(
|
|
|
81
|
+ f"Tests for {platform}-{architecture} are not supported yet. Skipping."
|
|
|
82
|
+ )
|
|
|
83
|
+ return None
|
|
|
84
|
+
|
|
|
85
|
+ hyphenated_browser = "tor-browser" if browser == "torbrowser" else "mullvad-browser"
|
|
|
86
|
+ date = publish_url.rstrip("/").split("/")[-1].split(".", 1)[1]
|
|
|
87
|
+ match platform:
|
|
|
88
|
+ case "linux":
|
|
|
89
|
+ installer = f"{hyphenated_browser}-{platform}-{architecture}-tbb-{channel}.{date}.tar.xz"
|
|
|
90
|
+ case "windows":
|
|
|
91
|
+ installer = f"{hyphenated_browser}-{platform}-{architecture}-portable-tbb-{channel}.{date}.exe"
|
|
|
92
|
+ case "macos":
|
|
|
93
|
+ installer = f"{hyphenated_browser}-{platform}-{architecture}-tbb-{channel}.{date}.dmg"
|
|
|
94
|
+ case "android":
|
|
|
95
|
+ installer = f"{hyphenated_browser}-qa-{platform}-{architecture}-tbb-{channel}.{date}.apk"
|
|
|
96
|
+ case _:
|
|
|
97
|
+ raise ValueError(f"Unsupported platform: {platform!r}. How did we get here?")
|
|
|
98
|
+
|
|
|
99
|
+ artifacts_url = f"{publish_url}/{publish_dir}/artifacts/{platform}-{architecture}"
|
|
|
100
|
+ input_prefix = f"{'debian' if platform == 'linux' else platform}_{architecture}"
|
|
|
101
|
+ inputs = {
|
|
|
102
|
+ "mozharness_url": f"{artifacts_url}/mozharness.zip",
|
|
|
103
|
+ f"{input_prefix}_installer_url": f"{publish_url}/{publish_dir}/{installer}",
|
|
|
104
|
+ f"{input_prefix}_artifacts_url": artifacts_url,
|
|
|
105
|
+ }
|
|
|
106
|
+
|
|
|
107
|
+ if platform == "android":
|
|
|
108
|
+ inputs[f"{input_prefix}_package_name"] = f"org.torproject.{browser}_{channel}"
|
|
|
109
|
+
|
|
|
110
|
+ return inputs
|
|
|
111
|
+
|
|
|
112
|
+
|
|
|
113
|
+def dry_run(inputs: dict[str, str], trigger_token: str) -> None:
|
|
|
114
|
+ url = f"{GITLAB_URL}/api/v4/projects/{GITLAB_PROJECT_ID}/trigger/pipeline?token={trigger_token}&ref={GITLAB_REF}"
|
|
|
115
|
+ payload = json.dumps({"inputs": inputs}, indent=2)
|
|
|
116
|
+ print(
|
|
|
117
|
+ f"curl --request POST --header 'Content-Type: application/json' --data '{payload}' '{url}'"
|
|
|
118
|
+ )
|
|
|
119
|
+
|
|
|
120
|
+
|
|
|
121
|
+def trigger_pipeline(trigger_token: str, inputs: dict[str, str]) -> str:
|
|
|
122
|
+ url = f"{GITLAB_URL}/api/v4/projects/{GITLAB_PROJECT_ID}/trigger/pipeline"
|
|
|
123
|
+ payload = json.dumps(
|
|
|
124
|
+ {"token": trigger_token, "ref": GITLAB_REF, "inputs": inputs}
|
|
|
125
|
+ ).encode()
|
|
|
126
|
+ req = urllib.request.Request(
|
|
|
127
|
+ url,
|
|
|
128
|
+ data=payload,
|
|
|
129
|
+ headers={"Content-Type": "application/json"},
|
|
|
130
|
+ method="POST",
|
|
|
131
|
+ )
|
|
|
132
|
+ with urllib.request.urlopen(req) as resp:
|
|
|
133
|
+ data = json.loads(resp.read())
|
|
|
134
|
+
|
|
|
135
|
+ return data["web_url"]
|
|
|
136
|
+
|
|
|
137
|
+
|
|
|
138
|
+def main() -> int:
|
|
|
139
|
+ setup_logging()
|
|
|
140
|
+ args = parse_args()
|
|
|
141
|
+
|
|
|
142
|
+ token_file = os.path.join(
|
|
|
143
|
+ os.path.dirname(os.path.abspath(__file__)), ".gitlab_trigger_token"
|
|
|
144
|
+ )
|
|
|
145
|
+ with open(token_file) as f:
|
|
|
146
|
+ trigger_token = f.read().strip()
|
|
|
147
|
+
|
|
|
148
|
+ inputs = build_inputs(args.step_name, args.publish_url, args.publish_dir)
|
|
|
149
|
+ if inputs is None:
|
|
|
150
|
+ logger.info(f"No CI inputs for step {args.step_name!r}, skipping.")
|
|
|
151
|
+ return 0
|
|
|
152
|
+
|
|
|
153
|
+ if args.dry_run:
|
|
|
154
|
+ dry_run(inputs, trigger_token)
|
|
|
155
|
+ return 0
|
|
|
156
|
+
|
|
|
157
|
+ logger.info(f"Triggering pipeline for step={args.step_name!r}")
|
|
|
158
|
+ pipeline_url = trigger_pipeline(trigger_token, inputs)
|
|
|
159
|
+ logger.info(f"Pipeline triggered: {pipeline_url}")
|
|
|
160
|
+ print(pipeline_url)
|
|
|
161
|
+ return 0
|
|
|
162
|
+
|
|
|
163
|
+
|
|
|
164
|
+if __name__ == "__main__":
|
|
|
165
|
+ sys.exit(main()) |