diff --git a/.github/actions/configure-nuget/README.md b/.github/actions/configure-nuget/README.md new file mode 100644 index 0000000000..cb7f73839a --- /dev/null +++ b/.github/actions/configure-nuget/README.md @@ -0,0 +1,15 @@ +# NuGet reader configuration + +Call this action immediately before a NuGet restore or another phase that can download NuGet packages. It delegates source configuration and fresh Vault authentication to [the shared config-nuget action](https://github.com/SonarSource/ci-github-actions/tree/b4eede988476ebb13a242fbc3ae4e8e374d78ad0/config-nuget), pinned to that revision, with an explicit `private-reader` role and selected Repox URL. + +Internal Windows and Linux runners use `https://repox.sonar.build/artifactory/api/nuget/v3/nuget/index.json` with the production native reader at `development/artifactory-edge-prod/token/{REPO_OWNER_NAME_DASH}-private-reader`. GitHub-hosted and macOS runners use `https://repox.jfrog.io/artifactory/api/nuget/v3/nuget/index.json` with the SaaS reader at `development/artifactory/token/{REPO_OWNER_NAME_DASH}-private-reader`. + +The `use-prod-edge` input defaults to empty. The first step then trims surrounding whitespace and normalizes the case of the inherited `NUGET_USE_PROD_EDGE` environment variable. Empty, `true`, and `false` are accepted; other values fail before requesting credentials. Production Edge is selected only for internal Windows or Linux runners when the normalized value is not `false`. Explicit `true` or `false` inputs override this default selection without reading the inherited variable; other input values fail before requesting credentials. + +The wrapper preserves the existing generic Artifactory environment, calls the shared action, then restores the prior values and exports the fresh reader as `NuGetPackageSourceCredentials_Repox`. It uses the `Username=...;Password=...` syntax supported by both modern NuGet and the NuGet client shipped with Visual Studio 2017. NuGet uses this source-specific credential before the credentials in `NuGet.Config`, so Maven, publication, signing, and Orchestrator credentials retain their own scope. Credentials stay out of the configuration file; source mapping, signature requirements, and trusted signers are preserved. The committed configuration retains its SaaS default for local use. + +The reader's configured default lifetime is one hour. A call refreshes the credential for subsequent steps; it does not refresh a running command. Keep calls close to network activity and refresh again before a later download phase. + +To roll back CI downloads to SaaS, set the repository or organization Actions variable `NUGET_USE_PROD_EDGE` to `false`, or pass `use-prod-edge: 'false'` at a call site. The CI workflows pass the variable to the action through the matching environment variable. The wrapper resets the source to SaaS before calling the shared action on this path, so rollback switches both source and reader even after an Edge call in the same job. An explicit `use-prod-edge: 'true'` selects production Edge regardless of runner defaults. + +NuGet's environment credential precedence and syntax are documented in [Consuming packages from authenticated feeds](https://learn.microsoft.com/en-us/nuget/consume-packages/consuming-packages-authenticated-feeds#credentials-in-environment-variables). diff --git a/.github/actions/configure-nuget/action.yml b/.github/actions/configure-nuget/action.yml new file mode 100644 index 0000000000..aee769338a --- /dev/null +++ b/.github/actions/configure-nuget/action.yml @@ -0,0 +1,85 @@ +name: Configure NuGet +description: Pair the Repox NuGet source with a fresh reader for the selected Artifactory instance. + +inputs: + use-prod-edge: + description: Use production Edge on internal runners. Set false to use the SaaS source and reader. + default: '' + +runs: + using: composite + steps: + - name: Select NuGet instance + id: instance + shell: bash + env: + USE_PROD_EDGE: ${{ inputs.use-prod-edge }} + RUNNER_ENVIRONMENT: ${{ runner.environment }} + run: | + set -euo pipefail + if [[ -z "$USE_PROD_EDGE" ]]; then + inherited_selection="$(printf '%s\n' "${NUGET_USE_PROD_EDGE:-}" | tr '[:upper:]' '[:lower:]' | sed -e ':all' -e '$!N' -e '$!b all' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + case "$inherited_selection" in + ''|true|false) ;; + *) echo '::error::NUGET_USE_PROD_EDGE must be empty, true or false'; exit 1 ;; + esac + USE_PROD_EDGE=false + if [[ ("$RUNNER_OS" == Windows || "$RUNNER_OS" == Linux) && "$RUNNER_ENVIRONMENT" != github-hosted && "$inherited_selection" != false ]]; then + USE_PROD_EDGE=true + fi + fi + case "$USE_PROD_EDGE" in + true|false) ;; + *) echo '::error::use-prod-edge must be true or false'; exit 1 ;; + esac + printf 'use-prod-edge=%s\n' "$USE_PROD_EDGE" >> "$GITHUB_OUTPUT" + + - name: Preserve Artifactory environment + id: environment + shell: bash + run: | + set -euo pipefail + for variable in ARTIFACTORY_URL ARTIFACTORY_USERNAME ARTIFACTORY_ACCESS_TOKEN ARTIFACTORY_USER ARTIFACTORY_PASSWORD ARTIFACTORY_READER_ROLE; do + printf '%s=%s\n' "$variable" "${!variable-}" >> "$GITHUB_OUTPUT" + done + + - name: Reset NuGet source for SaaS rollback + if: steps.instance.outputs.use-prod-edge == 'false' + shell: bash + run: | + dotnet nuget update source Repox \ + --source https://repox.jfrog.io/artifactory/api/nuget/v3/nuget/index.json \ + --configfile NuGet.Config + + - name: Configure NuGet source and reader + id: nuget + uses: SonarSource/ci-github-actions/config-nuget@b4eede988476ebb13a242fbc3ae4e8e374d78ad0 + with: + artifactory-reader-role: private-reader + repox-url: ${{ steps.instance.outputs.use-prod-edge == 'true' && 'https://repox.sonar.build' || 'https://repox.jfrog.io' }} + + - name: Scope reader credentials to NuGet + if: always() && steps.environment.outcome == 'success' + shell: bash + env: + PREVIOUS_ARTIFACTORY_URL: ${{ steps.environment.outputs.ARTIFACTORY_URL }} + PREVIOUS_ARTIFACTORY_USERNAME: ${{ steps.environment.outputs.ARTIFACTORY_USERNAME }} + PREVIOUS_ARTIFACTORY_ACCESS_TOKEN: ${{ steps.environment.outputs.ARTIFACTORY_ACCESS_TOKEN }} + PREVIOUS_ARTIFACTORY_USER: ${{ steps.environment.outputs.ARTIFACTORY_USER }} + PREVIOUS_ARTIFACTORY_PASSWORD: ${{ steps.environment.outputs.ARTIFACTORY_PASSWORD }} + PREVIOUS_ARTIFACTORY_READER_ROLE: ${{ steps.environment.outputs.ARTIFACTORY_READER_ROLE }} + NUGET_CONFIGURATION_OUTCOME: ${{ steps.nuget.outcome }} + run: | + set -euo pipefail + for variable in ARTIFACTORY_URL ARTIFACTORY_USERNAME ARTIFACTORY_ACCESS_TOKEN ARTIFACTORY_USER ARTIFACTORY_PASSWORD ARTIFACTORY_READER_ROLE; do + previous_variable="PREVIOUS_$variable" + printf '%s=%s\n' "$variable" "${!previous_variable}" >> "$GITHUB_ENV" + done + if [[ "$NUGET_CONFIGURATION_OUTCOME" != success ]]; then + exit 0 + fi + : "${ARTIFACTORY_USERNAME:?NuGet reader username is required}" "${ARTIFACTORY_ACCESS_TOKEN:?NuGet reader token is required}" + # NuGet 4.9 treats authentication-type metadata after Password as part of the password. + credentials="Username=$ARTIFACTORY_USERNAME;Password=$ARTIFACTORY_ACCESS_TOKEN" + printf '::add-mask::%s\n' "$credentials" + printf 'NuGetPackageSourceCredentials_Repox=%s\n' "$credentials" >> "$GITHUB_ENV" diff --git a/.github/actions/its-unix/action.yml b/.github/actions/its-unix/action.yml index 8e3c748597..de245e87d8 100644 --- a/.github/actions/its-unix/action.yml +++ b/.github/actions/its-unix/action.yml @@ -105,6 +105,9 @@ runs: fi echo "ORCHESTRATOR_CONFIG_URL=$config_url" >> "$GITHUB_ENV" + - name: Configure NuGet + uses: ./.github/actions/configure-nuget + - name: Run Maven ITs env: MAVEN_OPTS: -Xmx3072m diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml index d0fd4402e7..e283dd774c 100644 --- a/.github/actions/prepare/action.yml +++ b/.github/actions/prepare/action.yml @@ -55,6 +55,9 @@ runs: key: nuget-${{ steps.cache-month.outputs.CACHE_MONTH }}-${{ hashFiles('**/packages.lock.json') }} restore-keys: nuget-${{ steps.cache-month.outputs.CACHE_MONTH }}- + - name: Configure NuGet + uses: ./.github/actions/configure-nuget + - name: Restore shell: powershell env: diff --git a/.github/actions/qa-unix/action.yml b/.github/actions/qa-unix/action.yml index 3c33270cca..f616b8da10 100644 --- a/.github/actions/qa-unix/action.yml +++ b/.github/actions/qa-unix/action.yml @@ -51,10 +51,16 @@ runs: key: nuget-${{ runner.os }}-${{ steps.cache-month.outputs.CACHE_MONTH }}-${{ hashFiles('**/packages.lock.json') }} restore-keys: nuget-${{ runner.os }}-${{ steps.cache-month.outputs.CACHE_MONTH }}- + - name: Configure NuGet + uses: ./.github/actions/configure-nuget + - name: Build test pre-requisites shell: bash run: dotnet publish ./Tests/LogArgs/LogArgs.csproj --framework net10.0 --self-contained -r ${{ inputs.rid }} + - name: Refresh NuGet reader + uses: ./.github/actions/configure-nuget + - name: Run UTs shell: bash env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85882fb624..42fab58970 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ concurrency: env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + NUGET_USE_PROD_EDGE: ${{ vars.NUGET_USE_PROD_EDGE }} jobs: version: @@ -183,6 +184,9 @@ jobs: path: ${{ env.NUGET_PACKAGES }}/dotnet-sonarscanner key: s4net-${{ hashFiles('.config/dotnet-tools.json') }} + - name: Refresh NuGet reader + uses: ./.github/actions/configure-nuget + - name: Restore SonarScanner for .NET run: dotnet tool restore --tool-manifest .config/dotnet-tools.json --configfile "NuGet.Config" diff --git a/.github/workflows/its-windows.yml b/.github/workflows/its-windows.yml index f8ef2704e8..781e372e1b 100644 --- a/.github/workflows/its-windows.yml +++ b/.github/workflows/its-windows.yml @@ -13,6 +13,9 @@ on: SONARCLOUD_PROJECT_TOKEN: required: true +env: + NUGET_USE_PROD_EDGE: ${{ vars.NUGET_USE_PROD_EDGE }} + jobs: its_windows: name: ${{ matrix.scenario }} @@ -223,6 +226,9 @@ jobs: fi echo "ORCHESTRATOR_CONFIG_URL=$config_url" >> "$GITHUB_ENV" + - name: Configure NuGet + uses: ./.github/actions/configure-nuget + - name: Run Windows ITs (${{ matrix.scenario }}) env: MAVEN_OPTS: -Xmx3072m