diff --git a/.github/workflows/test-update-release-channel.yml b/.github/workflows/test-update-release-channel.yml index 3a23f1f3..839498d5 100644 --- a/.github/workflows/test-update-release-channel.yml +++ b/.github/workflows/test-update-release-channel.yml @@ -13,9 +13,11 @@ jobs: test-urc-happy-path: runs-on: sonar-xs permissions: + id-token: write contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./config-npm - name: Update release channel (dry-run, happy path) id: urc uses: ./update-release-channel diff --git a/.shellspec b/.shellspec index 149a2dfd..2009ff90 100644 --- a/.shellspec +++ b/.shellspec @@ -1,5 +1,5 @@ # kcov (coverage) options ---kcov-options "--include-pattern=build-poetry,get-build-number,pr_cleanup,promote,build-gradle,config-maven,build-maven,config-npm,build-npm,build-yarn,shared,config-gradle,config-pip,update-release-channel" +--kcov-options "--include-pattern=build-poetry,config-uv,get-build-number,pr_cleanup,promote,build-gradle,config-maven,build-maven,config-npm,build-npm,build-yarn,shared,config-gradle,config-pip,update-release-channel" # --kcov-options "--exclude-pattern=.github,.idea,.git" # define minimum coverage (fail otherwise) diff --git a/README.md b/README.md index 9c7d0873..69f61ed6 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ These badges show the status of workflows in dummy repositories that use (or sho - [`build-npm`](#build-npm) - [`build-yarn`](#build-yarn) - [`config-pip`](#config-pip) +- [`config-uv`](#config-uv) - [`promote`](#promote) - [`pr_cleanup`](#pr_cleanup) - [`code-signing`](#code-signing) @@ -1180,6 +1181,89 @@ Both actions produce the same configuration and are functionally equivalent. --- +## `config-uv` + +Configure uv build environment with build number, JFrog CLI authentication, and caching. + +This action configures `jf config` for the Repox server. +It also sets native `UV_INDEX_*` credentials so uv resolves dependencies from Artifactory instead of PyPI. + +There is no `jf uv-config` command. Configure indexes in `pyproject.toml` and run `jf uv` subcommands. + +See the [JFrog uv documentation](https://docs.jfrog.com/artifactory/docs/jf-uv). + +Repositories using uv should declare the Repox index in `pyproject.toml`: + +```toml +[[tool.uv.index]] +name = "repox" +url = "https://repox.jfrog.io/artifactory/api/pypi/sonarsource-pypi/simple" +default = true +``` + +> **Note:** This action automatically calls [`get-build-number`](#get-build-number) to manage the build number. + +### Requirements + +#### Required GitHub Permissions + +- `id-token: write` +- `contents: write` + +#### Required Vault Permissions + +- `public-reader` or `private-reader`: Artifactory role for reading dependencies + +#### Other Dependencies + +The `uv` tool must be pre-installed. Use of `mise` is recommended. + +### Usage + +```yaml +permissions: + id-token: write + contents: write +steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 + - uses: SonarSource/ci-github-actions/config-uv@v1 + - run: jf uv sync +``` + +For build-info collection, pass `--build-name` and `--build-number` to `jf uv` and publish with `jf rt build-publish`. + +### Inputs + +| Input | Description | Default | +|---------------------------|-----------------------------------------------------------------------------|----------------------------------------------------------------------| +| `working-directory` | Relative path under github.workspace to execute the build in | `.` | +| `artifactory-reader-role` | Suffix for the Artifactory reader role in Vault | `private-reader` for private repos, `public-reader` for public repos | +| `uv-index-name` | Name of the uv index in `pyproject.toml` to authenticate | `repox` | +| `repox-url` | URL for Repox | `https://repox.jfrog.io` | +| `uv-cache-dir` | Path to the uv cache directory, relative to GitHub workspace | `.cache/uv` | +| `disable-caching` | Whether to disable uv caching entirely | `false` | + +### Outputs + +| Output | Description | +|----------------|---------------------------------------------------------------------------| +| `BUILD_NUMBER` | The current build number. Also set as environment variable `BUILD_NUMBER` | + +### Output Environment Variables + +| Environment Variable | Description | +|---------------------------------|--------------------------| +| `BUILD_NUMBER` | The current build number | +| `UV_INDEX_REPOX_USERNAME` | Repox username for uv (`repox` index name) | +| `UV_INDEX_REPOX_PASSWORD` | Repox access token for uv | +| `UV_KEYRING_PROVIDER` | Set to `disabled` when index credentials are injected | +| `UV_CACHE_DIR` | Path to the uv cache directory | + +See also [`get-build-number`](#get-build-number) output environment variables. + +--- + ## `promote` This action promotes a build in JFrog Artifactory and updates the GitHub status check accordingly. diff --git a/config-uv/action.yml b/config-uv/action.yml new file mode 100644 index 00000000..efb1b2fa --- /dev/null +++ b/config-uv/action.yml @@ -0,0 +1,143 @@ +--- +name: Config uv +description: GitHub Action to configure uv build environment with build number, Artifactory authentication, and caching +inputs: + working-directory: + description: Relative path under github.workspace to execute the build in + default: . + artifactory-reader-role: + description: Suffix for the Artifactory reader role in Vault. Defaults to `private-reader` for private repositories, and `public-reader` + for public repositories. + default: '' + uv-index-name: + description: Name of the uv index in pyproject.toml to authenticate (e.g. repox for `[[tool.uv.index]]` with `name = "repox"`) + default: repox + repox-url: + description: URL for Repox + default: https://repox.jfrog.io + uv-cache-dir: + description: Path to the uv cache directory, relative to GitHub workspace + default: .cache/uv + disable-caching: + description: Whether to disable uv caching entirely + default: 'false' + host-actions-root: + description: Path to the actions folder on the host (used when called from another local action) + default: '' + +outputs: + BUILD_NUMBER: + description: The current build number. Also set as environment variable BUILD_NUMBER + value: ${{ steps.get-build-number.outputs.BUILD_NUMBER }} + +runs: + using: composite + steps: + - id: config-uv-completed + if: env.CONFIG_UV_COMPLETED != '' + shell: bash + run: | + echo "Action already called by $CONFIG_UV_COMPLETED, execution skipped." + echo "skip=true" >> "$GITHUB_OUTPUT" + + - name: Set local action paths + id: set-path + if: steps.config-uv-completed.outputs.skip != 'true' + shell: bash + run: | + echo "::group::Fix for using local actions" + echo "GITHUB_ACTION_PATH=$GITHUB_ACTION_PATH" + echo "github.action_path=${{ github.action_path }}" + ACTION_PATH_CONFIG_UV="${{ github.action_path }}" + host_actions_root="${{ inputs.host-actions-root }}" + if [[ -z "$host_actions_root" ]]; then + host_actions_root="$(dirname "$ACTION_PATH_CONFIG_UV")" + else + ACTION_PATH_CONFIG_UV="$host_actions_root/config-uv" + fi + echo "ACTION_PATH_CONFIG_UV=$ACTION_PATH_CONFIG_UV" + echo "ACTION_PATH_CONFIG_UV=$ACTION_PATH_CONFIG_UV" >> "$GITHUB_ENV" + echo "host_actions_root=$host_actions_root" >> "$GITHUB_OUTPUT" + + mkdir -p ".actions" + ln -sf "$host_actions_root/get-build-number" .actions/get-build-number + ln -sf "$host_actions_root/shared" .actions/shared + ls -la .actions/* + echo "::endgroup::" + + echo "::group::Backup mise files to configure uv without interference" + mise_backup=$(mktemp -d) + echo "MISE_BACKUP=$mise_backup" >> "$GITHUB_OUTPUT" + mv mise.* .mise.* mise/ .mise/ .tool-versions "$mise_backup/" 2>/dev/null || true + cp "$ACTION_PATH_CONFIG_UV/mise.local.toml" mise.local.toml + echo "::endgroup::" + + - uses: ./.actions/get-build-number + id: get-build-number + if: steps.config-uv-completed.outputs.skip != 'true' + with: + host-actions-root: ${{ steps.set-path.outputs.host_actions_root }} + + - name: Set Artifactory reader role + if: steps.config-uv-completed.outputs.skip != 'true' + shell: bash + env: + ARTIFACTORY_READER_ROLE: + ${{ inputs.artifactory-reader-role != '' && inputs.artifactory-reader-role || + (github.event.repository.visibility == 'public' && 'public-reader' || 'private-reader') }} + run: | + echo "ARTIFACTORY_READER_ROLE=${ARTIFACTORY_READER_ROLE}" >> "$GITHUB_ENV" + + - name: Cache uv dependencies + uses: SonarSource/gh-action_cache@a7d13cdd1c9f097a5f8382ccec463be2831e3dbc # v1.6.0 + if: steps.config-uv-completed.outputs.skip != 'true' && inputs.disable-caching == 'false' + with: + path: ${{ github.workspace }}/${{ inputs.uv-cache-dir }} + key: uv-${{ runner.os }}-${{ hashFiles(format('{0}/uv.lock', inputs.working-directory), + format('{0}/pyproject.toml', inputs.working-directory)) }} + restore-keys: uv-${{ runner.os }}- + + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 + if: steps.config-uv-completed.outputs.skip != 'true' + with: + version: 2026.5.9 + + - uses: SonarSource/vault-action-wrapper@0a3114fe1230b784c35b53b099f9ab1f1e538cc7 # 3.5.0 + if: steps.config-uv-completed.outputs.skip != 'true' + id: secrets + with: + url: ${{ contains(inputs.repox-url, 'dev.sonar.build') && 'https://vault.dev.sonar.build' || 'https://vault.sonar.build' }} + secrets: | + development/artifactory/token/{REPO_OWNER_NAME_DASH}-${{ env.ARTIFACTORY_READER_ROLE }} username | ARTIFACTORY_USERNAME; + development/artifactory/token/{REPO_OWNER_NAME_DASH}-${{ env.ARTIFACTORY_READER_ROLE }} access_token | ARTIFACTORY_ACCESS_TOKEN; + + - name: Configure uv authentication + if: steps.config-uv-completed.outputs.skip != 'true' + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_USERNAME: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_USERNAME }} + ARTIFACTORY_ACCESS_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_ACCESS_TOKEN }} + UV_INDEX_NAME: ${{ inputs.uv-index-name }} + UV_CACHE_DIR: ${{ github.workspace }}/${{ inputs.uv-cache-dir }} + run: | + $ACTION_PATH_CONFIG_UV/uv_config.sh + + echo "::group::Restore mise files" + rm mise.local.toml + mv "${{ steps.set-path.outputs.MISE_BACKUP }}"/* "${{ steps.set-path.outputs.MISE_BACKUP }}"/.* ./ 2>/dev/null || true + rmdir "${{ steps.set-path.outputs.MISE_BACKUP }}" + echo "::endgroup::" + + - name: Set Config uv completed + if: steps.config-uv-completed.outputs.skip != 'true' + shell: bash + run: echo "CONFIG_UV_COMPLETED=$GITHUB_ACTION" >> "$GITHUB_ENV" + + - name: Clean up local action symlinks + if: always() && steps.config-uv-completed.outputs.skip != 'true' + shell: bash + run: | + rm -f .actions/get-build-number .actions/shared + rmdir .actions 2>/dev/null || true diff --git a/config-uv/mise.local.toml b/config-uv/mise.local.toml new file mode 100644 index 00000000..7c5a49cc --- /dev/null +++ b/config-uv/mise.local.toml @@ -0,0 +1,6 @@ +[tools] +jfrog-cli = "2.96.0" + +[env] +JFROG_CLI_AVOID_NEW_VERSION_WARNING = "true" +JFROG_CLI_ENV_EXCLUDE = "*password*;*secret*;*key*;*token*;*auth*;*credential*" diff --git a/config-uv/uv_config.sh b/config-uv/uv_config.sh new file mode 100755 index 00000000..6442fd9c --- /dev/null +++ b/config-uv/uv_config.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Config script for uv to use SonarSource Artifactory via JFrog CLI. +# +# There is no `jf uv-config` command. Configure `jf config` and declare +# `[[tool.uv.index]]` entries in pyproject.toml, then run `jf uv` subcommands. +# See https://docs.jfrog.com/artifactory/docs/jf-uv +# +# Required environment variables (must be explicitly provided): +# - ARTIFACTORY_URL: URL to Artifactory repository +# - ARTIFACTORY_USERNAME: Username for Artifactory authentication +# - ARTIFACTORY_ACCESS_TOKEN: Access token to read Repox repositories +# - UV_INDEX_NAME: Name of the uv index in pyproject.toml (e.g. repox) +# +# Optional environment variables: +# - UV_CACHE_DIR: Path to the uv cache directory +# +# GitHub Actions auto-provided: +# - GITHUB_ENV: Path to GitHub Actions environment file + +set -euo pipefail + +: "${ARTIFACTORY_URL:?}" "${ARTIFACTORY_USERNAME:?}" "${ARTIFACTORY_ACCESS_TOKEN:?}" "${UV_INDEX_NAME:?}" "${GITHUB_ENV:?}" + +uv_index_env_suffix() { + local index_name_upper + + index_name_upper=$(echo "$UV_INDEX_NAME" | tr '[:lower:]' '[:upper:]') + index_name_upper=$(echo "$index_name_upper" | tr -c 'A-Za-z0-9' '_' | sed 's/_*$//') + echo "$index_name_upper" +} + +configure_uv_repox() { + local index_name_upper + + echo "Configuring uv to use Artifactory via JFrog CLI..." + + jf config remove repox > /dev/null 2>&1 || true # Ignore inexistent configuration + jf config add repox --url "${ARTIFACTORY_URL%/artifactory*}" --artifactory-url "$ARTIFACTORY_URL" --access-token "$ARTIFACTORY_ACCESS_TOKEN" + jf config use repox + + index_name_upper=$(uv_index_env_suffix) + + # Native uv variables for named indexes. Also set explicitly so plain `uv` works; + # `jf uv` injects the same variables when they are unset. + { + echo "UV_INDEX_${index_name_upper}_USERNAME=$ARTIFACTORY_USERNAME" + echo "UV_INDEX_${index_name_upper}_PASSWORD=$ARTIFACTORY_ACCESS_TOKEN" + echo "UV_KEYRING_PROVIDER=disabled" + } >> "$GITHUB_ENV" + + if [[ -n "${UV_CACHE_DIR:-}" ]]; then + echo "UV_CACHE_DIR=$UV_CACHE_DIR" >> "$GITHUB_ENV" + mkdir -p "$UV_CACHE_DIR" + fi + + return 0 +} + +main() { + echo "::group::Configure uv" + configure_uv_repox + echo "::endgroup::" + return 0 +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main +fi diff --git a/spec/config-uv_spec.sh b/spec/config-uv_spec.sh new file mode 100644 index 00000000..cb00b406 --- /dev/null +++ b/spec/config-uv_spec.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +eval "$(shellspec - -c) exit 1" + +Mock jf + echo "jf $*" +End + +export GITHUB_REPOSITORY="my-org/test-project" +export GITHUB_ENV=/dev/null +export ARTIFACTORY_URL="https://repox.jfrog.io/artifactory" +export ARTIFACTORY_USERNAME="test-user" +export ARTIFACTORY_ACCESS_TOKEN="test-token" +export UV_INDEX_NAME="repox" + +MESSAGE_CONFIGURING_UV="Configuring uv to use Artifactory via JFrog CLI..." + +Describe 'config-uv/uv_config.sh' + It 'does not run main when sourced' + When run source config-uv/uv_config.sh + The status should be success + The lines of output should equal 0 + The lines of error should equal 0 + End +End + +Include config-uv/uv_config.sh + +Describe 'configure_uv_repox()' + It 'configures JFrog CLI and uv index authentication' + GITHUB_ENV=$(mktemp) + export GITHUB_ENV + When call configure_uv_repox + The status should be success + The line 1 should equal "$MESSAGE_CONFIGURING_UV" + The line 2 should include "jf config add repox" + The line 3 should include "jf config use repox" + The contents of file "$GITHUB_ENV" should include "UV_INDEX_REPOX_USERNAME=test-user" + The contents of file "$GITHUB_ENV" should include "UV_INDEX_REPOX_PASSWORD=test-token" + The contents of file "$GITHUB_ENV" should include "UV_KEYRING_PROVIDER=disabled" + End + + It 'sets UV_CACHE_DIR when provided' + GITHUB_ENV=$(mktemp) + UV_CACHE_DIR=$(mktemp -d) + export GITHUB_ENV UV_CACHE_DIR + When call configure_uv_repox + The status should be success + The line 1 should equal "$MESSAGE_CONFIGURING_UV" + The contents of file "$GITHUB_ENV" should include "UV_CACHE_DIR=$UV_CACHE_DIR" + The path "$UV_CACHE_DIR" should be directory + End + + It 'uppercases custom index names for environment variables' + GITHUB_ENV=$(mktemp) + export GITHUB_ENV + export UV_INDEX_NAME="pypi-virtual" + When call configure_uv_repox + The status should be success + The line 1 should equal "$MESSAGE_CONFIGURING_UV" + The contents of file "$GITHUB_ENV" should include "UV_INDEX_PYPI_VIRTUAL_USERNAME=test-user" + The contents of file "$GITHUB_ENV" should include "UV_INDEX_PYPI_VIRTUAL_PASSWORD=test-token" + End +End + +Describe 'main()' + It 'runs configure_uv_repox within a GitHub Actions group' + GITHUB_ENV=$(mktemp) + export GITHUB_ENV + When run script config-uv/uv_config.sh + The status should be success + The line 1 should equal "::group::Configure uv" + The line 2 should equal "$MESSAGE_CONFIGURING_UV" + The line 3 should include "jf config add repox" + The line 4 should include "jf config use repox" + The line 5 should equal "::endgroup::" + The contents of file "$GITHUB_ENV" should include "UV_INDEX_REPOX_USERNAME=test-user" + The contents of file "$GITHUB_ENV" should include "UV_INDEX_REPOX_PASSWORD=test-token" + The contents of file "$GITHUB_ENV" should include "UV_KEYRING_PROVIDER=disabled" + End +End