Skip to content

Commit f0c65cf

Browse files
committed
pants-plugins/release: calculate packagecloud next release number
1 parent 6b901f3 commit f0c65cf

2 files changed

Lines changed: 189 additions & 4 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Copyright 2025 The StackStorm Authors.
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+
from __future__ import annotations
16+
17+
from dataclasses import dataclass
18+
from typing import Any, Optional
19+
20+
import requests
21+
from pants.engine.env_vars import EnvironmentVars, EnvironmentVarsRequest
22+
from requests.auth import HTTPBasicAuth
23+
24+
from pants.engine.internals.selectors import Get
25+
from pants.engine.rules import _uncacheable_rule, collect_rules
26+
27+
ARCH_NAMES = { # {nfpm_arch: {pkg_type: packagecloud_arch}}
28+
# The key comes from the 'arch' field of nfpm_*_package targets (GOARCH or GOARCH+GOARM).
29+
# https://www.pantsbuild.org/stable/reference/targets/nfpm_deb_package#arch
30+
# https://www.pantsbuild.org/stable/reference/targets/nfpm_rpm_package#arch
31+
"amd64": {
32+
"deb": "amd64",
33+
"rpm": "x86_64",
34+
}
35+
}
36+
37+
# This includes distros we do not support.
38+
DISTROS_BY_PKG_TYPE = { # {pkg_type: {distro: {distro_id: distro_version}}}
39+
"deb": {
40+
"debian": { # no releases in packagecloud (so far)
41+
"buster": "10",
42+
"bullseye": "11",
43+
"bookworm": "12",
44+
"trixie": "13",
45+
"forky": "14",
46+
},
47+
"ubuntu": { # Only LTS releases
48+
"trusty": "14.04", # the oldest with releases in packagecloud
49+
"xenial": "16.04",
50+
"bionic": "18.04",
51+
"focal": "20.04",
52+
"jammy": "22.04",
53+
"noble": "24.04",
54+
},
55+
},
56+
"rpm": {
57+
"el": { # EL = Enterprise Linux (RHEL, Rocky, Alma, ...)
58+
# 6 is the oldest with releases in packagecloud
59+
f"el{v}": f"{v}"
60+
for v in (6, 7, 8, 9)
61+
},
62+
},
63+
}
64+
65+
DISTRO_INFO = {
66+
distro_id: {
67+
"distro": distro,
68+
"version": distro_version,
69+
"pkg_type": pkg_type,
70+
}
71+
for pkg_type, distros in DISTROS_BY_PKG_TYPE.items()
72+
for distro, distro_ids in distros.items()
73+
for distro_id, distro_version in distro_ids.items()
74+
}
75+
76+
77+
@dataclass
78+
class PackageCloudNextReleaseRequest:
79+
nfpm_arch: str
80+
distro_id: str
81+
package_name: str
82+
package_version: str
83+
production: bool
84+
85+
86+
@dataclass
87+
class PackageCloudNextRelease:
88+
value: Optional[int] = None
89+
90+
91+
@_uncacheable_rule
92+
async def packagecloud_get_next_release(
93+
request: PackageCloudNextReleaseRequest,
94+
) -> PackageCloudNextRelease:
95+
env_vars: EnvironmentVars = await Get(
96+
EnvironmentVars, EnvironmentVarsRequest(["PACKAGECLOUD_TOKEN"])
97+
)
98+
package_cloud_token = env_vars.get("PACKAGECLOUD_TOKEN")
99+
if not package_cloud_token:
100+
return PackageCloudNextRelease()
101+
102+
client = requests.session()
103+
client.auth = HTTPBasicAuth(package_cloud_token, "")
104+
105+
def get(url_path: str) -> list[dict[str, Any]]:
106+
response = client.get(f"https://packagecloud.io{url_path}")
107+
response.raise_for_status()
108+
ret: list[dict[str, Any]] = response.json()
109+
next_url = response.links.get("next", {}).get("url")
110+
while next_url:
111+
response = client.get(f"https://packagecloud.io{next_url}")
112+
response.raise_for_status()
113+
ret.extend(response.json())
114+
next_url = response.links.get("next", {}).get("url")
115+
return ret
116+
117+
distro_id = request.distro_id
118+
distro_info = DISTRO_INFO[distro_id]
119+
pkg_is_unstable = "dev" in request.package_version
120+
121+
# packagecloud url params:
122+
org = "stackstorm"
123+
repo = f"{'' if request.production else 'staging-'}{'unstable' if pkg_is_unstable else 'stable'}"
124+
pkg_type = distro_info["pkg_type"]
125+
distro = distro_info["distro"]
126+
distro_version = distro_id if pkg_type == "deb" else distro_info["version"]
127+
pkg_name = request.package_name
128+
arch = ARCH_NAMES[request.nfpm_arch][pkg_type]
129+
130+
# https://packagecloud.io/docs/api#resource_packages_method_index (api doc incorrectly drops /:package)
131+
# /api/v1/repos/:user_id/:repo/packages/:type/:distro/:version/:package/:arch.json
132+
index_url = f"/api/v1/repos/{org}/{repo}/packages/{pkg_type}/{distro}/{distro_version}/{pkg_name}/{arch}.json"
133+
package_index: list[dict[str, Any]] = get(index_url)
134+
if not package_index:
135+
return PackageCloudNextRelease()
136+
137+
versions_url: str = package_index[0]["versions_url"]
138+
versions: list[dict[str, Any]] = get(versions_url)
139+
releases = [
140+
version_info["release"]
141+
for version_info in versions
142+
if version_info["version"] == request.package_version
143+
]
144+
if not releases:
145+
return PackageCloudNextRelease()
146+
147+
max_release = max(int(release) for release in releases)
148+
next_release = max_release + 1
149+
return PackageCloudNextRelease(next_release)
150+
151+
152+
def rules():
153+
return [
154+
*collect_rules(),
155+
]

