Building a Production-Ready GitLab Release with Binary Distribution Using the Generic Package Registry
Introduction
In previous articles, we explored how to build and distribute software using native package managers such as APT and YUM, backed by Nexus repositories. Those approaches provide strong integration within specific Linux ecosystems.
However, not all environments fit into those models.
There are cases where:
- the target system is not Debian-based or RHEL-based
- the tool must run across multiple platforms (Linux, macOS)
- the overhead of maintaining package repositories is unnecessary
- a simple, versioned, and controlled binary distribution is sufficient
In this article, we document a complete, practical approach to distributing binaries using GitLab — combining:
- Git tags for versioning
- GitLab Releases for visibility
- The Generic Package Registry for reliable artifact storage and download
- Structured Release Notes and CHANGELOG
This is a pragmatic and production-ready solution for distributing internal tools.
https://github.com/faustobranco/git-semver
1. Preparing the Repository and Establishing Versioning
After implementing fixes and improvements:
git add .
git commit -m "fix: fix json return"
git push origin main
A new version is then defined:
git tag -a v0.2.1 -m "Release v0.2.1"
git push origin v0.2.1
Why this matters
Tags provide:
- an immutable reference to the code state
- a clear boundary between versions
- a foundation for changelog generation and binary versioning
Without tags, releases are not reproducible.
2. Building Multi-Platform Binaries
To ensure compatibility across environments, binaries are compiled for multiple platforms.
GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=v0.2.1" -o git-semver-linux-amd64
GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=v0.2.1" -o git-semver-linux-arm64
GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.version=v0.2.1" -o git-semver-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -ldflags "-X main.version=v0.2.1" -o git-semver-darwin-arm64
chmod +x git-semver-*
Why embed the version
The -ldflags parameter injects the version directly into the binary:
git-semver --version
This guarantees that:
- the binary always reports its version
- there is no ambiguity between artifacts
- debugging and traceability are simplified
3. Creating Release Notes
Release Notes provide structured documentation of what changed in a version.
Create a file:
mkdir releases
vi releases/v0.2.1.md
Example:
## git-semver v0.2.1
### Features
- Add JSON output support
- Improve commit analysis with regex
### Improvements
- Better handling of repositories without tags
- Optional push flag behavior
### Fixes
- Correct version bump logic
- Clean CLI output for automation
Create the release via API:
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--data "name=git-semver v0.2.1" \
--data "tag_name=v0.2.1" \
--data-urlencode "description=$(cat releases/v0.2.1.md)" \
"https://gitlab.devops-db.internal/api/v4/projects/32/releases"
Why use the API
- reproducible process
- scriptable
- avoids manual UI errors
- integrates easily with automation later if needed
4. Uploading Binaries: The Right Approach
GitLab Releases do not support proper binary uploads for automation purposes. The /uploads mechanism is UI-oriented and not suitable for CLI consumption.
Instead, the Generic Package Registry is used.
4.1 Uploading to the Registry and Linking Binaries to the Release
GITLAB_TOKEN=glpat-8a-1Cz-weLVaZUl5Fdcq6286MQp1OjcH.01.0w0p6oorb
GITLAB_URL="https://gitlab.devops-db.internal"
PROJECT_ID="32"
RELEASE_VERSION="v0.2.1"
PACKAGE_NAME="git-semver"
# List of files to upload
FILES=(
"git-semver-linux-amd64"
"git-semver-linux-arm64"
"git-semver-darwin-amd64"
"git-semver-darwin-arm64"
)
for FILENAME in "${FILES[@]}"; do
echo "Processing $FILENAME..."
# 1. Upload to Generic Package Registry
# This endpoint is API-friendly and supports PRIVATE-TOKEN for downloads
curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--upload-file "$FILENAME" \
"$GITLAB_URL/api/v4/projects/$PROJECT_ID/packages/generic/$PACKAGE_NAME/$RELEASE_VERSION/$FILENAME"
# 2. Define the permanent direct URL
DIRECT_URL="$GITLAB_URL/api/v4/projects/$PROJECT_ID/packages/generic/$PACKAGE_NAME/$RELEASE_VERSION/$FILENAME"
# 3. Link the asset to the Release
# We use --data-urlencode for the URL to ensure it's handled correctly by the API
curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--data "name=$FILENAME" \
--data-urlencode "url=$DIRECT_URL" \
"$GITLAB_URL/api/v4/projects/$PROJECT_ID/releases/$RELEASE_VERSION/assets/links"
echo -e "\nDone: $FILENAME linked."
doneWhy the Generic Package Registry
- stable and predictable URLs
- native API support
- token-based authentication
- designed for artifact storage
Why link instead of upload
This creates a clean separation:
- Release → documentation and visibility
- Registry → storage and distribution
5. Generating and Maintaining the CHANGELOG
The CHANGELOG provides a chronological history of changes.
Generate automatically:
LAST_TAG=v0.2.0
NEW_TAG=v0.2.1
echo "## [$NEW_TAG] - $(date +%F)" > CHANGELOG.tmp
git log $LAST_TAG..HEAD --pretty=format:"- %s" >> CHANGELOG.tmp
Next, prepare the CHANGELOG.md file, always making changes incrementally, with the latest updates at the top of the file.
## [v0.2.1] - 2026-03-26
### Added
-
### Fixed
- fix: build version/tag v0.2.1
- fix: fix json return%
Commit:
git add .
git commit -m "doc: CHANGELOG.md v0.2.1"
git push origin main
Why maintain a CHANGELOG
- provides historical traceability
- complements Release Notes
- supports auditing and debugging
6. Updating Release Notes (if needed)
Release Notes can be updated after creation:
curl --request PUT \
--header "PRIVATE-TOKEN: glpat-8a-1Cz-weLVaZUl5Fdcq6286MQp1OjcH.01.0w0p6oorb" \
--data-urlencode "description=$(cat releases/v0.2.1.md)" \
"https://gitlab.devops-db.internal/api/v4/projects/32/releases/v0.2.1"7. Testing the Distribution in a Clean Environment
Validation is performed using a clean container.
docker run -it --rm -v $(pwd):/work ubuntu:22.04 bash
Inside the container:
apt update && apt install -y curl
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-o git-semver \
"https://gitlab.devops-db.internal/api/v4/projects/32/packages/generic/git-semver/v0.2.1/git-semver-linux-amd64"
chmod +x git-semver
./git-semver --version
Why this step is critical
- validates real-world usage
- ensures no hidden dependencies
- guarantees reproducibility
Conclusion
This approach provides a clean and effective way to distribute binaries using GitLab.
It combines:
- structured versioning (Git tags)
- clear documentation (Release Notes, CHANGELOG)
- reliable storage (Generic Package Registry)
- simple consumption (curl-based installation)
Most importantly, it fills the gap left by traditional package managers.
APT and YUM remain the best choice within their ecosystems.
However, for cross-platform tools, lightweight distribution, and internal utilities, this model offers a practical and production-ready alternative.
