Skip to content

Commit 36c57ff

Browse files
committed
actions: add publish-to-gitlab
Add a custom action which uses a deploy token to push built wheels to the GitLab package registry. Signed-off-by: Trevor Gamblin <tgamblin@baylibre.com>
1 parent 1887a91 commit 36c57ff

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
name: 'Publish to GitLab Package Registry'
2+
description: >
3+
Uploads one or more build artifacts from a GitHub Actions runner to a
4+
GitLab Generic Package Registry endpoint using the GitLab REST API.
5+
6+
inputs:
7+
8+
# ── Required ────────────────────────────────────────────────────────────────
9+
10+
gitlab-token:
11+
description: >
12+
GitLab authentication token. Use a Personal Access Token (scope: api)
13+
or a Deploy Token (scope: write_package_registry).
14+
Store this value as a GitHub Actions secret and pass it in; never
15+
hard-code it in a workflow file.
16+
required: true
17+
18+
gitlab-project-id:
19+
description: >
20+
Numeric ID of the target GitLab project (found under
21+
Settings → General → Project ID).
22+
required: true
23+
24+
package-name:
25+
description: >
26+
Name of the package to create or update in the registry.
27+
Allowed characters: a-z, A-Z, 0-9, dot (.), hyphen (-), underscore (_).
28+
required: true
29+
30+
package-version:
31+
description: >
32+
Semantic version string for this package release, e.g. "1.2.3" or
33+
"1.0.0-beta.1". Tip: pass github.ref_name for tag-triggered workflows.
34+
required: true
35+
36+
# files is a newline-separated list of glob patterns resolved by the runner.
37+
files:
38+
description: >
39+
Newline-separated list of file paths (or globs) to upload.
40+
Each matched file is uploaded as a separate package file.
41+
Example:
42+
files: |
43+
dist/my-app-linux-amd64
44+
dist/my-app-darwin-arm64
45+
checksums.txt
46+
required: true
47+
48+
# ── Optional ────────────────────────────────────────────────────────────────
49+
50+
gitlab-host:
51+
description: >
52+
Base URL of your GitLab instance. Override for self-hosted GitLab.
53+
required: false
54+
default: 'https://gitlab.com'
55+
56+
token-type:
57+
description: >
58+
The type of token supplied in gitlab-token.
59+
Accepted values: "private-token" (Personal / Project Access Token) or
60+
"deploy-token".
61+
required: false
62+
default: 'private-token'
63+
64+
status:
65+
description: >
66+
Package status after upload. Use "default" to make the package visible
67+
in the UI, or "hidden" to suppress it from listings (useful for
68+
draft/pre-release packages).
69+
required: false
70+
default: 'default'
71+
72+
fail-on-duplicate:
73+
description: >
74+
When "true", the action exits with an error if a file with the same
75+
name already exists in the specified package version. When "false"
76+
(default), the existing file is silently overwritten.
77+
required: false
78+
default: 'false'
79+
80+
outputs:
81+
82+
package-url:
83+
description: >
84+
URL of the package in the GitLab Package Registry UI.
85+
value: >-
86+
${{ inputs.gitlab-host }}/-/packages?search[]=${{ inputs.package-name }}
87+
88+
uploaded-files:
89+
description: Newline-separated list of filenames that were successfully uploaded.
90+
value: ${{ steps.upload.outputs.uploaded_files }}
91+
92+
runs:
93+
using: 'composite'
94+
steps:
95+
96+
# ── 1. Validate inputs ───────────────────────────────────────────────────
97+
- name: Validate inputs
98+
shell: bash
99+
run: |
100+
echo "::group::Validating inputs"
101+
102+
error=0
103+
104+
if [[ -z "${{ inputs.gitlab-token }}" ]]; then
105+
echo "::error::Input 'gitlab-token' must not be empty."
106+
error=1
107+
fi
108+
109+
if [[ ! "${{ inputs.gitlab-project-id }}" =~ ^[0-9]+$ ]]; then
110+
echo "::error::Input 'gitlab-project-id' must be a numeric project ID."
111+
error=1
112+
fi
113+
114+
if [[ ! "${{ inputs.package-name }}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
115+
echo "::error::Input 'package-name' contains invalid characters. Allowed: a-z A-Z 0-9 . - _"
116+
error=1
117+
fi
118+
119+
if [[ -z "${{ inputs.package-version }}" ]]; then
120+
echo "::error::Input 'package-version' must not be empty."
121+
error=1
122+
fi
123+
124+
TOKEN_TYPE="${{ inputs.token-type }}"
125+
if [[ "$TOKEN_TYPE" != "private-token" && "$TOKEN_TYPE" != "deploy-token" ]]; then
126+
echo "::error::Input 'token-type' must be 'private-token' or 'deploy-token'. Got: $TOKEN_TYPE"
127+
error=1
128+
fi
129+
130+
STATUS="${{ inputs.status }}"
131+
if [[ "$STATUS" != "default" && "$STATUS" != "hidden" ]]; then
132+
echo "::error::Input 'status' must be 'default' or 'hidden'. Got: $STATUS"
133+
error=1
134+
fi
135+
136+
if [[ $error -eq 1 ]]; then
137+
echo "::endgroup::"
138+
exit 1
139+
fi
140+
141+
echo "All inputs validated successfully."
142+
echo "::endgroup::"
143+
144+
# ── 2. Resolve globs and upload files ────────────────────────────────────
145+
- name: Upload files
146+
id: upload
147+
shell: bash
148+
env:
149+
GITLAB_TOKEN: ${{ inputs.gitlab-token }}
150+
run: |
151+
echo "::group::Uploading files to GitLab Package Registry"
152+
153+
GITLAB_HOST="${{ inputs.gitlab-host }}"
154+
PROJECT_ID="${{ inputs.gitlab-project-id }}"
155+
PKG_NAME="${{ inputs.package-name }}"
156+
PKG_VERSION="${{ inputs.package-version }}"
157+
TOKEN_TYPE="${{ inputs.token-type }}"
158+
STATUS="${{ inputs.status }}"
159+
FAIL_ON_DUPLICATE="${{ inputs.fail-on-duplicate }}"
160+
161+
API_BASE="${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/packages/generic/${PKG_NAME}/${PKG_VERSION}"
162+
163+
uploaded_files=()
164+
failed_files=()
165+
166+
# Read the newline-separated file list and expand globs
167+
while IFS= read -r pattern; do
168+
# Skip blank lines
169+
[[ -z "$pattern" ]] && continue
170+
171+
# Expand glob; if no match, the literal string is returned — catch that
172+
matched=( $pattern )
173+
if [[ ${#matched[@]} -eq 0 ]] || [[ ! -e "${matched[0]}" ]]; then
174+
echo "::warning::Pattern '${pattern}' did not match any files — skipping."
175+
continue
176+
fi
177+
178+
for filepath in "${matched[@]}"; do
179+
if [[ ! -f "$filepath" ]]; then
180+
echo "::warning::'${filepath}' is not a regular file — skipping."
181+
continue
182+
fi
183+
184+
filename=$(basename "$filepath")
185+
url="${API_BASE}/${filename}?status=${STATUS}"
186+
187+
echo "Uploading: ${filepath} → ${url}"
188+
189+
http_status=$(curl \
190+
--silent \
191+
--output /tmp/gitlab_upload_response.txt \
192+
--write-out "%{http_code}" \
193+
--location \
194+
--header "${TOKEN_TYPE}: ${GITLAB_TOKEN}" \
195+
--upload-file "${filepath}" \
196+
"${url}")
197+
198+
response_body=$(cat /tmp/gitlab_upload_response.txt)
199+
200+
if [[ "$http_status" == "201" ]]; then
201+
echo " ✓ Uploaded successfully (HTTP 201)."
202+
uploaded_files+=("$filename")
203+
204+
elif [[ "$http_status" == "200" ]]; then
205+
if [[ "$FAIL_ON_DUPLICATE" == "true" ]]; then
206+
echo "::error::File '${filename}' already exists in ${PKG_NAME}@${PKG_VERSION} and fail-on-duplicate is true."
207+
failed_files+=("$filename")
208+
else
209+
echo " ✓ File already existed and was overwritten (HTTP 200)."
210+
uploaded_files+=("$filename")
211+
fi
212+
213+
else
214+
echo "::error::Failed to upload '${filename}'. HTTP ${http_status}: ${response_body}"
215+
failed_files+=("$filename")
216+
fi
217+
done
218+
done <<< "${{ inputs.files }}"
219+
220+
# Emit output
221+
uploaded_list=$(printf '%s\n' "${uploaded_files[@]}")
222+
echo "uploaded_files<<EOF" >> "$GITHUB_OUTPUT"
223+
echo "$uploaded_list" >> "$GITHUB_OUTPUT"
224+
echo "EOF" >> "$GITHUB_OUTPUT"
225+
226+
echo ""
227+
echo "── Summary ──────────────────────────────────────────────────"
228+
echo " Uploaded : ${#uploaded_files[@]} file(s)"
229+
echo " Failed : ${#failed_files[@]} file(s)"
230+
echo "─────────────────────────────────────────────────────────────"
231+
232+
echo "::endgroup::"
233+
234+
if [[ ${#failed_files[@]} -gt 0 ]]; then
235+
echo "::error::${#failed_files[@]} file(s) failed to upload: ${failed_files[*]}"
236+
exit 1
237+
fi
238+
239+
# ── 3. Annotate the workflow run with the registry URL ───────────────────
240+
- name: Print registry URL
241+
shell: bash
242+
run: |
243+
echo "### GitLab Package Registry" >> "$GITHUB_STEP_SUMMARY"
244+
echo "" >> "$GITHUB_STEP_SUMMARY"
245+
echo "Package \`${{ inputs.package-name }}@${{ inputs.package-version }}\` published." >> "$GITHUB_STEP_SUMMARY"
246+
echo "" >> "$GITHUB_STEP_SUMMARY"
247+
echo "**Registry:** ${{ inputs.gitlab-host }}/$( \
248+
echo '${{ inputs.gitlab-host }}' | sed 's|https://||' \
249+
)" >> "$GITHUB_STEP_SUMMARY"
250+
echo "" >> "$GITHUB_STEP_SUMMARY"
251+
echo "**Uploaded files:**" >> "$GITHUB_STEP_SUMMARY"
252+
while IFS= read -r f; do
253+
[[ -n "$f" ]] && echo "- \`$f\`" >> "$GITHUB_STEP_SUMMARY"
254+
done <<< "${{ steps.upload.outputs.uploaded_files }}"

0 commit comments

Comments
 (0)