Skip to content

Commit e75a5a9

Browse files
committed
feat(shipitscript): add support for creating new nightly releases
1 parent d3f773b commit e75a5a9

8 files changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"title": "Taskcluster ShipIt create new nightly release task minimal schema",
3+
"type": "object",
4+
"properties": {
5+
"dependencies": {
6+
"type": "array",
7+
"minItems": 1,
8+
"uniqueItems": true,
9+
"items": {
10+
"type": "string"
11+
}
12+
},
13+
"scopes": {
14+
"type": "array",
15+
"minItems": 2,
16+
"uniqueItems": true,
17+
"items": {
18+
"type": "string"
19+
}
20+
},
21+
"payload": {
22+
"type": "object",
23+
"properties": {
24+
"product": {
25+
"type": "string"
26+
},
27+
"channel": {
28+
"type": "string"
29+
},
30+
"buildid": {
31+
"type": "string"
32+
},
33+
"version": {
34+
"type": "string"
35+
},
36+
"locales": {
37+
"type": "array",
38+
"minItems": 1,
39+
"uniqueItems": true,
40+
"items": {
41+
"type": "string"
42+
}
43+
}
44+
},
45+
"required": ["product", "channel", "buildid", "version", "locales"],
46+
"additionalProperties": false
47+
}
48+
},
49+
"required": ["dependencies", "scopes", "payload"]
50+
}
51+

shipitscript/src/shipitscript/script.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,29 @@ def update_product_channel_version_action(context):
114114
ship_actions.update_product_channel_version(shipit_config, product, channel, version)
115115

116116

117+
def create_new_nightly_release_action(context):
118+
payload = context.task["payload"]
119+
shipit_config = context.ship_it_instance_config
120+
product = payload["product"]
121+
channel = payload["channel"]
122+
version = payload["version"]
123+
buildid = payload["buildid"]
124+
locales = payload["locales"]
125+
nightly = ship_actions.get_nightly_metadata(shipit_config, product, channel, buildid)
126+
if nightly and (nightly["version"] != version or nightly["locales"] != locales):
127+
log.error("Nightly already exists, but version and/or locales don't match!")
128+
sys.exit(1)
129+
else:
130+
ship_actions.create_new_nightly_release(shipit_config, product, channel, buildid, version, locales)
131+
132+
117133
# ACTION_MAP {{{1
118134
ACTION_MAP = {
119135
"mark-as-shipped": mark_as_shipped_action,
120136
"mark-as-merged": mark_as_merged_action,
121137
"create-new-release": create_new_release_action,
122138
"update-product-channel-version": update_product_channel_version_action,
139+
"create-new-nightly-release": create_new_nightly_release_action,
123140
}
124141

125142

@@ -134,6 +151,7 @@ def get_default_config():
134151
"mark_as_shipped_schema_file": os.path.join(data_dir, "mark_as_shipped_task_schema.json"),
135152
"mark_as_merged_schema_file": os.path.join(data_dir, "mark_as_merged_task_schema.json"),
136153
"create_new_release_schema_file": os.path.join(data_dir, "create_new_release_task_schema.json"),
154+
"create_new_nightly_release_schema_file": os.path.join(data_dir, "create_new_nightly_release_task_schema.json"),
137155
}
138156

139157

shipitscript/src/shipitscript/ship_actions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,16 @@ def update_product_channel_version(shipit_config, product, channel, version):
123123
log.info(f"Updating the current version of {product} {channel} to {version}...")
124124
response = release_api.update_product_channel_version(product, channel, version, headers=headers)
125125
log.info(response["message"])
126+
127+
128+
def get_nightly_metadata(shipit_config, product, channel, buildid):
129+
release_api, headers = get_shipit_api_instance(shipit_config)
130+
log.info(f"Getting metadata for nightly: {product}, {channel}, {buildid}")
131+
return release_api.get_nightly_metadata(product, channel, buildid, headers=headers)
132+
133+
134+
def create_new_nightly_release(shipit_config, product, channel, buildid, version, locales):
135+
release_api, headers = get_shipit_api_instance(shipit_config)
136+
log.info(f"Creating new nightly release: {product}, {channel}, {buildid}")
137+
release_api.create_new_nightly_release(product, channel, buildid, version, locales, headers=headers)
138+
log.info("New nightly release successfully created")

