Skip to content

BUILD-11568 Add config-uv action#293

Draft
hedinasr wants to merge 3 commits into
masterfrom
feat/hnasr/BUILD-11568-createConfigUv
Draft

BUILD-11568 Add config-uv action#293
hedinasr wants to merge 3 commits into
masterfrom
feat/hnasr/BUILD-11568-createConfigUv

Conversation

@hedinasr

Copy link
Copy Markdown
Contributor

Summary

  • Add config-uv composite action to fetch Artifactory reader credentials from Vault and expose UV_INDEX_* authentication for the Repox PyPI index
  • Cache uv dependencies under .cache/uv keyed on uv.lock and pyproject.toml
  • Add shellspec coverage and README documentation

Test plan

  • shellspec spec/config-uv_spec.sh passes locally (5 examples)
  • Pre-commit hooks pass (yamllint, shellcheck, markdownlint)
  • CI shellspec workflow passes
  • Dogfood in a uv-based repository (e.g. sonar-skunk) once merged

Extract uv index credentials and caching into a reusable composite
action so workflows can authenticate against the repox PyPI index.
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Jun 11, 2026

Copy link
Copy Markdown

BUILD-11568

There is no jf uv-config command. Configure jf config for Repox,
set native UV_INDEX_* credentials, and document jf uv usage.
Comment thread config-uv/uv_config.sh
Comment on lines +23 to +24
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/_*$//')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Degenerate uv-index-name yields malformed env var names

index_name_upper is built by uppercasing the index name and replacing non-alphanumerics with _, then stripping only trailing underscores via sed 's/_*$//'. For a normal name like repox or my-index this works (and the trailing-underscore strip correctly removes the _ produced from echo's newline). However, edge inputs are not guarded: an empty or all-symbol uv-index-name produces UV_INDEX__USERNAME/UV_INDEX__PASSWORD, and a name beginning with a digit (e.g. 2repox) produces UV_INDEX_2REPOX_.... uv would then look up a differently-named variable and authentication would silently fall back to anonymous/public PyPI access, causing confusing 401/resolution failures rather than a clear error. Consider validating that the sanitized index name is non-empty (and optionally that it matches uv's own naming rules) and failing fast with a descriptive message if not.

Strip leading underscores too and fail fast when the sanitized name is empty.:

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/_*$//;s/^_*//')
if [[ -z "$index_name_upper" ]]; then
  echo "::error::Invalid uv-index-name '${UV_INDEX_NAME}': no usable characters for env var name" >&2
  return 1
fi
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment thread config-uv/uv_config.sh
Comment on lines +26 to +29
{
echo "UV_INDEX_${index_name_upper}_USERNAME=$ARTIFACTORY_USERNAME"
echo "UV_INDEX_${index_name_upper}_PASSWORD=$ARTIFACTORY_ACCESS_TOKEN"
} >> "$GITHUB_ENV"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Security: Artifactory token persisted to GITHUB_ENV for whole job

Unlike config-pip, which writes the token into a local pip.conf file, config-uv writes UV_INDEX_<NAME>_PASSWORD=<access_token> into GITHUB_ENV. This is required so that later uv sync steps can authenticate, but be aware it persists the credential as an environment variable visible to every subsequent step in the job, including any third-party actions invoked afterward. The value is masked in logs only because the vault-action-wrapper registers the secret with ::add-mask::; the persistence itself broadens the exposure surface compared to a file written under $HOME. This is largely inherent to how uv consumes index credentials, so no change may be possible, but it is worth documenting the trade-off and ensuring no untrusted steps run later in the same job.

Was this helpful? React with 👍 / 👎

Comment thread config-uv/action.yml
Comment on lines +68 to +73
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::"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: mise backup/restore use mismatched working directories

