Skip to content

Commit d2404f9

Browse files
committed
Publish to the GitLab package registry when it lacks the version
The package job asked the registry's PyPI index whether the files were there already. For a package it does not hold, GitLab redirects that index to pypi.org, so the check saw PyPI's files and skipped the upload. It now asks the packages API, which answers for the project alone, and a test keeps the index check out. The release's checksums now cover only the wheel and sdist, not the .gitignore uv build writes into dist/, and the package name comes from pyproject.toml.
1 parent 4da6ea7 commit d2404f9

2 files changed

Lines changed: 53 additions & 9 deletions

File tree

‎.gitlab-ci.yml‎

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ build:
144144
script:
145145
- |
146146
set -euo pipefail
147-
version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
147+
read -r package version < <(python3 -c 'import tomllib; p = tomllib.load(open("pyproject.toml", "rb"))["project"]; print(p["name"], p["version"])')
148148
if [ -n "${CI_COMMIT_TAG:-}" ] && [ "v${version}" != "${CI_COMMIT_TAG}" ]; then
149149
echo "tag ${CI_COMMIT_TAG} does not match pyproject.toml version ${version}"
150150
exit 1
@@ -156,7 +156,7 @@ build:
156156
/tmp/smoke/bin/ldo --version
157157
/tmp/smoke/bin/ldo --help > /dev/null
158158
# For the jobs after this one: the version, and the tags each image is published with.
159-
echo "VERSION=${version}" > build.env
159+
printf 'PACKAGE=%s\nVERSION=%s\n' "${package}" "${version}" > build.env
160160
mkdir -p image-tags
161161
for variant in az tool; do
162162
python3 scripts/image_tags.py --version "${version}" --variant "${variant}" \
@@ -248,8 +248,7 @@ container:
248248

249249
# Publish (release tags only) ---------------------------------------------------------
250250

251-
# After the images, so the package never exists without them. A file already there (a
252-
# retried pipeline) is skipped, not an error.
251+
# After the images, so the package never exists without them.
253252
package:
254253
stage: publish
255254
needs: [build, container]
@@ -258,10 +257,44 @@ package:
258257
- *release-tag
259258
variables:
260259
UV_PUBLISH_URL: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi
261-
UV_PUBLISH_CHECK_URL: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi/simple
262260
UV_PUBLISH_USERNAME: gitlab-ci-token
263261
script:
264-
- UV_PUBLISH_PASSWORD="${CI_JOB_TOKEN}" uv publish dist/*
262+
- |
263+
set -euo pipefail
264+
# Whether this version is here already (a retried pipeline), asked of the packages
265+
# API. Not of the PyPI index (uv publish --check-url): for a package it does not hold,
266+
# GitLab redirects there to pypi.org, whose files would pass for its own.
267+
present="$(python3 - <<'PY'
268+
import json
269+
import os
270+
import urllib.parse
271+
import urllib.request
272+
273+
env = os.environ
274+
query = urllib.parse.urlencode(
275+
{"package_type": "pypi", "package_name": env["PACKAGE"], "package_version": env["VERSION"]}
276+
)
277+
request = urllib.request.Request(
278+
f"{env['CI_API_V4_URL']}/projects/{env['CI_PROJECT_ID']}/packages?{query}",
279+
headers={"JOB-TOKEN": env["CI_JOB_TOKEN"]},
280+
)
281+
with urllib.request.urlopen(request, timeout=30) as response:
282+
packages = json.load(response)
283+
284+
def normal(name):
285+
return name.casefold().replace("_", "-").replace(".", "-")
286+
287+
print(any(
288+
normal(item["name"]) == normal(env["PACKAGE"]) and item["version"] == env["VERSION"]
289+
for item in packages
290+
))
291+
PY
292+
)"
293+
if [ "${present}" = True ]; then
294+
echo "${PACKAGE} ${VERSION} is in the package registry already"
295+
exit 0
296+
fi
297+
UV_PUBLISH_PASSWORD="${CI_JOB_TOKEN}" uv publish dist/*.whl dist/*.tar.gz
265298
266299
# The GitLab release, last: how to install from this project's registries, and the files'
267300
# checksums. A release that is there already (a retried pipeline) is left as it is.
@@ -283,8 +316,9 @@ release:
283316
import urllib.request
284317
285318
env = os.environ
286-
tag, version = env["CI_COMMIT_TAG"], env["VERSION"]
287-
files = sorted(pathlib.Path("dist").iterdir())
319+
tag, package, version = env["CI_COMMIT_TAG"], env["PACKAGE"], env["VERSION"]
320+
dist = pathlib.Path("dist")
321+
files = sorted([*dist.glob("*.whl"), *dist.glob("*.tar.gz")])
288322
sums = "\n".join(f"{hashlib.sha256(f.read_bytes()).hexdigest()} {f.name}" for f in files)
289323
index = f"{env['CI_API_V4_URL']}/projects/{env['CI_PROJECT_ID']}/packages/pypi/simple"
290324
image = env["CI_REGISTRY_IMAGE"]
@@ -293,7 +327,7 @@ release:
293327
"" if final else "A pre-release: installed only by asking for this version.\n",
294328
f"What changed: [CHANGELOG.md]({env['CI_PROJECT_URL']}/-/blob/{tag}/CHANGELOG.md).\n",
295329
"```bash",
296-
f"uv tool install --index {index} libre-devops-helpers=={version}",
330+
f"uv tool install --index {index} {package}=={version}",
297331
f"podman pull {image}:{version} # with the Azure CLI",
298332
f"podman pull {image}:{version}-slim # the tool alone",
299333
"```\n",

‎tests/project/test_gitlab_ci.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,13 @@ def test_it_publishes_only_to_its_own_registries():
5656
for job in ("package", "release"):
5757
assert GITLAB[job]["rules"] == [{"if": "$CI_COMMIT_TAG =~ /^v\\d+\\.\\d+\\.\\d+/"}], job
5858
assert "CI_REGISTRY_IMAGE" in "\n".join(GITLAB["container"]["script"])
59+
60+
61+
def test_the_package_job_does_not_ask_the_index_what_is_there():
62+
# GitLab redirects the index of a package it does not hold to pypi.org, so a check
63+
# there sees PyPI's files and skips the upload.
64+
package = GITLAB["package"]
65+
assert "UV_PUBLISH_CHECK_URL" not in package["variables"]
66+
lines = "\n".join(package["script"]).splitlines()
67+
commands = [line for line in lines if not line.lstrip().startswith("#")]
68+
assert not any("uv publish" in line and "--check-url" in line for line in commands)

0 commit comments

Comments
 (0)