shipitscript/src/shipitscript/shipitapi.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,32 @@ def complete_merge_automation(self, automation_id, headers={}):
189189
"""
190190
resp = self._request(api_endpoint=f"/merge-automation/{automation_id}", method="PATCH", data="", headers=headers).content
191191
return resp
192+
193+
def get_nightly_metadata(self, product, channel, buildid, headers=None):
194+
headers = headers if headers else {}
195+
params = {"product": product, "channel": channel, "buildid": buildid}
196+
197+
try:
198+
response = self._request(api_endpoint=f"/nightly-release?{urllib.parse.urlencode(params)}", method="GET", headers=headers)
199+
return response.json()
200+
except Exception:
201+
log.error(f"Caught error while getting metadata for {product}, {channel}, {buildid}!", exc_info=True)
202+
raise
203+
204+
def create_new_nightly_release(self, product, channel, buildid, version, locales, headers=None):
205+
headers = headers if headers else {}
206+
data = json.dumps(
207+
{
208+
"product": product,
209+
"channel": channel,
210+
"buildid": buildid,
211+
"version": version,
212+
"locales": locales,
213+
}
214+
)
215+
try:
216+
response = self._request(api_endpoint="/nightly-release", method="POST", data=data, headers=headers)
217+
return response.json()
218+
except Exception:
219+
log.error(f"Caught error while creating new nightly release: {product}, {channel}, {buildid}", exc_info=True)
220+
raise

shipitscript/src/shipitscript/task.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"mark-as-merged": "mark_as_merged_schema_file",
1313
"create-new-release": "create_new_release_schema_file",
1414
"update-product-channel-version": "update_product_channel_version_schema_file",
15+
"create-new-nightly-release": "create_new_nightly_release_schema_file",
1516
}
1617

1718

shipitscript/tests/test_script.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,49 @@ async def test_async_main(context, monkeypatch, task, raises):
9494
await script.async_main(context)
9595

9696

97+
@pytest.mark.parametrize(
98+
"existing_nightly,expect_create,expect_exit",
99+
(
100+
# no existing nightly: create one
101+
(None, True, False),
102+
# existing nightly matches version + locales: still calls create
103+
({"version": "150.0a1", "locales": ["en-US", "de"]}, True, False),
104+
# existing nightly mismatches version: sys.exit(1)
105+
({"version": "149.0a1", "locales": ["en-US", "de"]}, False, True),
106+
# existing nightly mismatches locales: sys.exit(1)
107+
({"version": "150.0a1", "locales": ["en-US"]}, False, True),
108+
),
109+
)
110+
def test_create_new_nightly_release_action(context, monkeypatch, existing_nightly, expect_create, expect_exit):
111+
context.ship_it_instance_config = context.config["shipit_instance"]
112+
context.task["payload"] = {
113+
"product": "firefox",
114+
"channel": "nightly",
115+
"buildid": "20260525000000",
116+
"version": "150.0a1",
117+
"locales": ["en-US", "de"],
118+
}
119+
120+
get_nightly_metadata_mock = MagicMock(return_value=existing_nightly)
121+
create_new_nightly_release_mock = MagicMock()
122+
monkeypatch.setattr(ship_actions, "get_nightly_metadata", get_nightly_metadata_mock)
123+
monkeypatch.setattr(ship_actions, "create_new_nightly_release", create_new_nightly_release_mock)
124+
125+
if expect_exit:
126+
with pytest.raises(SystemExit):
127+
script.create_new_nightly_release_action(context)
128+
else:
129+
script.create_new_nightly_release_action(context)
130+
131+
get_nightly_metadata_mock.assert_called_with(context.ship_it_instance_config, "firefox", "nightly", "20260525000000")
132+
if expect_create:
133+
create_new_nightly_release_mock.assert_called_with(
134+
context.ship_it_instance_config, "firefox", "nightly", "20260525000000", "150.0a1", ["en-US", "de"]
135+
)
136+
else:
137+
create_new_nightly_release_mock.assert_not_called()
138+
139+
97140
def test_get_default_config():
98141
parent_dir = os.path.dirname(os.getcwd())
99142
data_dir = os.path.join(os.path.dirname(shipitscript.__file__), "data")
@@ -103,6 +146,7 @@ def test_get_default_config():
103146
"mark_as_shipped_schema_file": os.path.join(data_dir, "mark_as_shipped_task_schema.json"),
104147
"mark_as_merged_schema_file": os.path.join(data_dir, "mark_as_merged_task_schema.json"),
105148
"create_new_release_schema_file": os.path.join(data_dir, "create_new_release_task_schema.json"),
149+
"create_new_nightly_release_schema_file": os.path.join(data_dir, "create_new_nightly_release_task_schema.json"),
106150
}
107151

108152

shipitscript/tests/test_ship_actions.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,53 @@ def test_mark_as_merged(monkeypatch, timeout, expected_timeout):
4848
taskcluster_client_id="some-id", taskcluster_access_token="some-token", api_root="http://some.ship-it.tld/api/root", timeout=expected_timeout
4949
)
5050
release_instance_mock.complete_merge_automation.assert_called_with(123, headers={"X-Forwarded-Proto": "https", "X-Forwarded-Port": "80"})
51+
52+
53+
@pytest.mark.parametrize("timeout, expected_timeout", ((1, 1), ("10", 10), (None, 60)))
54+
def test_get_nightly_metadata(monkeypatch, timeout, expected_timeout):
55+
ReleaseClassMock = MagicMock()
56+
release_instance_mock = MagicMock()
57+
nightly_metadata = {"version": "150.0a1", "locales": ["en-US", "de"]}
58+
attrs = {"get_nightly_metadata.return_value": nightly_metadata}
59+
release_instance_mock.configure_mock(**attrs)
60+
ReleaseClassMock.side_effect = lambda *args, **kwargs: release_instance_mock
61+
monkeypatch.setattr(shipitscript.ship_actions, "Release_V2", ReleaseClassMock)
62+
63+
ship_it_instance_config = {"taskcluster_client_id": "some-id", "taskcluster_access_token": "some-token", "api_root_v2": "http://some.ship-it.tld/api/root"}
64+
if timeout is not None:
65+
ship_it_instance_config["timeout_in_seconds"] = timeout
66+
67+
ret = shipitscript.ship_actions.get_nightly_metadata(ship_it_instance_config, "firefox", "nightly", "20260525000000")
68+
69+
assert ret == nightly_metadata
70+
ReleaseClassMock.assert_called_with(
71+
taskcluster_client_id="some-id", taskcluster_access_token="some-token", api_root="http://some.ship-it.tld/api/root", timeout=expected_timeout
72+
)
73+
release_instance_mock.get_nightly_metadata.assert_called_with(
74+
"firefox", "nightly", "20260525000000", headers={"X-Forwarded-Proto": "https", "X-Forwarded-Port": "80"}
75+
)
76+
77+
78+
@pytest.mark.parametrize("timeout, expected_timeout", ((1, 1), ("10", 10), (None, 60)))
79+
def test_create_new_nightly_release(monkeypatch, timeout, expected_timeout):
80+
ReleaseClassMock = MagicMock()
81+
release_instance_mock = MagicMock()
82+
attrs = {"create_new_nightly_release.return_value": {"message": "ok"}}
83+
release_instance_mock.configure_mock(**attrs)
84+
ReleaseClassMock.side_effect = lambda *args, **kwargs: release_instance_mock
85+
monkeypatch.setattr(shipitscript.ship_actions, "Release_V2", ReleaseClassMock)
86+
87+
ship_it_instance_config = {"taskcluster_client_id": "some-id", "taskcluster_access_token": "some-token", "api_root_v2": "http://some.ship-it.tld/api/root"}
88+
if timeout is not None:
89+
ship_it_instance_config["timeout_in_seconds"] = timeout
90+
91+
shipitscript.ship_actions.create_new_nightly_release(
92+
ship_it_instance_config, "firefox", "nightly", "20260525000000", "150.0a1", ["en-US", "de"]
93+
)
94+
95+
ReleaseClassMock.assert_called_with(
96+
taskcluster_client_id="some-id", taskcluster_access_token="some-token", api_root="http://some.ship-it.tld/api/root", timeout=expected_timeout
97+
)
98+
release_instance_mock.create_new_nightly_release.assert_called_with(
99+
"firefox", "nightly", "20260525000000", "150.0a1", ["en-US", "de"], headers={"X-Forwarded-Proto": "https", "X-Forwarded-Port": "80"}
100+
)

shipitscript/tests/test_shipitapi.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,63 @@ def __init__(self):
8989
release.session.request.return_value.content = "Not JSON at all"
9090
with pytest.raises(json.decoder.JSONDecodeError):
9191
release.getRelease(release_name)
92+
93+
94+
def test_release_v2_nightly_release(mocker):
95+
class MockResponse(requests.Response):
96+
_payload = {"success": True, "test": True}
97+
98+
def __init__(self):
99+
super(MockResponse, self).__init__()
100+
self.status_code = 200
101+
102+
def json(self):
103+
return self._payload
104+
105+
release = Release_V2(taskcluster_client_id="some-id", taskcluster_access_token="some-token", api_root="https://www.apiroot.com/", retry_attempts=1)
106+
mocker.patch.object(release, "session")
107+
release.session.request.return_value = MockResponse()
108+
api_call_count = 0
109+
110+
# test that get_nightly_metadata calls correct URL with query string
111+
headers = {"X-Test": "yes"}
112+
ret = release.get_nightly_metadata("firefox", "nightly", "20260525000000", headers=headers)
113+
assert ret["test"] is True
114+
correct_url = "https://www.apiroot.com/nightly-release?product=firefox&channel=nightly&buildid=20260525000000"
115+
release.session.request.assert_called_with(data=None, headers={"X-Test": "yes"}, method="GET", timeout=mock.ANY, verify=mock.ANY, url=correct_url)
116+
api_call_count += 1
117+
assert release.session.request.call_count == api_call_count
118+
# make sure we don't modify the passed headers dictionary in the methods
119+
assert headers == {"X-Test": "yes"}
120+
121+
# test that get_nightly_metadata works with no headers
122+
ret = release.get_nightly_metadata("firefox", "nightly", "20260525000000")
123+
assert ret["test"] is True
124+
release.session.request.assert_called_with(data=None, headers={}, method="GET", timeout=mock.ANY, verify=mock.ANY, url=correct_url)
125+
api_call_count += 1
126+
assert release.session.request.call_count == api_call_count
127+
128+
# test that create_new_nightly_release calls correct URL
129+
headers = {"X-Test": "yes"}
130+
ret = release.create_new_nightly_release("firefox", "nightly", "20260525000000", "150.0a1", ["en-US", "de"], headers=headers)
131+
assert ret["test"] is True
132+
correct_url = "https://www.apiroot.com/nightly-release"
133+
expected_data = json.dumps(
134+
{
135+
"product": "firefox",
136+
"channel": "nightly",
137+
"buildid": "20260525000000",
138+
"version": "150.0a1",
139+
"locales": ["en-US", "de"],
140+
}
141+
)
142+
release.session.request.assert_called_with(data=expected_data, headers=mock.ANY, method="POST", timeout=mock.ANY, verify=mock.ANY, url=correct_url)
143+
api_call_count += 1
144+
assert release.session.request.call_count == api_call_count
145+
assert headers == {"X-Test": "yes"}
146+
147+
# test that exception is raised on error, and api call is retried
148+
release.session.request.return_value.status_code = 400
149+
with pytest.raises(requests.exceptions.HTTPError):
150+
release.get_nightly_metadata("firefox", "nightly", "20260525000000")
151+
assert release.session.request.call_count == api_call_count + release.retries

0 commit comments

Comments
 (0)