The mise backup logic and the restore logic run in different directories, which breaks when inputs.working-directory is not the default ..

  • The "Set local action paths" step (lines 68-73) has no working-directory, so it runs at the workspace root: it moves existing mise files into $mise_backup and cps mise.local.toml into the workspace root.
  • The "Configure uv authentication" step (lines 124-131) sets working-directory: ${{ inputs.working-directory }}, so the restore block (rm mise.local.toml, mv "$MISE_BACKUP"/* ./ ...) executes inside that subdirectory.

When working-directory is a subdirectory:

  1. rm mise.local.toml fails because the file was created at the workspace root, not the subdirectory. GitHub runs shell: bash with -eo pipefail, so this aborts the step and fails the whole action.
  2. Even if it didn't fail, the backed-up mise files would be restored into the wrong directory, leaving the workspace root permanently polluted with mise.local.toml and missing its original mise files.

Perform the backup and restore in the same directory. Either add working-directory: ${{ inputs.working-directory }} to the backup step, or remove working-directory from the configure step and have uv_config.sh/the build handle the path. Make sure the cp target and the rm/mv operate on the same location.

Run uv_config.sh in the working-directory but keep the mise restore at the workspace root where the backup/cp occurred.:

# In the "Configure uv authentication" step, drop the working-directory so
# backup (workspace root) and restore happen in the same place, OR run the
# backup at inputs.working-directory. Simplest: keep both at workspace root.
#
# backup step (no working-directory) already at workspace root -> keep restore there too:
    - name: Configure uv authentication
      if: steps.config-uv-completed.outputs.skip != 'true'
      shell: bash
      env:
        ...
      run: |
        (cd "${{ inputs.working-directory }}" && $ACTION_PATH_CONFIG_UV/uv_config.sh)

        echo "::group::Restore mise files"
        rm mise.local.toml
        mv "${{ steps.set-path.outputs.MISE_BACKUP }}"/* ./ 2>/dev/null || true
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment thread config-uv/action.yml
Comment on lines +124 to +131
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::"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: mise restore not run when uv_config.sh fails or action errors

The mise restore block is appended after $ACTION_PATH_CONFIG_UV/uv_config.sh in the same run: step and is not guarded by always(). Because the step uses -eo pipefail, if uv_config.sh exits non-zero the restore lines never execute. This leaves the workspace with mise.local.toml copied in and the repo's original mise files (mise., .mise., .tool-versions) stranded in the temp backup directory. In self-hosted or reused checkouts this corrupts subsequent steps/runs. Consider moving the restore into a separate step with if: always() (and tolerating a missing backup), so cleanup happens even on failure.

Move restore into a dedicated always() step and use .[!.] to avoid matching . and .. when restoring dotfiles.:*

- name: Restore mise files
  if: always() && steps.config-uv-completed.outputs.skip != 'true'
  shell: bash
  run: |
    echo "::group::Restore mise files"
    rm -f mise.local.toml
    backup="${{ steps.set-path.outputs.MISE_BACKUP }}"
    if [[ -n "$backup" && -d "$backup" ]]; then
      mv "$backup"/* "$backup"/.[!.]* ./ 2>/dev/null || true
      rmdir "$backup" 2>/dev/null || true
    fi
    echo "::endgroup::"
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Route npm through Repox before mise installs markdownlint-cli on
sonar-xs runners that cannot reach registry.npmjs.org directly.
@sonarqubecloud

Copy link
Copy Markdown

@gitar-bot

gitar-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 4 findings

Adds the config-uv composite action for Repox authentication, but mise backup/restore logic fails due to mismatched working directories. Additionally, Artifactory tokens are insecurely persisted to GITHUB_ENV, and the uv-index-name processing yields malformed variables.

⚠️ Bug: mise backup/restore use mismatched working directories

📄 config-uv/action.yml:68-73 📄 config-uv/action.yml:124-131

The mise backup logic and the restore logic run in different directories, which breaks when inputs.working-directory is not the default ..

  • The "Set local action paths" step (lines 68-73) has no working-directory, so it runs at the workspace root: it moves existing mise files into $mise_backup and cps mise.local.toml into the workspace root.
  • The "Configure uv authentication" step (lines 124-131) sets working-directory: ${{ inputs.working-directory }}, so the restore block (rm mise.local.toml, mv "$MISE_BACKUP"/* ./ ...) executes inside that subdirectory.

When working-directory is a subdirectory:

  1. rm mise.local.toml fails because the file was created at the workspace root, not the subdirectory. GitHub runs shell: bash with -eo pipefail, so this aborts the step and fails the whole action.
  2. Even if it didn't fail, the backed-up mise files would be restored into the wrong directory, leaving the workspace root permanently polluted with mise.local.toml and missing its original mise files.

Perform the backup and restore in the same directory. Either add working-directory: ${{ inputs.working-directory }} to the backup step, or remove working-directory from the configure step and have uv_config.sh/the build handle the path. Make sure the cp target and the rm/mv operate on the same location.

Run uv_config.sh in the working-directory but keep the mise restore at the workspace root where the backup/cp occurred.
# In the "Configure uv authentication" step, drop the working-directory so
# backup (workspace root) and restore happen in the same place, OR run the
# backup at inputs.working-directory. Simplest: keep both at workspace root.
#
# backup step (no working-directory) already at workspace root -> keep restore there too:
    - name: Configure uv authentication
      if: steps.config-uv-completed.outputs.skip != 'true'
      shell: bash
      env:
        ...
      run: |
        (cd "${{ inputs.working-directory }}" && $ACTION_PATH_CONFIG_UV/uv_config.sh)

        echo "::group::Restore mise files"
        rm mise.local.toml
        mv "${{ steps.set-path.outputs.MISE_BACKUP }}"/* ./ 2>/dev/null || true
💡 Edge Case: Degenerate uv-index-name yields malformed env var names

📄 config-uv/uv_config.sh:23-24

index_name_upper is built by uppercasing the index name and replacing non-alphanumerics with _, then stripping only trailing underscores via sed 's/_*$//'. For a normal name like repox or my-index this works (and the trailing-underscore strip correctly removes the _ produced from echo's newline). However, edge inputs are not guarded: an empty or all-symbol uv-index-name produces UV_INDEX__USERNAME/UV_INDEX__PASSWORD, and a name beginning with a digit (e.g. 2repox) produces UV_INDEX_2REPOX_.... uv would then look up a differently-named variable and authentication would silently fall back to anonymous/public PyPI access, causing confusing 401/resolution failures rather than a clear error. Consider validating that the sanitized index name is non-empty (and optionally that it matches uv's own naming rules) and failing fast with a descriptive message if not.

Strip leading underscores too and fail fast when the sanitized name is empty.
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/_*$//;s/^_*//')
if [[ -z "$index_name_upper" ]]; then
  echo "::error::Invalid uv-index-name '${UV_INDEX_NAME}': no usable characters for env var name" >&2
  return 1
fi
💡 Security: Artifactory token persisted to GITHUB_ENV for whole job

📄 config-uv/uv_config.sh:26-29 📄 config-uv/action.yml:102-111

Unlike config-pip, which writes the token into a local pip.conf file, config-uv writes UV_INDEX_<NAME>_PASSWORD=<access_token> into GITHUB_ENV. This is required so that later uv sync steps can authenticate, but be aware it persists the credential as an environment variable visible to every subsequent step in the job, including any third-party actions invoked afterward. The value is masked in logs only because the vault-action-wrapper registers the secret with ::add-mask::; the persistence itself broadens the exposure surface compared to a file written under $HOME. This is largely inherent to how uv consumes index credentials, so no change may be possible, but it is worth documenting the trade-off and ensuring no untrusted steps run later in the same job.

💡 Bug: mise restore not run when uv_config.sh fails or action errors

📄 config-uv/action.yml:124-131

The mise restore block is appended after $ACTION_PATH_CONFIG_UV/uv_config.sh in the same run: step and is not guarded by always(). Because the step uses -eo pipefail, if uv_config.sh exits non-zero the restore lines never execute. This leaves the workspace with mise.local.toml copied in and the repo's original mise files (mise., .mise., .tool-versions) stranded in the temp backup directory. In self-hosted or reused checkouts this corrupts subsequent steps/runs. Consider moving the restore into a separate step with if: always() (and tolerating a missing backup), so cleanup happens even on failure.

Move restore into a dedicated always() step and use .[!.]* to avoid matching . and .. when restoring dotfiles.
- name: Restore mise files
  if: always() && steps.config-uv-completed.outputs.skip != 'true'
  shell: bash
  run: |
    echo "::group::Restore mise files"
    rm -f mise.local.toml
    backup="${{ steps.set-path.outputs.MISE_BACKUP }}"
    if [[ -n "$backup" && -d "$backup" ]]; then
      mv "$backup"/* "$backup"/.[!.]* ./ 2>/dev/null || true
      rmdir "$backup" 2>/dev/null || true
    fi
    echo "::endgroup::"
🤖 Prompt for agents
Code Review: Adds the config-uv composite action for Repox authentication, but mise backup/restore logic fails due to mismatched working directories. Additionally, Artifactory tokens are insecurely persisted to GITHUB_ENV, and the uv-index-name processing yields malformed variables.

1. 💡 Edge Case: Degenerate uv-index-name yields malformed env var names
   Files: config-uv/uv_config.sh:23-24

   `index_name_upper` is built by uppercasing the index name and replacing non-alphanumerics with `_`, then stripping only *trailing* underscores via `sed 's/_*$//'`. For a normal name like `repox` or `my-index` this works (and the trailing-underscore strip correctly removes the `_` produced from echo's newline). However, edge inputs are not guarded: an empty or all-symbol `uv-index-name` produces `UV_INDEX__USERNAME`/`UV_INDEX__PASSWORD`, and a name beginning with a digit (e.g. `2repox`) produces `UV_INDEX_2REPOX_...`. uv would then look up a differently-named variable and authentication would silently fall back to anonymous/public PyPI access, causing confusing 401/resolution failures rather than a clear error. Consider validating that the sanitized index name is non-empty (and optionally that it matches uv's own naming rules) and failing fast with a descriptive message if not.

   Fix (Strip leading underscores too and fail fast when the sanitized name is empty.):
   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/_*$//;s/^_*//')
   if [[ -z "$index_name_upper" ]]; then
     echo "::error::Invalid uv-index-name '${UV_INDEX_NAME}': no usable characters for env var name" >&2
     return 1
   fi

2. 💡 Security: Artifactory token persisted to GITHUB_ENV for whole job
   Files: config-uv/uv_config.sh:26-29, config-uv/action.yml:102-111

   Unlike `config-pip`, which writes the token into a local `pip.conf` file, `config-uv` writes `UV_INDEX_<NAME>_PASSWORD=<access_token>` into `GITHUB_ENV`. This is required so that later `uv sync` steps can authenticate, but be aware it persists the credential as an environment variable visible to every subsequent step in the job, including any third-party actions invoked afterward. The value is masked in logs only because the vault-action-wrapper registers the secret with `::add-mask::`; the persistence itself broadens the exposure surface compared to a file written under `$HOME`. This is largely inherent to how uv consumes index credentials, so no change may be possible, but it is worth documenting the trade-off and ensuring no untrusted steps run later in the same job.

3. ⚠️ Bug: mise backup/restore use mismatched working directories
   Files: config-uv/action.yml:68-73, config-uv/action.yml:124-131

   The mise backup logic and the restore logic run in different directories, which breaks when `inputs.working-directory` is not the default `.`.
   
   - The "Set local action paths" step (lines 68-73) has no `working-directory`, so it runs at the workspace root: it moves existing mise files into `$mise_backup` and `cp`s `mise.local.toml` into the workspace root.
   - The "Configure uv authentication" step (lines 124-131) sets `working-directory: ${{ inputs.working-directory }}`, so the restore block (`rm mise.local.toml`, `mv "$MISE_BACKUP"/* ./ ...`) executes inside that subdirectory.
   
   When `working-directory` is a subdirectory:
   1. `rm mise.local.toml` fails because the file was created at the workspace root, not the subdirectory. GitHub runs `shell: bash` with `-eo pipefail`, so this aborts the step and fails the whole action.
   2. Even if it didn't fail, the backed-up mise files would be restored into the wrong directory, leaving the workspace root permanently polluted with `mise.local.toml` and missing its original mise files.
   
   Perform the backup and restore in the same directory. Either add `working-directory: ${{ inputs.working-directory }}` to the backup step, or remove `working-directory` from the configure step and have `uv_config.sh`/the build handle the path. Make sure the `cp` target and the `rm`/`mv` operate on the same location.

   Fix (Run uv_config.sh in the working-directory but keep the mise restore at the workspace root where the backup/cp occurred.):
   # In the "Configure uv authentication" step, drop the working-directory so
   # backup (workspace root) and restore happen in the same place, OR run the
   # backup at inputs.working-directory. Simplest: keep both at workspace root.
   #
   # backup step (no working-directory) already at workspace root -> keep restore there too:
       - name: Configure uv authentication
         if: steps.config-uv-completed.outputs.skip != 'true'
         shell: bash
         env:
           ...
         run: |
           (cd "${{ inputs.working-directory }}" && $ACTION_PATH_CONFIG_UV/uv_config.sh)
   
           echo "::group::Restore mise files"
           rm mise.local.toml
           mv "${{ steps.set-path.outputs.MISE_BACKUP }}"/* ./ 2>/dev/null || true

4. 💡 Bug: mise restore not run when uv_config.sh fails or action errors
   Files: config-uv/action.yml:124-131

   The mise restore block is appended after `$ACTION_PATH_CONFIG_UV/uv_config.sh` in the same `run:` step and is not guarded by `always()`. Because the step uses `-eo pipefail`, if `uv_config.sh` exits non-zero the restore lines never execute. This leaves the workspace with `mise.local.toml` copied in and the repo's original mise files (mise.*, .mise.*, .tool-versions) stranded in the temp backup directory. In self-hosted or reused checkouts this corrupts subsequent steps/runs. Consider moving the restore into a separate step with `if: always()` (and tolerating a missing backup), so cleanup happens even on failure.

   Fix (Move restore into a dedicated always() step and use .[!.]* to avoid matching . and .. when restoring dotfiles.):
   - name: Restore mise files
     if: always() && steps.config-uv-completed.outputs.skip != 'true'
     shell: bash
     run: |
       echo "::group::Restore mise files"
       rm -f mise.local.toml
       backup="${{ steps.set-path.outputs.MISE_BACKUP }}"
       if [[ -n "$backup" && -d "$backup" ]]; then
         mv "$backup"/* "$backup"/.[!.]* ./ 2>/dev/null || true
         rmdir "$backup" 2>/dev/null || true
       fi
       echo "::endgroup::"

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.
Unblock → Override a blocking verdict and allow merging.

Comment with these commands to change:

Auto-apply Compact Unblock
gitar auto-apply:on         
gitar display:verbose         
gitar unblock         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant