Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .shellspec
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# kcov (coverage) options
--kcov-options "--include-pattern=build-poetry,config-uv,config-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,report-ci-metrics"
--kcov-options "--include-pattern=build-poetry,config-uv,config-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,report-ci-metrics,config-nuget"
Comment thread
gitar-bot[bot] marked this conversation as resolved.
# --kcov-options "--exclude-pattern=.github,.idea,.git"

# define minimum coverage (fail otherwise)
Expand Down
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ These badges show the status of workflows in dummy repositories that use (or sho
- [`build-yarn`](#build-yarn)
- [`config-pip`](#config-pip)
- [`config-uv`](#config-uv)
- [`config-nuget`](#config-nuget)
- [`promote`](#promote)
- [`pr_cleanup`](#pr_cleanup)
- [`code-signing`](#code-signing)
Expand Down Expand Up @@ -1426,6 +1427,71 @@ See also [`get-build-number`](#get-build-number) output environment variables.

---

## `config-nuget`

Configure NuGet authentication and resolve the Repox package sources of NuGet configuration files through `repox-url`.

The action reads a Repox reader token from Vault and exports it as `ARTIFACTORY_USER` and `ARTIFACTORY_PASSWORD`, the variables
referenced by the `packageSourceCredentials` of the NuGet configuration files. With a `repox-url` other than `https://repox.jfrog.io`,
every package source `value` starting with `https://repox.jfrog.io/artifactory/` in the listed files is rewritten to `repox-url`,
so the committed files can keep SaaS Repox for local builds. Other attributes, such as `trustedSigners` service indexes, are unchanged.

> **Note:** The listed files are rewritten in place, so the checkout has local modifications for the rest of the job. Do not commit them,
> for example with `git add -A`, and exclude them from any diff check.

Calling the action again in the same job, for example with other `nuget-config-files`, is supported: files that already resolve
through `repox-url` are skipped. Unlike `config-maven` or `config-npm`, it therefore has no `CONFIG_NUGET_COMPLETED` guard.

The action does not cache NuGet packages. The global packages folder or a `RestorePackagesPath` is specific to each repository, so
cache it in the calling workflow, for example with [`SonarSource/gh-action_cache`](https://github.com/SonarSource/gh-action_cache)
keyed on `packages.lock.json`.

### Requirements

#### Required GitHub Permissions

- `id-token: write`

#### Required Vault Permissions

- `public-reader` or `private-reader`: Artifactory role for reading dependencies.

### Usage

```yaml
permissions:
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: SonarSource/ci-github-actions/config-nuget@v2
with:
nuget-config-files: |
NuGet.Config
its/NuGet.config
- run: dotnet restore
```

### Inputs

| Input | Description | Default |
|---------------------------|---------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `nuget-config-files` | NuGet configuration files to update (multiline), relative to `working-directory` | `NuGet.Config` |
| `working-directory` | Relative path under github.workspace to execute the action in | `.` |
| `artifactory-reader-role` | Suffix for the Artifactory reader role in Vault | `private-reader` for private repos, `public-reader` for public repos |
| `repox-url` | URL for Repox | `https://repox.jfrog.io` |

### Output Environment Variables

| Environment Variable | Description |
|----------------------------|---------------------------------------------|
| `ARTIFACTORY_URL` | Repox Artifactory URL used for resolving |
| `ARTIFACTORY_USERNAME` | Repox username |
| `ARTIFACTORY_ACCESS_TOKEN` | Repox access token |
| `ARTIFACTORY_USER` | Repox username for NuGet configurations |
| `ARTIFACTORY_PASSWORD` | Repox access token for NuGet configurations |

---

## `promote`

This action promotes a build in JFrog Artifactory and updates the GitHub status check accordingly.
Expand Down
48 changes: 48 additions & 0 deletions config-nuget/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Config NuGet
description: GitHub Action to configure NuGet authentication and resolve the Repox package sources of NuGet configuration files
inputs:
nuget-config-files:
description: NuGet configuration files whose `https://repox.jfrog.io/artifactory` package sources resolve through `repox-url`
(multiline), relative to `working-directory`.
default: NuGet.Config
working-directory:
description: Relative path under github.workspace to execute the action 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: ''
repox-url:
description: URL for Repox used to resolve dependencies. Use https://repox-internal.dev.sonar.build for the JFrog Edge.
default: https://repox.jfrog.io

runs:
using: composite
steps:
Comment thread
hedinasr marked this conversation as resolved.
Comment thread
hedinasr marked this conversation as resolved.
- name: Set Artifactory reader role
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"

- uses: SonarSource/vault-action-wrapper@881045d830534a70ec3c7c275fa3714412c8ff6e # 3.6.1
id: secrets
with:
# yamllint disable rule:line-length
url: ${{ contains(inputs.repox-url, 'jfrog.io') && 'https://vault.sonar.build' || 'https://vault.dev.sonar.build' }}
secrets: |
development/${{ contains(inputs.repox-url, 'jfrog.io') && 'artifactory' || 'artifactory-edge-dev' }}/token/{REPO_OWNER_NAME_DASH}-${{ env.ARTIFACTORY_READER_ROLE }} username | ARTIFACTORY_USERNAME;
development/${{ contains(inputs.repox-url, 'jfrog.io') && 'artifactory' || 'artifactory-edge-dev' }}/token/{REPO_OWNER_NAME_DASH}-${{ env.ARTIFACTORY_READER_ROLE }} access_token | ARTIFACTORY_ACCESS_TOKEN;
# yamllint enable rule:line-length

- name: Configure NuGet
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
ACTION_PATH_CONFIG_NUGET: ${{ github.action_path }}
ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }}
ARTIFACTORY_USERNAME: ${{ steps.secrets.outputs.vault && fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_USERNAME || '' }}
ARTIFACTORY_ACCESS_TOKEN: ${{ steps.secrets.outputs.vault && fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_ACCESS_TOKEN || '' }}
NUGET_CONFIG_FILES: ${{ inputs.nuget-config-files }}
run: '"$ACTION_PATH_CONFIG_NUGET/nuget_config.sh"'
72 changes: 72 additions & 0 deletions config-nuget/nuget_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash
# Configure NuGet authentication and point the Repox package sources of NuGet configuration files at ARTIFACTORY_URL.
#
# Required environment variables (must be explicitly provided):
# - ARTIFACTORY_URL: URL to Artifactory used to resolve packages
# - ARTIFACTORY_USERNAME: Username to read Repox repositories
# - ARTIFACTORY_ACCESS_TOKEN: Access token to read Repox repositories
# - NUGET_CONFIG_FILES: Newline-separated NuGet configuration files to update
#
# GitHub Actions auto-provided:
# - GITHUB_ENV: Path to GitHub Actions environment file

set -euo pipefail

: "${ARTIFACTORY_URL:?}" "${ARTIFACTORY_USERNAME:?}" "${ARTIFACTORY_ACCESS_TOKEN:?}" "${NUGET_CONFIG_FILES:?}" "${GITHUB_ENV:?}"

readonly SAAS_ARTIFACTORY_URL="https://repox.jfrog.io/artifactory"

export_credentials() {
{
echo "ARTIFACTORY_URL=$ARTIFACTORY_URL"
echo "ARTIFACTORY_USERNAME=$ARTIFACTORY_USERNAME"
echo "ARTIFACTORY_ACCESS_TOKEN=$ARTIFACTORY_ACCESS_TOKEN"
echo "ARTIFACTORY_USER=$ARTIFACTORY_USERNAME"
echo "ARTIFACTORY_PASSWORD=$ARTIFACTORY_ACCESS_TOKEN"
} >> "$GITHUB_ENV"
return 0
}

point_nuget_config() {
local config="$1"
local saas_pattern="value=\"${SAAS_ARTIFACTORY_URL//./\\.}/"
if [[ ! -f "$config" ]]; then
echo "::error::NuGet configuration file not found: $config" >&2
return 1
fi
if ! grep -q "$saas_pattern" "$config"; then
if grep -qF "value=\"${ARTIFACTORY_URL}/" "$config"; then
echo "Package sources of $config already resolve through $ARTIFACTORY_URL"
return 0
fi
echo "::error::No package source on $SAAS_ARTIFACTORY_URL in $config" >&2
return 1
fi
sed -i.bak "s#${saas_pattern}#value=\"${ARTIFACTORY_URL}/#g" "$config"
Comment thread
hedinasr marked this conversation as resolved.
rm -f "$config.bak"
echo "Package sources of $config now resolve through $ARTIFACTORY_URL"
return 0
}

main() {
echo "::group::Configure NuGet"
export_credentials
if [[ "${ARTIFACTORY_URL%/}" == "$SAAS_ARTIFACTORY_URL" ]]; then
echo "NuGet configuration files already resolve through $SAAS_ARTIFACTORY_URL"
else
local config
while IFS= read -r config; do
config="${config%$'\r'}"
config="${config#"${config%%[![:space:]]*}"}"
config="${config%"${config##*[![:space:]]}"}"
[[ -z "$config" ]] && continue
point_nuget_config "$config" || return 1
Comment thread
gitar-bot[bot] marked this conversation as resolved.
done <<< "$NUGET_CONFIG_FILES"
fi
echo "::endgroup::"
return 0
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main
fi
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sonar.projectName=ci-github-actions

sonar.sourceEncoding=UTF-8

sonar.sources=build-poetry,get-build-number,pr_cleanup,promote,build-gradle,build-maven,config-npm,build-npm,build-yarn,shared,config-pip,config-poetry,cache,code-signing,config-gradle,config-maven,check-sca,update-release-channel,report-ci-metrics
sonar.sources=build-poetry,get-build-number,pr_cleanup,promote,build-gradle,build-maven,config-npm,build-npm,build-yarn,shared,config-pip,config-poetry,cache,code-signing,config-gradle,config-maven,check-sca,update-release-channel,report-ci-metrics,config-nuget
sonar.tests=spec

sonar.coverageReportPaths=coverage/coverage_data/sonar_coverage.xml
151 changes: 151 additions & 0 deletions spec/config-nuget_spec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
eval "$(shellspec - -c) exit 1"

export GITHUB_ENV=/dev/null
export ARTIFACTORY_URL="https://repox-internal.dev.sonar.build/artifactory"
export ARTIFACTORY_USERNAME="reader"
export ARTIFACTORY_ACCESS_TOKEN="reader-token"
export NUGET_CONFIG_FILES="NuGet.Config"

Describe 'config-nuget/nuget_config.sh'
It 'does not run main when sourced'
When run source config-nuget/nuget_config.sh
The status should be success
The output should equal ""
End
End

Include config-nuget/nuget_config.sh

write_nuget_config() {
local nuget_config="$1"
cat > "$nuget_config" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="Repox proxy" value="https://repox.jfrog.io/artifactory/api/nuget/v3/nuget/index.json" protocolVersion="3" />
<add key="Repox releases" value="https://repox.jfrog.io/artifactory/api/nuget/v3/sonarsource-nuget-releases/index.json" />
</packageSources>
<trustedSigners>
<repository name="Repox" serviceIndex="https://repox.jfrog.io/artifactory/api/nuget/v3/nuget/index.json" />
</trustedSigners>
</configuration>
EOF
return
}

common_setup() {
WORK_DIR=$(mktemp -d)
cd "$WORK_DIR" || exit 1
GITHUB_ENV=$(mktemp)
export GITHUB_ENV
}

common_cleanup() {
cd - > /dev/null || true
rm -rf "$WORK_DIR" "$GITHUB_ENV"
return
}

Describe 'export_credentials()'
BeforeEach 'common_setup'
AfterEach 'common_cleanup'

It 'exports the reader credentials for NuGet and other tools'
When call export_credentials
The status should be success
The contents of file "$GITHUB_ENV" should include "ARTIFACTORY_URL=https://repox-internal.dev.sonar.build/artifactory"
The contents of file "$GITHUB_ENV" should include "ARTIFACTORY_USERNAME=reader"
The contents of file "$GITHUB_ENV" should include "ARTIFACTORY_ACCESS_TOKEN=reader-token"
The contents of file "$GITHUB_ENV" should include "ARTIFACTORY_USER=reader"
The contents of file "$GITHUB_ENV" should include "ARTIFACTORY_PASSWORD=reader-token"
End
End

Describe 'point_nuget_config()'
BeforeEach 'common_setup'
AfterEach 'common_cleanup'

It 'points every Repox package source at ARTIFACTORY_URL'
write_nuget_config NuGet.Config
When call point_nuget_config NuGet.Config
The status should be success
The output should equal "Package sources of NuGet.Config now resolve through https://repox-internal.dev.sonar.build/artifactory"
The contents of file NuGet.Config should include 'value="https://repox-internal.dev.sonar.build/artifactory/api/nuget/v3/nuget/index.json"'
The contents of file NuGet.Config should include 'value="https://repox-internal.dev.sonar.build/artifactory/api/nuget/v3/sonarsource-nuget-releases/index.json"'
The contents of file NuGet.Config should include 'serviceIndex="https://repox.jfrog.io/artifactory/api/nuget/v3/nuget/index.json"'
The file NuGet.Config.bak should not be exist
End

It 'skips a file that already resolves through ARTIFACTORY_URL'
write_nuget_config NuGet.Config
point_nuget_config NuGet.Config > /dev/null
When call point_nuget_config NuGet.Config
The status should be success
The output should equal "Package sources of NuGet.Config already resolve through https://repox-internal.dev.sonar.build/artifactory"
The contents of file NuGet.Config should include 'value="https://repox-internal.dev.sonar.build/artifactory/api/nuget/v3/nuget/index.json"'
End

It 'fails when the file does not exist'
When call point_nuget_config missing/NuGet.Config
The status should be failure
The stderr should equal "::error::NuGet configuration file not found: missing/NuGet.Config"
End

It 'fails when the file has no Repox package source'
echo '<configuration><packageSources><add key="nuget.org" value="https://api.nuget.org/v3/index.json" /></packageSources></configuration>' > NuGet.Config
When call point_nuget_config NuGet.Config
The status should be failure
The stderr should equal "::error::No package source on https://repox.jfrog.io/artifactory in NuGet.Config"
End
End

Describe 'main()'
BeforeEach 'common_setup'
AfterEach 'common_cleanup'

It 'updates every listed file and skips blank lines'
mkdir -p a b
write_nuget_config a/NuGet.Config
write_nuget_config b/nuget.config
export NUGET_CONFIG_FILES=$'a/NuGet.Config\r\n\n b/nuget.config \n'
When call main
The status should be success
The line 1 should equal "::group::Configure NuGet"
The line 2 should equal "Package sources of a/NuGet.Config now resolve through https://repox-internal.dev.sonar.build/artifactory"
The line 3 should equal "Package sources of b/nuget.config now resolve through https://repox-internal.dev.sonar.build/artifactory"
The line 4 should equal "::endgroup::"
The contents of file b/nuget.config should not include 'value="https://repox.jfrog.io/'
End

It 'keeps backslashes and apostrophes in listed paths'
write_nuget_config $'its\\NuGet.config'
mkdir -p "owner's"
write_nuget_config "owner's/NuGet.Config"
export NUGET_CONFIG_FILES=$'its\\NuGet.config\nowner'"'"'s/NuGet.Config'
When call main
The status should be success
The line 2 should equal $'Package sources of its\\NuGet.config now resolve through https://repox-internal.dev.sonar.build/artifactory'
The line 3 should equal "Package sources of owner's/NuGet.Config now resolve through https://repox-internal.dev.sonar.build/artifactory"
End

It 'leaves the files unchanged with SaaS Repox'
write_nuget_config NuGet.Config
export ARTIFACTORY_URL="https://repox.jfrog.io/artifactory"
export NUGET_CONFIG_FILES="NuGet.Config"
When call main
The status should be success
The line 2 should equal "NuGet configuration files already resolve through https://repox.jfrog.io/artifactory"
The contents of file NuGet.Config should include 'value="https://repox.jfrog.io/artifactory/api/nuget/v3/nuget/index.json"'
The contents of file "$GITHUB_ENV" should include "ARTIFACTORY_URL=https://repox.jfrog.io/artifactory"
End

It 'fails when a listed file is missing'
export NUGET_CONFIG_FILES="missing/NuGet.Config"
When run main
The status should be failure
The line 1 should equal "::group::Configure NuGet"
The stderr should equal "::error::NuGet configuration file not found: missing/NuGet.Config"
End
End
Loading