From 36c57ffb09367c96e229f31cbe91a0659b81b52e Mon Sep 17 00:00:00 2001 From: Trevor Gamblin Date: Tue, 30 Jun 2026 16:05:27 -0400 Subject: [PATCH 1/2] 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 --- actions/publish-to-gitlab/action.yml | 254 +++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 actions/publish-to-gitlab/action.yml diff --git a/actions/publish-to-gitlab/action.yml b/actions/publish-to-gitlab/action.yml new file mode 100644 index 0000000..40cc6e3 --- /dev/null +++ b/actions/publish-to-gitlab/action.yml @@ -0,0 +1,254 @@ +name: 'Publish to GitLab 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. + +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: + description: > + Numeric ID of the target GitLab project (found under + Settings → General → Project ID). + required: true + + package-name: + description: > + Name of the package to create or update in the registry. + Allowed characters: a-z, A-Z, 0-9, dot (.), hyphen (-), underscore (_). + required: true + + package-version: + 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. + 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: + files: | + dist/my-app-linux-amd64 + dist/my-app-darwin-arm64 + checksums.txt + required: true + + # ── Optional ──────────────────────────────────────────────────────────────── + + gitlab-host: + description: > + Base URL of your 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: + 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). + required: false + default: 'default' + + fail-on-duplicate: + 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. + required: false + default: 'false' + +outputs: + + package-url: + description: > + URL of the package in the GitLab Package Registry UI. + value: >- + ${{ inputs.gitlab-host }}/-/packages?search[]=${{ inputs.package-name }} + + uploaded-files: + description: Newline-separated list of filenames that were successfully uploaded. + value: ${{ steps.upload.outputs.uploaded_files }} + +runs: + using: 'composite' + steps: + + # ── 1. Validate inputs ─────────────────────────────────────────────────── + - name: Validate inputs + shell: bash + run: | + echo "::group::Validating inputs" + + error=0 + + if [[ -z "${{ inputs.gitlab-token }}" ]]; then + echo "::error::Input 'gitlab-token' must not be empty." + error=1 + fi + + if [[ ! "${{ inputs.gitlab-project-id }}" =~ ^[0-9]+$ ]]; then + echo "::error::Input 'gitlab-project-id' must be a numeric project ID." + 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" + error=1 + fi + + if [[ $error -eq 1 ]]; then + echo "::endgroup::" + exit 1 + fi + + echo "All inputs validated successfully." + echo "::endgroup::" + + # ── 2. Resolve globs and upload files ──────────────────────────────────── + - name: Upload files + id: upload + shell: bash + env: + GITLAB_TOKEN: ${{ inputs.gitlab-token }} + run: | + echo "::group::Uploading files to GitLab Package Registry" + + 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 }}" + + 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 + echo "::warning::Pattern '${pattern}' did not match any files — skipping." + continue + fi + + for filepath in "${matched[@]}"; 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 + 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" + + echo "" + echo "── Summary ──────────────────────────────────────────────────" + echo " Uploaded : ${#uploaded_files[@]} file(s)" + echo " Failed : ${#failed_files[@]} file(s)" + echo "─────────────────────────────────────────────────────────────" + + echo "::endgroup::" + + if [[ ${#failed_files[@]} -gt 0 ]]; then + echo "::error::${#failed_files[@]} file(s) failed to upload: ${failed_files[*]}" + exit 1 + fi + + # ── 3. Annotate the workflow run with the registry URL ─────────────────── + - 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 "" >> "$GITHUB_STEP_SUMMARY" + echo "**Registry:** ${{ inputs.gitlab-host }}/$( \ + echo '${{ inputs.gitlab-host }}' | sed 's|https://||' \ + )" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "**Uploaded files:**" >> "$GITHUB_STEP_SUMMARY" + while IFS= read -r f; do + [[ -n "$f" ]] && echo "- \`$f\`" >> "$GITHUB_STEP_SUMMARY" + done <<< "${{ steps.upload.outputs.uploaded_files }}" From 449e2581e8762f06b7bb3bab4689408acf5ae359 Mon Sep 17 00:00:00 2001 From: Trevor Gamblin Date: Tue, 30 Jun 2026 16:18:51 -0400 Subject: [PATCH 2/2] numpy: add riscv64 workflow Upstream numpy has started building riscv64 wheels, but they are not yet deploying them. In the meantime, we still need to support builds and publish them to the RISE registry. Use the custom publish-to-gitlab action in the publish step, since the registry is on GitLab. Unlike upstream, we build for Python 3.12, 3.13, 3.14, and 3.14t (3.11 is no longer supported as of NumPy 2.5.0). This is consistent with how RISE has been building for a broader range of versions. This workflow keeps the general structure of the upstream project, but also checks out the python-wheels repository at 'python-wheels-repo' so that we can use publish-to-gitlab when deploying. Signed-off-by: Trevor Gamblin --- .github/workflows/build-numpy.yml | 135 ++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 .github/workflows/build-numpy.yml diff --git a/.github/workflows/build-numpy.yml b/.github/workflows/build-numpy.yml new file mode 100644 index 0000000..ca34253 --- /dev/null +++ b/.github/workflows/build-numpy.yml @@ -0,0 +1,135 @@ +--- +name: Build numpy wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'numpy version to build (git tag without leading v, e.g. 2.5.0)' + required: true + default: '2.5.0' + pull_request: + paths: + - '.github/workflows/build-numpy.yml' + - 'actions/publish-to-gitlab/**' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '2.5.0' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # `inputs.version` is empty on pull_request events; default to 2.5.0 there. + NUMPY_VERSION: ${{ inputs.version || '2.5.0' }} + +jobs: + build_wheels: + name: Build numpy ${{ inputs.version || '2.5.0' }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + strategy: + fail-fast: false + matrix: + python: ["cp312", "cp313", "cp314", "cp314t"] + + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_BASEDIR: "/project" + CCACHE_COMPILERCHECK: "content" + + steps: + # Layout note: numpy is checked out at the workspace root (not under a + # subdir) so cibuildwheel's `{project}` template substitution resolves + # to the numpy source tree. numpy's pyproject.toml uses + # `{project}/tools/wheels/cibw_before_build.sh`, which only works when + # CWD == numpy root at cibuildwheel invocation time. python-wheels is + # placed under `python-wheels-repo/` to free the workspace root for numpy. + - name: Checkout python-wheels + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: python-wheels-repo + persist-credentials: false + + - name: Checkout numpy v${{ env.NUMPY_VERSION }} + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: numpy/numpy + ref: v${{ env.NUMPY_VERSION }} + submodules: true + persist-credentials: false + + - name: Restore compilation cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: ccache-restore + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-wheels-numpy-v${{ env.NUMPY_VERSION }}-manylinux_riscv64-${{ matrix.python }}-${{ github.run_id }} + restore-keys: | + ccache-wheels-numpy-v${{ env.NUMPY_VERSION }}-manylinux_riscv64-${{ matrix.python }}- + ccache-wheels-numpy-manylinux_riscv64-${{ matrix.python }}- + + - name: Build wheels + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 + env: + CIBW_BUILD: ${{ matrix.python }}-manylinux_riscv64 + CIBW_BEFORE_ALL_LINUX: | + set -eux + CCACHE_VERSION=4.13.6 + curl -fsSL https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/ccache-${CCACHE_VERSION}-linux-$(uname -m)-musl-static.tar.gz | \ + tar -xvzf - --strip-components 1 -C /usr/local/bin ccache-${CCACHE_VERSION}-linux-$(uname -m)-musl-static/ccache + ccache --version + ccache --zero-stats + CIBW_ENVIRONMENT_PASS_LINUX: >- + CCACHE_BASEDIR + CCACHE_COMPILERCHECK + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume ${{ env.CCACHE_DIR }}:/root/.ccache" + + - name: Save compilation cache + if: always() && github.ref == 'refs/heads/main' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-wheels-numpy-v${{ env.NUMPY_VERSION }}-manylinux_riscv64-${{ matrix.python }}-${{ github.run_id }} + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: numpy-${{ env.NUMPY_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish numpy ${{ inputs.version || '2.5.0' }} to GitLab + needs: build_wheels + # Only publish when the workflow was triggered from main with a specific + # version. Manual trigger is the only entry point, so checking the ref is + # enough to gate uploads. + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04-riscv + permissions: + contents: read + + steps: + - name: Checkout python-wheels + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: python-wheels-repo + persist-credentials: false + + - name: Download wheels + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: numpy-${{ env.NUMPY_VERSION }}-*-manylinux_riscv64 + path: dist + merge-multiple: true + + - name: Publish to GitLab Package Registry + uses: ./python-wheels-repo/actions/publish-to-gitlab + with: + 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