pants-plugins/release/rules.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@
2323
import re
2424
from dataclasses import dataclass
2525

26-
from pants.backend.nfpm.fields.version import NfpmVersionField, NfpmVersionSchemaField
26+
from pants.backend.nfpm.fields.all import (
27+
NfpmArchField,
28+
NfpmPackageNameField,
29+
)
30+
from pants.backend.nfpm.fields.version import (
31+
NfpmVersionField,
32+
NfpmVersionReleaseField,
33+
NfpmVersionSchemaField,
34+
)
2735
from pants.backend.nfpm.util_rules.inject_config import (
2836
InjectedNfpmPackageFields,
2937
InjectNfpmPackageFieldsRequest,
@@ -38,6 +46,12 @@
3846
from pants.engine.rules import collect_rules, Get, MultiGet, rule, UnionRule
3947
from pants.util.frozendict import FrozenDict
4048

49+
from .packagecloud_rules import (
50+
PackageCloudNextReleaseRequest,
51+
packagecloud_get_next_release,
52+
)
53+
from .packagecloud_rules import rules as packagecloud_rules
54+
4155

4256
REQUIRED_KWARGS = (
4357
"description",
@@ -203,7 +217,8 @@ def is_applicable(cls, _: Target) -> bool:
203217
async def inject_package_fields(
204218
request: StackStormNfpmPackageFieldsRequest,
205219
) -> InjectedNfpmPackageFields:
206-
address = request.target.address
220+
target = request.target
221+
address = target.address
207222

208223
version_file = "st2common/st2common/__init__.py"
209224
extracted_version = await Get(
@@ -215,20 +230,35 @@ async def inject_package_fields(
215230
)
216231

217232
version: str = extracted_version.value
218-
if version.endswith("dev") and version[-4] != "-":
233+
is_dev = "dev" in version
234+
if is_dev and "-dev" not in version:
219235
# nfpm parses this into version[-version_prerelease][+version_metadata]
220-
# that dash is required to be a valid semver version.
236+
# that dash is required to be a valid semver version (3.9dev => 3.9-dev).
221237
version = version.replace("dev", "-dev")
222238

239+
# this is specific to distro-version (EL8, EL9, Ubuntu Focal, Ubuntu Jammy, ...)
240+
next_release = await packagecloud_get_next_release(
241+
PackageCloudNextReleaseRequest(
242+
nfpm_arch=target[NfpmArchField].value,
243+
distro_id="", # TODO: add field for distro ID
244+
package_name=target[NfpmPackageNameField].value,
245+
package_version=version,
246+
production=not is_dev,
247+
)
248+
)
249+
release = 1 if next_release.value is None else next_release.value
250+
223251
fields: list[Field] = [
224252
NfpmVersionSchemaField("semver", address=address),
225253
NfpmVersionField(version, address=address),
254+
NfpmVersionReleaseField(release, address=address),
226255
]
227256
return InjectedNfpmPackageFields(fields, address=address)
228257

229258

230259
def rules():
231260
return [
261+
*packagecloud_rules(),
232262
*collect_rules(),
233263
UnionRule(SetupKwargsRequest, StackStormSetupKwargsRequest),
234264
UnionRule(InjectNfpmPackageFieldsRequest, StackStormNfpmPackageFieldsRequest),

0 commit comments

Comments
 (0)