From f0bc63d91cebbef48cb4bc602484c5777b6bcc06 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 1 Oct 2026 08:20:23 +0000 Subject: [PATCH] Create GitLab releases through API --- .gitlab-ci.yml | 7 +- docker-configs/create-gitlab-release.sh | 42 ++++++++++++ tests/test_ci_release.py | 87 +++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 4 deletions(-) create mode 100755 docker-configs/create-gitlab-release.sh create mode 100644 tests/test_ci_release.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9a21019..16e8b5a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -78,10 +78,9 @@ build-release: # This job runs in the build stage, which runs first. - docker buildx build --no-cache -f docker-configs/Dockerfile-ci -t gitlab.hacknix.net:5050/hacknix/freedmr:latest -t gitlab.hacknix.net:5050/hacknix/freedmr:$CI_COMMIT_TAG-with-proxy -t gitlab.hacknix.net:5050/hacknix/freedmr:development-latest --platform linux/arm/v7,linux/amd64,linux/i386,linux/arm64 --push . #- docker buildx build --no-cache -f docker-configs/Dockerfile-ci -t hacknix/freedmr:latest -t gitlab.hacknix.net:5050/hacknix/freedmr:latest -t hacknix/$CI_COMMIT_TAG-with-proxy -t gitlab.hacknix.net:5050/hacknix/freedmr:$CI_COMMIT_TAG-with-proxy -t hacknix/freedmr:development-latest -t gitlab.hacknix.net:5050/hacknix/freedmr:development-latest --platform linux/amd64,linux/i386 --push . - echo "Compile complete." - release: - tag_name: $CI_COMMIT_TAG - name: 'Release $CI_COMMIT_TAG' - description: 'Release created using CI.' + - >- + docker-configs/create-gitlab-release.sh "$CI_COMMIT_TAG" "Release $CI_COMMIT_TAG" + "FreeDMR $CI_COMMIT_TAG release. See the versioned release notes for deployment and validation details." only: - tags diff --git a/docker-configs/create-gitlab-release.sh b/docker-configs/create-gitlab-release.sh new file mode 100755 index 0000000..29f7b69 --- /dev/null +++ b/docker-configs/create-gitlab-release.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -eu + +if [ "$#" -ne 3 ]; then + echo "usage: $0 TAG NAME DESCRIPTION" >&2 + exit 2 +fi + +: "${CI_API_V4_URL:?CI_API_V4_URL is required}" +: "${CI_PROJECT_ID:?CI_PROJECT_ID is required}" +: "${CI_JOB_TOKEN:?CI_JOB_TOKEN is required}" + +tag_name=$1 +release_name=$2 +description=$3 +response_file=$(mktemp) +trap 'rm -f "$response_file"' EXIT HUP INT TERM + +status=$(curl --silent --show-error \ + --output "$response_file" \ + --write-out '%{http_code}' \ + --request POST \ + --header "JOB-TOKEN: $CI_JOB_TOKEN" \ + --data-urlencode "tag_name=$tag_name" \ + --data-urlencode "name=$release_name" \ + --data-urlencode "description=$description" \ + "$CI_API_V4_URL/projects/$CI_PROJECT_ID/releases") + +case "$status" in + 201) + echo "GitLab release created for $tag_name" + ;; + 409) + echo "GitLab release already exists for $tag_name" + ;; + *) + echo "GitLab release creation failed for $tag_name (HTTP $status)" >&2 + cat "$response_file" >&2 + exit 1 + ;; +esac diff --git a/tests/test_ci_release.py b/tests/test_ci_release.py new file mode 100644 index 0000000..d04be38 --- /dev/null +++ b/tests/test_ci_release.py @@ -0,0 +1,87 @@ +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +HELPER = ROOT / "docker-configs" / "create-gitlab-release.sh" +PIPELINE = ROOT / ".gitlab-ci.yml" + + +class GitLabReleaseHelperTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(dir=ROOT / "tests") + self.bin_dir = Path(self.temporary.name) + fake_curl = self.bin_dir / "curl" + fake_curl.write_text( + """#!/bin/sh +output= +while [ \"$#\" -gt 0 ]; do + case \"$1\" in + --output) + output=$2 + shift 2 + ;; + *) + shift + ;; + esac +done +printf '%s' \"${FAKE_CURL_BODY:-}\" > \"$output\" +printf '%s' \"${FAKE_CURL_STATUS:-201}\" +""", + encoding="ascii", + ) + fake_curl.chmod(0o755) + + def tearDown(self): + self.temporary.cleanup() + + def run_helper(self, status, body=""): + environment = os.environ.copy() + environment.update( + { + "CI_API_V4_URL": "https://gitlab.example/api/v4", + "CI_PROJECT_ID": "123", + "CI_JOB_TOKEN": "not-logged", + "FAKE_CURL_STATUS": str(status), + "FAKE_CURL_BODY": body, + "PATH": "{}:{}".format(self.bin_dir, environment["PATH"]), + } + ) + return subprocess.run( + ["/bin/sh", str(HELPER), "v1.2.3", "Release v1.2.3", "Test release"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + env=environment, + ) + + def test_created_release_succeeds(self): + result = self.run_helper(201) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("release created", result.stdout) + + def test_existing_release_is_idempotent(self): + result = self.run_helper(409) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("already exists", result.stdout) + + def test_api_failure_is_reported(self): + result = self.run_helper(500, '{"message":"failure"}') + self.assertNotEqual(result.returncode, 0) + self.assertIn("HTTP 500", result.stderr) + self.assertIn('"message":"failure"', result.stderr) + self.assertNotIn("not-logged", result.stderr) + + def test_pipeline_does_not_use_runner_release_keyword(self): + pipeline = PIPELINE.read_text(encoding="utf-8") + self.assertNotIn("\n release:\n", pipeline) + self.assertIn("docker-configs/create-gitlab-release.sh", pipeline) + + +if __name__ == "__main__": + unittest.main()