From c43b5c10113f5ccfd070e7f644aa3e220e4b2ec6 Mon Sep 17 00:00:00 2001 From: Trevor Gamblin Date: Tue, 30 Jun 2026 22:18:26 -0400 Subject: [PATCH 1/2] publish-to-gitlab: publish to PyPI package registry We want PyPI packages so that pip, uv, and so on can discover them properly. Modify publish-to-gitlab so that it produces these instead of "Generic" package uploads. This requires a username for the deploy token and a small change to the registry URL. Signed-off-by: Trevor Gamblin --- actions/publish-to-gitlab/action.yml | 238 +++++++++++---------------- 1 file changed, 95 insertions(+), 143 deletions(-) diff --git a/actions/publish-to-gitlab/action.yml b/actions/publish-to-gitlab/action.yml index 40cc6e3..a75bde9 100644 --- a/actions/publish-to-gitlab/action.yml +++ b/actions/publish-to-gitlab/action.yml @@ -1,92 +1,80 @@ -name: 'Publish to GitLab Package Registry' +name: 'Publish to GitLab PyPI Package Registry' description: > - Uploads one or more build artifacts from a GitHub Actions runner to a - GitLab Generic Package Registry endpoint using the GitLab REST API. + Uploads Python distributions (wheels and/or sdists) from a GitHub Actions + runner to a GitLab PyPI Package Registry endpoint via twine. Packages + uploaded here are pip-discoverable through the /packages/pypi/simple + index — unlike the Generic Package Registry, which cannot be consumed + by pip. inputs: # ── Required ──────────────────────────────────────────────────────────────── - gitlab-token: - description: > - GitLab authentication token. Use a Personal Access Token (scope: api) - or a Deploy Token (scope: write_package_registry). - Store this value as a GitHub Actions secret and pass it in; never - hard-code it in a workflow file. - required: true - - gitlab-project-id: + gitlab-username: description: > - Numeric ID of the target GitLab project (found under - Settings → General → Project ID). + Username for basic auth against the GitLab PyPI endpoint. For a + deploy token this is the token name (e.g. + "gitlab+deploy-token-12345"). For a personal access token use + "__token__" (or any placeholder). required: true - package-name: + gitlab-token: description: > - Name of the package to create or update in the registry. - Allowed characters: a-z, A-Z, 0-9, dot (.), hyphen (-), underscore (_). + GitLab authentication token used as the password for basic auth. + Use a Personal Access Token (scope: api) or a Deploy Token (scope: + write_package_registry). Store as a GitHub Actions secret; never + hard-code. required: true - package-version: + gitlab-project-id: description: > - Semantic version string for this package release, e.g. "1.2.3" or - "1.0.0-beta.1". Tip: pass github.ref_name for tag-triggered workflows. + Numeric ID of the target GitLab project (Settings → General → + Project ID). required: true # files is a newline-separated list of glob patterns resolved by the runner. files: description: > - Newline-separated list of file paths (or globs) to upload. - Each matched file is uploaded as a separate package file. - Example: + Newline-separated list of file paths (or globs) to upload. Each + matched file is uploaded as a separate PyPI distribution. Example: files: | - dist/my-app-linux-amd64 - dist/my-app-darwin-arm64 - checksums.txt + dist/*.whl + dist/*.tar.gz required: true # ── Optional ──────────────────────────────────────────────────────────────── gitlab-host: description: > - Base URL of your GitLab instance. Override for self-hosted GitLab. + Base URL of the GitLab instance. Override for self-hosted GitLab. required: false default: 'https://gitlab.com' - token-type: - description: > - The type of token supplied in gitlab-token. - Accepted values: "private-token" (Personal / Project Access Token) or - "deploy-token". - required: false - default: 'private-token' - - status: + skip-existing: description: > - Package status after upload. Use "default" to make the package visible - in the UI, or "hidden" to suppress it from listings (useful for - draft/pre-release packages). + When "true", pass --skip-existing to twine so files that already + exist in the registry are silently skipped instead of failing the + upload. Default "false". required: false - default: 'default' + default: 'false' - fail-on-duplicate: + twine-version: description: > - When "true", the action exits with an error if a file with the same - name already exists in the specified package version. When "false" - (default), the existing file is silently overwritten. + Pin a specific twine version for reproducibility. Passed to + `uvx --from twine==`. Leave empty to use the latest. required: false - default: 'false' + default: '' outputs: - package-url: + registry-url: description: > - URL of the package in the GitLab Package Registry UI. + URL of the PyPI package listing in the GitLab UI. value: >- - ${{ inputs.gitlab-host }}/-/packages?search[]=${{ inputs.package-name }} + ${{ inputs.gitlab-host }}/-/packages uploaded-files: - description: Newline-separated list of filenames that were successfully uploaded. + description: Newline-separated list of files that twine successfully uploaded. value: ${{ steps.upload.outputs.uploaded_files }} runs: @@ -101,6 +89,11 @@ runs: error=0 + if [[ -z "${{ inputs.gitlab-username }}" ]]; then + echo "::error::Input 'gitlab-username' must not be empty." + error=1 + fi + if [[ -z "${{ inputs.gitlab-token }}" ]]; then echo "::error::Input 'gitlab-token' must not be empty." error=1 @@ -111,25 +104,9 @@ runs: error=1 fi - if [[ ! "${{ inputs.package-name }}" =~ ^[a-zA-Z0-9._-]+$ ]]; then - echo "::error::Input 'package-name' contains invalid characters. Allowed: a-z A-Z 0-9 . - _" - error=1 - fi - - if [[ -z "${{ inputs.package-version }}" ]]; then - echo "::error::Input 'package-version' must not be empty." - error=1 - fi - - TOKEN_TYPE="${{ inputs.token-type }}" - if [[ "$TOKEN_TYPE" != "private-token" && "$TOKEN_TYPE" != "deploy-token" ]]; then - echo "::error::Input 'token-type' must be 'private-token' or 'deploy-token'. Got: $TOKEN_TYPE" - error=1 - fi - - STATUS="${{ inputs.status }}" - if [[ "$STATUS" != "default" && "$STATUS" != "hidden" ]]; then - echo "::error::Input 'status' must be 'default' or 'hidden'. Got: $STATUS" + SKIP_EXISTING="${{ inputs.skip-existing }}" + if [[ "$SKIP_EXISTING" != "true" && "$SKIP_EXISTING" != "false" ]]; then + echo "::error::Input 'skip-existing' must be 'true' or 'false'. Got: $SKIP_EXISTING" error=1 fi @@ -141,112 +118,87 @@ runs: echo "All inputs validated successfully." echo "::endgroup::" - # ── 2. Resolve globs and upload files ──────────────────────────────────── + # ── 2. Ensure uv is available (provides uvx for one-shot twine runs) ───── + - name: Set up uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: false + + # ── 3. Resolve globs and upload via twine ──────────────────────────────── - name: Upload files id: upload shell: bash env: - GITLAB_TOKEN: ${{ inputs.gitlab-token }} + TWINE_USERNAME: ${{ inputs.gitlab-username }} + TWINE_PASSWORD: ${{ inputs.gitlab-token }} + TWINE_REPOSITORY_URL: ${{ inputs.gitlab-host }}/api/v4/projects/${{ inputs.gitlab-project-id }}/packages/pypi + TWINE_NON_INTERACTIVE: '1' run: | - echo "::group::Uploading files to GitLab Package Registry" + echo "::group::Resolving file globs" - GITLAB_HOST="${{ inputs.gitlab-host }}" - PROJECT_ID="${{ inputs.gitlab-project-id }}" - PKG_NAME="${{ inputs.package-name }}" - PKG_VERSION="${{ inputs.package-version }}" - TOKEN_TYPE="${{ inputs.token-type }}" - STATUS="${{ inputs.status }}" - FAIL_ON_DUPLICATE="${{ inputs.fail-on-duplicate }}" + matched_files=() - API_BASE="${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/packages/generic/${PKG_NAME}/${PKG_VERSION}" - - uploaded_files=() - failed_files=() - - # Read the newline-separated file list and expand globs while IFS= read -r pattern; do - # Skip blank lines [[ -z "$pattern" ]] && continue - # Expand glob; if no match, the literal string is returned — catch that - matched=( $pattern ) - if [[ ${#matched[@]} -eq 0 ]] || [[ ! -e "${matched[0]}" ]]; then + expanded=( $pattern ) + if [[ ${#expanded[@]} -eq 0 ]] || [[ ! -e "${expanded[0]}" ]]; then echo "::warning::Pattern '${pattern}' did not match any files — skipping." continue fi - for filepath in "${matched[@]}"; do + for filepath in "${expanded[@]}"; do if [[ ! -f "$filepath" ]]; then echo "::warning::'${filepath}' is not a regular file — skipping." continue fi - - filename=$(basename "$filepath") - url="${API_BASE}/${filename}?status=${STATUS}" - - echo "Uploading: ${filepath} → ${url}" - - http_status=$(curl \ - --silent \ - --output /tmp/gitlab_upload_response.txt \ - --write-out "%{http_code}" \ - --location \ - --header "${TOKEN_TYPE}: ${GITLAB_TOKEN}" \ - --upload-file "${filepath}" \ - "${url}") - - response_body=$(cat /tmp/gitlab_upload_response.txt) - - if [[ "$http_status" == "201" ]]; then - echo " ✓ Uploaded successfully (HTTP 201)." - uploaded_files+=("$filename") - - elif [[ "$http_status" == "200" ]]; then - if [[ "$FAIL_ON_DUPLICATE" == "true" ]]; then - echo "::error::File '${filename}' already exists in ${PKG_NAME}@${PKG_VERSION} and fail-on-duplicate is true." - failed_files+=("$filename") - else - echo " ✓ File already existed and was overwritten (HTTP 200)." - uploaded_files+=("$filename") - fi - - else - echo "::error::Failed to upload '${filename}'. HTTP ${http_status}: ${response_body}" - failed_files+=("$filename") - fi + matched_files+=("$filepath") done done <<< "${{ inputs.files }}" - # Emit output - uploaded_list=$(printf '%s\n' "${uploaded_files[@]}") - echo "uploaded_files<> "$GITHUB_OUTPUT" - echo "$uploaded_list" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" + if [[ ${#matched_files[@]} -eq 0 ]]; then + echo "::error::No files matched the provided globs; nothing to upload." + echo "::endgroup::" + exit 1 + fi - echo "" - echo "── Summary ──────────────────────────────────────────────────" - echo " Uploaded : ${#uploaded_files[@]} file(s)" - echo " Failed : ${#failed_files[@]} file(s)" - echo "─────────────────────────────────────────────────────────────" + printf 'Matched %d file(s):\n' "${#matched_files[@]}" + printf ' %s\n' "${matched_files[@]}" echo "::endgroup::" + echo "::group::Uploading via twine" - if [[ ${#failed_files[@]} -gt 0 ]]; then - echo "::error::${#failed_files[@]} file(s) failed to upload: ${failed_files[*]}" - exit 1 + twine_from='twine' + if [[ -n "${{ inputs.twine-version }}" ]]; then + twine_from="twine==${{ inputs.twine-version }}" + fi + + skip_flag='' + if [[ "${{ inputs.skip-existing }}" == 'true' ]]; then + skip_flag='--skip-existing' fi - # ── 3. Annotate the workflow run with the registry URL ─────────────────── + uvx --from "$twine_from" twine upload \ + --disable-progress-bar \ + $skip_flag \ + "${matched_files[@]}" + + echo "::endgroup::" + + # Emit uploaded_files output + { + echo "uploaded_files<> "$GITHUB_OUTPUT" + + # ── 4. Annotate the workflow run summary ───────────────────────────────── - name: Print registry URL shell: bash run: | - echo "### GitLab Package Registry" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Package \`${{ inputs.package-name }}@${{ inputs.package-version }}\` published." >> "$GITHUB_STEP_SUMMARY" + echo "### GitLab PyPI Package Registry" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "**Registry:** ${{ inputs.gitlab-host }}/$( \ - echo '${{ inputs.gitlab-host }}' | sed 's|https://||' \ - )" >> "$GITHUB_STEP_SUMMARY" + echo "**Registry endpoint:** \`${{ inputs.gitlab-host }}/api/v4/projects/${{ inputs.gitlab-project-id }}/packages/pypi\`" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "**Uploaded files:**" >> "$GITHUB_STEP_SUMMARY" while IFS= read -r f; do From cb2a039af93af81b1b43a511eded5c4f2eab773d Mon Sep 17 00:00:00 2001 From: Trevor Gamblin Date: Tue, 30 Jun 2026 22:17:47 -0400 Subject: [PATCH 2/2] build-numpy: use publish-to-gitlab with username Signed-off-by: Trevor Gamblin --- .github/workflows/build-numpy.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-numpy.yml b/.github/workflows/build-numpy.yml index ca34253..661b34f 100644 --- a/.github/workflows/build-numpy.yml +++ b/.github/workflows/build-numpy.yml @@ -123,13 +123,11 @@ jobs: path: dist merge-multiple: true - - name: Publish to GitLab Package Registry + - name: Publish to GitLab PyPI registry uses: ./python-wheels-repo/actions/publish-to-gitlab with: + gitlab-username: ${{ vars.GITLAB_DEPLOY_USER }} gitlab-token: ${{ secrets.GITLAB_DEPLOY_TOKEN }} - token-type: deploy-token gitlab-project-id: ${{ vars.GITLAB_PROJECT_ID }} - package-name: numpy - package-version: ${{ env.NUMPY_VERSION }} files: | dist/*.whl