diff --git a/build-gradle/action.yml b/build-gradle/action.yml index c97f3432..4f21f88d 100644 --- a/build-gradle/action.yml +++ b/build-gradle/action.yml @@ -45,7 +45,10 @@ inputs: for public repositories. default: '' repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution + default: https://repox.jfrog.io + repox-deploy-url: + description: URL for Repox used for artifact deployment. Defaults to SaaS Repox so deploy does not depend on edge Access Federation. default: https://repox.jfrog.io use-develocity: description: Whether to use Develocity for build tracking. @@ -129,7 +132,6 @@ runs: if: inputs.deploy != 'false' && inputs.run-shadow-scans != 'true' id: artifactory with: - url: ${{ contains(inputs.repox-url, 'dev.sonar.build') && 'https://vault.dev.sonar.build' || 'https://vault.sonar.build' }} # yamllint disable rule:line-length secrets: | ${{ format('development/artifactory/token/{{REPO_OWNER_NAME_DASH}}-{0} username | ARTIFACTORY_DEPLOY_USERNAME;', env.ARTIFACTORY_DEPLOYER_ROLE) }} @@ -169,6 +171,8 @@ runs: RUN_SHADOW_SCANS: ${{ inputs.run-shadow-scans }} ARTIFACTORY_DEPLOY_REPO: ${{ inputs.artifactory-deploy-repo != '' && inputs.artifactory-deploy-repo || github.event.repository.visibility == 'public' && 'sonarsource-public-qa' || 'sonarsource-private-qa' }} + # Upload target for artifactoryPublish (downloads still use ARTIFACTORY_RESOLVE_URL from config-gradle). + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} # Vault secrets # yamllint disable rule:line-length @@ -217,6 +221,8 @@ runs: - name: Generate workflow summary if: always() && inputs.generate-summary != 'false' shell: bash + env: + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} run: | build_name="${GITHUB_REPOSITORY#*/}" echo "## 🏗 Gradle Build Summary (\`${GITHUB_JOB}\`)" >> $GITHUB_STEP_SUMMARY diff --git a/build-maven/action.yml b/build-maven/action.yml index 9c8db9f7..46156bdb 100644 --- a/build-maven/action.yml +++ b/build-maven/action.yml @@ -49,7 +49,10 @@ inputs: description: Maven flags for all subsequent mvn calls default: --batch-mode --no-transfer-progress --errors --fail-at-end --show-version -Dmaven.test.redirectTestOutputToFile=false repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution (Maven mirror) + default: https://repox.jfrog.io + repox-deploy-url: + description: URL for Repox used for artifact deployment. Defaults to SaaS Repox so deploy does not depend on edge Access Federation. default: https://repox.jfrog.io use-develocity: description: Whether to use Develocity for build tracking. @@ -125,6 +128,8 @@ runs: env: MIXED_PRIVACY: ${{ inputs.mixed-privacy }} USER_MAVEN_ARGS: ${{ inputs.maven-args }} + ARTIFACTORY_RESOLVE_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_DEPLOY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} ARTIFACTORY_DEPLOY_REPO: ${{ inputs.artifactory-deploy-repo != '' && inputs.artifactory-deploy-repo || (github.event.repository.visibility == 'public' || inputs.mixed-privacy == 'true') && 'sonarsource-public-qa' || 'sonarsource-private-qa' }} @@ -141,17 +146,21 @@ runs: fi echo "USER_MAVEN_ARGS=${USER_MAVEN_ARGS}" echo "ARTIFACTORY_DEPLOY_REPO=${ARTIFACTORY_DEPLOY_REPO}" + echo "ARTIFACTORY_DEPLOY_URL=${ARTIFACTORY_DEPLOY_URL}" echo "ARTIFACTORY_DEPLOY_USERNAME_VAULT=development/artifactory/token/{REPO_OWNER_NAME_DASH}-${ARTIFACTORY_DEPLOYER_ROLE} username | ARTIFACTORY_DEPLOY_USERNAME;" echo "ARTIFACTORY_DEPLOY_ACCESS_TOKEN_VAULT=development/artifactory/token/{REPO_OWNER_NAME_DASH}-${ARTIFACTORY_DEPLOYER_ROLE} access_token | ARTIFACTORY_DEPLOY_ACCESS_TOKEN;" } >> "$GITHUB_OUTPUT" - echo "SONARSOURCE_REPOSITORY_URL=${ARTIFACTORY_URL}/sonarsource" >> "$GITHUB_ENV" + # Downloads (Maven settings mirror) always use repox-url; uploads use ARTIFACTORY_DEPLOY_URL / repox-deploy-url. + echo "ARTIFACTORY_URL=${ARTIFACTORY_RESOLVE_URL}" >> "$GITHUB_ENV" + echo "SONARSOURCE_REPOSITORY_URL=${ARTIFACTORY_RESOLVE_URL}/sonarsource" >> "$GITHUB_ENV" + echo "Artifactory resolve (download): ${ARTIFACTORY_RESOLVE_URL}" + echo "Artifactory deploy (upload): ${ARTIFACTORY_DEPLOY_URL}" # yamllint enable rule:line-length - uses: SonarSource/vault-action-wrapper@881045d830534a70ec3c7c275fa3714412c8ff6e # 3.6.1 if: inputs.deploy != 'false' id: artifactory with: - url: ${{ contains(inputs.repox-url, 'dev.sonar.build') && 'https://vault.dev.sonar.build' || 'https://vault.sonar.build' }} # yamllint disable rule:line-length secrets: | ${{ inputs.deploy != 'false' && inputs.run-shadow-scans != 'true' && steps.params.outputs.ARTIFACTORY_DEPLOY_USERNAME_VAULT || '' }} @@ -190,6 +199,8 @@ runs: SONAR_PLATFORM: ${{ inputs.sonar-platform }} RUN_SHADOW_SCANS: ${{ inputs.run-shadow-scans }} ARTIFACTORY_DEPLOY_REPO: ${{ steps.params.outputs.ARTIFACTORY_DEPLOY_REPO }} + # Upload target for the Artifactory Maven plugin / parent POM (downloads still use SONARSOURCE_REPOSITORY_URL). + ARTIFACTORY_URL: ${{ steps.params.outputs.ARTIFACTORY_DEPLOY_URL }} # Vault secrets # yamllint disable rule:line-length @@ -214,6 +225,7 @@ runs: inputs.mixed-privacy == 'true' && steps.build.outputs.deployed == 'true' && (github.event_name != 'pull_request' || inputs.deploy-pull-request == 'true') env: + ARTIFACTORY_URL: ${{ steps.params.outputs.ARTIFACTORY_DEPLOY_URL }} ARTIFACTORY_DEPLOY_REPO: ${{ steps.params.outputs.ARTIFACTORY_DEPLOY_REPO }} # yamllint disable rule:line-length ARTIFACTORY_DEPLOY_ACCESS_TOKEN: ${{ steps.artifactory.outputs.vault && fromJSON(steps.artifactory.outputs.vault).ARTIFACTORY_DEPLOY_ACCESS_TOKEN || '' }} @@ -246,6 +258,7 @@ runs: if: always() && inputs.generate-summary != 'false' shell: bash env: + ARTIFACTORY_URL: ${{ steps.params.outputs.ARTIFACTORY_DEPLOY_URL }} JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR: ${{ runner.temp }}/jfrog-summary run: | build_name="${GITHUB_REPOSITORY#*/}" diff --git a/build-npm/action.yml b/build-npm/action.yml index 6bd09439..f6677443 100644 --- a/build-npm/action.yml +++ b/build-npm/action.yml @@ -33,7 +33,10 @@ inputs: description: Deprecated. Use `disable-caching` instead. Whether to cache NPM dependencies. default: 'true' repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution (npm registry) + default: https://repox.jfrog.io + repox-deploy-url: + description: URL for Repox used for artifact deployment. Defaults to SaaS Repox so deploy does not depend on edge Access Federation. default: https://repox.jfrog.io sonar-platform: description: SonarQube primary platform (next, sqc-eu, sqc-us, or none). Use 'none' to skip sonar scans. @@ -135,7 +138,6 @@ runs: if: inputs.deploy != 'false' && inputs.run-shadow-scans != 'true' id: artifactory with: - url: ${{ contains(inputs.repox-url, 'dev.sonar.build') && 'https://vault.dev.sonar.build' || 'https://vault.sonar.build' }} # yamllint disable rule:line-length secrets: | ${{ format('development/artifactory/token/{{REPO_OWNER_NAME_DASH}}-{0} access_token | ARTIFACTORY_DEPLOY_ACCESS_TOKEN;', env.ARTIFACTORY_DEPLOYER_ROLE) }} @@ -162,7 +164,8 @@ runs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} BUILD_NAME: ${{ inputs.build-name || github.event.repository.name }} - ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + # Upload target for jf npm publish (downloads still use ~/.npmrc from config-npm / repox-url). + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} ARTIFACTORY_DEPLOY_REPO: ${{ inputs.artifactory-deploy-repo != '' && inputs.artifactory-deploy-repo || (github.event.repository.visibility == 'public' && 'sonarsource-npm-public-qa' || 'sonarsource-npm-private-qa') }} # yamllint disable rule:line-length @@ -215,7 +218,7 @@ runs: if: always() && inputs.generate-summary != 'false' shell: bash env: - ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR: ${{ runner.temp }}/jfrog-summary run: | build_name="${GITHUB_REPOSITORY#*/}" diff --git a/build-poetry/action.yml b/build-poetry/action.yml index 0fbb88d2..6c5b0387 100644 --- a/build-poetry/action.yml +++ b/build-poetry/action.yml @@ -31,7 +31,10 @@ inputs: description: Path to the Poetry cache directory, relative to GitHub workspace. default: .cache/pypoetry repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution + default: https://repox.jfrog.io + repox-deploy-url: + description: URL for Repox used for artifact deployment. Defaults to SaaS Repox so deploy does not depend on edge Access Federation. default: https://repox.jfrog.io sonar-platform: description: SonarQube primary platform (next, sqc-eu, sqc-us, or none). Use 'none' to skip sonar scans. @@ -123,7 +126,6 @@ runs: if: inputs.deploy != 'false' && inputs.run-shadow-scans != 'true' id: artifactory with: - url: ${{ contains(inputs.repox-url, 'dev.sonar.build') && 'https://vault.dev.sonar.build' || 'https://vault.sonar.build' }} # yamllint disable rule:line-length secrets: | ${{ format('development/artifactory/token/{{REPO_OWNER_NAME_DASH}}-{0} access_token | ARTIFACTORY_DEPLOY_ACCESS_TOKEN;', env.ARTIFACTORY_DEPLOYER_ROLE) }} @@ -149,8 +151,8 @@ runs: PULL_REQUEST_SHA: ${{ github.event.pull_request.base.sha || '' }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - # Action inputs - ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + # Upload target for jf poetry publish (downloads still use config-poetry / repox-url). + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} DEPLOY: ${{ inputs.deploy }} DEPLOY_PULL_REQUEST: ${{ inputs.deploy-pull-request }} ARTIFACTORY_DEPLOY_REPO: ${{ inputs.artifactory-deploy-repo != '' && inputs.artifactory-deploy-repo || @@ -193,7 +195,7 @@ runs: if: always() && inputs.generate-summary != 'false' shell: bash env: - ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR: ${{ runner.temp }}/jfrog-summary run: | build_name="${GITHUB_REPOSITORY#*/}" diff --git a/build-yarn/action.yml b/build-yarn/action.yml index 6556a815..9d14d5fe 100644 --- a/build-yarn/action.yml +++ b/build-yarn/action.yml @@ -36,7 +36,10 @@ inputs: description: Deprecated. Use `disable-caching` instead. Whether to cache Yarn dependencies. default: 'true' repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution (npm registry) + default: https://repox.jfrog.io + repox-deploy-url: + description: URL for Repox used for artifact deployment. Defaults to SaaS Repox so deploy does not depend on edge Access Federation. default: https://repox.jfrog.io sonar-platform: description: SonarQube primary platform (next, sqc-eu, sqc-us, or none). Use 'none' to skip sonar scans. @@ -130,13 +133,23 @@ runs: - uses: SonarSource/vault-action-wrapper@881045d830534a70ec3c7c275fa3714412c8ff6e # 3.6.1 id: artifactory with: - url: ${{ contains(inputs.repox-url, 'dev.sonar.build') && 'https://vault.dev.sonar.build' || 'https://vault.sonar.build' }} # yamllint disable rule:line-length 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; ${{ inputs.deploy != 'false' && inputs.run-shadow-scans != 'true' && format('development/artifactory/token/{{REPO_OWNER_NAME_DASH}}-{0} access_token | ARTIFACTORY_DEPLOY_ACCESS_TOKEN;', env.ARTIFACTORY_DEPLOYER_ROLE) || '' }} # yamllint enable rule:line-length + + # Temporary: Access Federation can lag when SaaS-minted tokens are used on an edge node. + - name: Wait for Artifactory token to sync + shell: bash + env: + REPOX_URL: ${{ inputs.repox-url }} + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_USERNAME: ${{ fromJSON(steps.artifactory.outputs.vault).ARTIFACTORY_USERNAME }} + ARTIFACTORY_ACCESS_TOKEN: ${{ fromJSON(steps.artifactory.outputs.vault).ARTIFACTORY_ACCESS_TOKEN }} + run: $ACTION_PATH_BUILD_YARN/../shared/wait_artifactory_token_sync.sh + - uses: SonarSource/vault-action-wrapper@881045d830534a70ec3c7c275fa3714412c8ff6e # 3.6.1 id: secrets # yamllint disable rule:line-length @@ -160,6 +173,7 @@ runs: # Action inputs ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_DEPLOY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} ARTIFACTORY_DEPLOY_REPO: ${{ inputs.artifactory-deploy-repo != '' && inputs.artifactory-deploy-repo || github.event.repository.visibility == 'public' && 'sonarsource-public-qa' || 'sonarsource-private-qa' }} ARTIFACTORY_USERNAME: ${{ fromJSON(steps.artifactory.outputs.vault).ARTIFACTORY_USERNAME }} @@ -198,7 +212,7 @@ runs: if: always() && inputs.generate-summary != 'false' shell: bash env: - ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-deploy-url) }} JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR: ${{ runner.temp }}/jfrog-summary run: | build_name="${GITHUB_REPOSITORY#*/}" diff --git a/build-yarn/build.sh b/build-yarn/build.sh index 1263d403..28b08b7f 100755 --- a/build-yarn/build.sh +++ b/build-yarn/build.sh @@ -171,9 +171,10 @@ sonar_scanner_implementation() { } jfrog_yarn_publish() { - echo "::debug::Configuring JFrog and NPM repositories..." + local deploy_url="${ARTIFACTORY_DEPLOY_URL:-$ARTIFACTORY_URL}" + echo "::debug::Configuring JFrog and NPM repositories for deploy ($deploy_url)..." 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_DEPLOY_ACCESS_TOKEN" + jf config add repox --url "${deploy_url%/artifactory*}" --artifactory-url "$deploy_url" --access-token "$ARTIFACTORY_DEPLOY_ACCESS_TOKEN" jf config use repox jf npm-config --global --repo-resolve "npm" --repo-deploy "$ARTIFACTORY_DEPLOY_REPO" diff --git a/config-gradle/action.yml b/config-gradle/action.yml index 730c7ee7..98082f9c 100644 --- a/config-gradle/action.yml +++ b/config-gradle/action.yml @@ -10,7 +10,7 @@ inputs: and `public-reader` for public repositories. default: '' repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution default: https://repox.jfrog.io use-develocity: description: Whether to use Develocity for build tracking. @@ -92,7 +92,6 @@ runs: if: steps.config-gradle-completed.outputs.skip != 'true' id: artifactory 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; @@ -131,6 +130,8 @@ runs: fi echo "ARTIFACTORY_URL=$ARTIFACTORY_URL" >> "$GITHUB_ENV" + # Resolve URL kept separately so build steps can override ARTIFACTORY_URL for deploy. + echo "ARTIFACTORY_RESOLVE_URL=$ARTIFACTORY_URL" >> "$GITHUB_ENV" echo "ARTIFACTORY_USERNAME=$ARTIFACTORY_USERNAME" >> "$GITHUB_ENV" echo "ARTIFACTORY_ACCESS_USERNAME=$ARTIFACTORY_USERNAME" >> "$GITHUB_ENV" # deprecated, backward compliance echo "ARTIFACTORY_ACCESS_TOKEN=$ARTIFACTORY_ACCESS_TOKEN" >> "$GITHUB_ENV" @@ -139,6 +140,14 @@ runs: echo "DEVELOCITY_ACCESS_KEY=${{ steps.develocity-hostname.outputs.hostname }}=$DEVELOCITY_TOKEN" >> "$GITHUB_ENV" fi + # Temporary: Access Federation can lag when SaaS-minted tokens are used on an edge node. + - name: Wait for Artifactory token to sync + if: steps.config-gradle-completed.outputs.skip != 'true' + shell: bash + env: + REPOX_URL: ${{ inputs.repox-url }} + run: $ACTION_PATH_CONFIG_GRADLE/../shared/wait_artifactory_token_sync.sh + - name: Configure Gradle wrapper download resilience if: steps.config-gradle-completed.outputs.skip != 'true' shell: bash diff --git a/config-gradle/resources/repoxAuth.init.gradle.kts b/config-gradle/resources/repoxAuth.init.gradle.kts index 08e97d59..54e1b1e5 100644 --- a/config-gradle/resources/repoxAuth.init.gradle.kts +++ b/config-gradle/resources/repoxAuth.init.gradle.kts @@ -89,7 +89,11 @@ allprojects { class RepoxAuth { companion object { - val artifactoryUrl = System.getenv("ARTIFACTORY_URL") ?: "https://repox.jfrog.io/artifactory" + // Prefer ARTIFACTORY_RESOLVE_URL so dependency resolution can target an edge node + // while ARTIFACTORY_URL is used for artifactoryPublish / deploy (often SaaS). + val artifactoryUrl = System.getenv("ARTIFACTORY_RESOLVE_URL") + ?: System.getenv("ARTIFACTORY_URL") + ?: "https://repox.jfrog.io/artifactory" val host = java.net.URI(artifactoryUrl).host val sonarsourceRepositoryUrl = RepoxAuth.artifactoryUrl.trimEnd('/') + "/" + (System.getenv("SONARSOURCE_REPOSITORY") ?: "sonarsource") diff --git a/config-maven/action.yml b/config-maven/action.yml index 2738ea18..77561747 100644 --- a/config-maven/action.yml +++ b/config-maven/action.yml @@ -93,7 +93,6 @@ runs: if: steps.config-maven-completed.outputs.skip != 'true' id: artifactory 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; @@ -140,6 +139,14 @@ runs: echo "DEVELOCITY_ACCESS_KEY=${{ steps.develocity-hostname.outputs.hostname }}=$DEVELOCITY_TOKEN" >> "$GITHUB_ENV" fi + # Temporary: Access Federation can lag when SaaS-minted tokens are used on an edge node. + - name: Wait for Artifactory token to sync + if: steps.config-maven-completed.outputs.skip != 'true' + shell: bash + env: + REPOX_URL: ${{ inputs.repox-url }} + run: $ACTION_PATH_CONFIG_MAVEN/../shared/wait_artifactory_token_sync.sh + - name: Configure Maven settings and set repository URL if: steps.config-maven-completed.outputs.skip != 'true' shell: bash diff --git a/config-npm/action.yml b/config-npm/action.yml index 451034af..d7783e3f 100644 --- a/config-npm/action.yml +++ b/config-npm/action.yml @@ -16,7 +16,7 @@ inputs: description: Deprecated. Use `disable-caching` instead. Whether to cache NPM dependencies. default: 'true' repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution default: https://repox.jfrog.io host-actions-root: description: Path to the actions folder on the host (used when called from another local action) @@ -93,11 +93,21 @@ runs: if: steps.config-npm-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; + # Temporary: Access Federation can lag when SaaS-minted tokens are used on an edge node. + - name: Wait for Artifactory token to sync + if: steps.config-npm-completed.outputs.skip != 'true' + shell: bash + env: + REPOX_URL: ${{ inputs.repox-url }} + 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 || '' }} + run: $ACTION_PATH_CONFIG_NPM/../shared/wait_artifactory_token_sync.sh + - name: Configure NPM authentication if: steps.config-npm-completed.outputs.skip != 'true' shell: bash diff --git a/config-pip/action.yml b/config-pip/action.yml index f2149a7f..164649ac 100644 --- a/config-pip/action.yml +++ b/config-pip/action.yml @@ -11,7 +11,7 @@ inputs: for public repositories. default: '' repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution default: https://repox.jfrog.io cache-paths: description: Cache paths to use (multiline). @@ -73,11 +73,20 @@ runs: - uses: SonarSource/vault-action-wrapper@881045d830534a70ec3c7c275fa3714412c8ff6e # 3.6.1 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; + # Temporary: Access Federation can lag when SaaS-minted tokens are used on an edge node. + - name: Wait for Artifactory token to sync + shell: bash + env: + REPOX_URL: ${{ inputs.repox-url }} + 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 }} + run: $ACTION_PATH_CONFIG_PIP/../shared/wait_artifactory_token_sync.sh + - name: Run pip configuration script id: config shell: bash diff --git a/config-poetry/action.yml b/config-poetry/action.yml index 8c7fb1f5..1a71ec20 100644 --- a/config-poetry/action.yml +++ b/config-poetry/action.yml @@ -13,7 +13,7 @@ inputs: description: PyPI virtual repository to resolve dependencies from default: sonarsource-pypi repox-url: - description: URL for Repox + description: URL for Repox used for dependency resolution default: https://repox.jfrog.io poetry-virtualenvs-path: description: Path to the Poetry virtual environments, relative to GitHub workspace. The folder is cached only if it is a subdirectory of @@ -118,11 +118,21 @@ runs: if: steps.config-poetry-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; + # Temporary: Access Federation can lag when SaaS-minted tokens are used on an edge node. + - name: Wait for Artifactory token to sync + if: steps.config-poetry-completed.outputs.skip != 'true' + shell: bash + env: + REPOX_URL: ${{ inputs.repox-url }} + 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 }} + run: $ACTION_PATH_CONFIG_POETRY/../shared/wait_artifactory_token_sync.sh + - name: Configure Poetry authentication if: steps.config-poetry-completed.outputs.skip != 'true' shell: bash diff --git a/config-uv/action.yml b/config-uv/action.yml index 6d4fbfbf..72d3b0ef 100644 --- a/config-uv/action.yml +++ b/config-uv/action.yml @@ -13,7 +13,7 @@ inputs: 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 + description: URL for Repox used for dependency resolution default: https://repox.jfrog.io uv-cache-dir: description: Path to the uv cache directory, relative to GitHub workspace @@ -106,11 +106,21 @@ runs: 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; + # Temporary: Access Federation can lag when SaaS-minted tokens are used on an edge node. + - name: Wait for Artifactory token to sync + if: steps.config-uv-completed.outputs.skip != 'true' + shell: bash + env: + REPOX_URL: ${{ inputs.repox-url }} + 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 }} + run: $ACTION_PATH_CONFIG_UV/../shared/wait_artifactory_token_sync.sh + - name: Configure uv authentication if: steps.config-uv-completed.outputs.skip != 'true' shell: bash diff --git a/promote/action.yml b/promote/action.yml index 5b37419b..0a9a424d 100644 --- a/promote/action.yml +++ b/promote/action.yml @@ -3,7 +3,7 @@ name: Promote description: GitHub Action to promote a project inputs: repox-url: - description: URL for Repox + description: URL for Repox used for promotion (typically SaaS where artifacts were deployed) default: https://repox.jfrog.io promote-pull-request: description: Whether to promote pull request artifacts. Requires `deploy-pull-request` input to be set to `true` in the build action. @@ -51,7 +51,6 @@ runs: - uses: SonarSource/vault-action-wrapper@881045d830534a70ec3c7c275fa3714412c8ff6e # 3.6.1 id: artifactory 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}-promoter access_token | ARTIFACTORY_PROMOTE_ACCESS_TOKEN; - uses: SonarSource/vault-action-wrapper@881045d830534a70ec3c7c275fa3714412c8ff6e # 3.6.1 diff --git a/shared/wait_artifactory_token_sync.sh b/shared/wait_artifactory_token_sync.sh new file mode 100755 index 00000000..68da8df5 --- /dev/null +++ b/shared/wait_artifactory_token_sync.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Wait until a SaaS-minted Artifactory token is accepted by an edge node. +# +# /api/system/ping is anonymous; use a protected storage API that validates the token. +# +# Required environment variables: +# - ARTIFACTORY_URL: Artifactory base URL (…/artifactory) +# - ARTIFACTORY_USERNAME: Artifactory username +# - ARTIFACTORY_ACCESS_TOKEN: Artifactory access token +# +# Optional: +# - REPOX_URL: Instance URL used to skip the wait for SaaS (jfrog.io) +# - ARTIFACTORY_TOKEN_SYNC_MAX_ATTEMPTS: Max poll attempts (default 60) +# - ARTIFACTORY_TOKEN_SYNC_SLEEP_SECONDS: Sleep between attempts (default 5) + +set -euo pipefail + +is_saas_repox() { + local url="${1:-}" + [[ "$url" == *jfrog.io* ]] +} + +require_artifactory_credentials() { + if [[ -z "${ARTIFACTORY_URL:-}" || -z "${ARTIFACTORY_USERNAME:-}" || -z "${ARTIFACTORY_ACCESS_TOKEN:-}" ]]; then + echo "::error title=Missing Artifactory credentials::Cannot wait for token sync without credentials" >&2 + return 1 + fi + return 0 +} + +check_artifactory_token() { + local check_url="$1" + local http_code + # Do not append a fallback via || echo: curl -w still emits http_code (often 000) on failure. + http_code="$(curl -sS -o /dev/null -w '%{http_code}' \ + --connect-timeout 10 --max-time 30 \ + -u "${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}" \ + "$check_url" || true)" + echo "${http_code:-000}" +} + +wait_for_artifactory_token_sync() { + local repox_url check_url max_attempts sleep_seconds attempt http_code + + repox_url="${REPOX_URL:-${ARTIFACTORY_URL:-}}" + if is_saas_repox "$repox_url"; then + echo "Skipping token sync wait for SaaS Repox ($repox_url)" + return 0 + fi + + require_artifactory_credentials || return 1 + + check_url="${ARTIFACTORY_URL%/}/api/storage/sonarsource-qa" + max_attempts="${ARTIFACTORY_TOKEN_SYNC_MAX_ATTEMPTS:-60}" + sleep_seconds="${ARTIFACTORY_TOKEN_SYNC_SLEEP_SECONDS:-5}" + attempt=0 + + echo "Waiting for Artifactory token federation sync at $check_url (up to $((max_attempts * sleep_seconds))s)" + + while true; do + attempt=$((attempt + 1)) + http_code="$(check_artifactory_token "$check_url")" + + if [[ "$http_code" == "200" ]]; then + echo "Artifactory accepted credentials after ${attempt} attempt(s) (HTTP 200)" + return 0 + fi + + if (( attempt >= max_attempts )); then + echo "::error title=Artifactory token sync timeout::Credentials were not accepted by $check_url" \ + "within $((max_attempts * sleep_seconds))s (last HTTP ${http_code})" >&2 + return 1 + fi + + echo "Attempt ${attempt}/${max_attempts}: HTTP ${http_code}, retrying in ${sleep_seconds}s..." + sleep "$sleep_seconds" + done +} + +main() { + wait_for_artifactory_token_sync +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main +fi diff --git a/spec/shared_wait_artifactory_token_sync_spec.sh b/spec/shared_wait_artifactory_token_sync_spec.sh new file mode 100644 index 00000000..420719d5 --- /dev/null +++ b/spec/shared_wait_artifactory_token_sync_spec.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 # ShellSpec DSL (Describe/It/Mock/End) is invoked indirectly +eval "$(shellspec - -c) exit 1" + +Describe 'shared/wait_artifactory_token_sync.sh' + It 'does not run main when sourced' + When run source shared/wait_artifactory_token_sync.sh + The status should be success + The lines of output should equal 0 + The lines of error should equal 0 + End +End + +Include shared/wait_artifactory_token_sync.sh + +Describe 'is_saas_repox()' + It 'detects jfrog.io SaaS URLs' + When call is_saas_repox 'https://repox.jfrog.io' + The status should be success + End + + It 'does not treat edge URLs as SaaS' + When call is_saas_repox 'https://repox-internal.dev.sonar.build' + The status should be failure + End +End + +Describe 'require_artifactory_credentials()' + It 'fails when credentials are missing' + unset ARTIFACTORY_URL ARTIFACTORY_USERNAME ARTIFACTORY_ACCESS_TOKEN + When call require_artifactory_credentials + The status should be failure + The stderr should include 'Missing Artifactory credentials' + End + + It 'succeeds when credentials are set' + export ARTIFACTORY_URL='https://repox-internal.dev.sonar.build/artifactory' + export ARTIFACTORY_USERNAME='user' + export ARTIFACTORY_ACCESS_TOKEN='token' + When call require_artifactory_credentials + The status should be success + End +End + +Describe 'check_artifactory_token()' + Mock curl + echo -n '200' + End + + It 'returns the curl http code' + export ARTIFACTORY_USERNAME='user' + export ARTIFACTORY_ACCESS_TOKEN='token' + When call check_artifactory_token 'https://example/artifactory/api/storage/sonarsource-qa' + The status should be success + The output should equal '200' + End +End + +Describe 'wait_for_artifactory_token_sync()' + Describe 'SaaS skip' + It 'skips when REPOX_URL is jfrog.io' + export REPOX_URL='https://repox.jfrog.io' + unset ARTIFACTORY_URL ARTIFACTORY_USERNAME ARTIFACTORY_ACCESS_TOKEN + When call wait_for_artifactory_token_sync + The status should be success + The output should include 'Skipping token sync wait for SaaS Repox' + End + End + + Describe 'missing credentials on edge' + It 'fails when edge URL has no credentials' + export REPOX_URL='https://repox-internal.dev.sonar.build' + unset ARTIFACTORY_URL ARTIFACTORY_USERNAME ARTIFACTORY_ACCESS_TOKEN + When call wait_for_artifactory_token_sync + The status should be failure + The stderr should include 'Missing Artifactory credentials' + End + End + + Describe 'successful sync' + Mock curl + echo -n '200' + End + Mock sleep + : + End + + It 'returns success on first HTTP 200' + export REPOX_URL='https://repox-internal.dev.sonar.build' + export ARTIFACTORY_URL='https://repox-internal.dev.sonar.build/artifactory' + export ARTIFACTORY_USERNAME='user' + export ARTIFACTORY_ACCESS_TOKEN='token' + When call wait_for_artifactory_token_sync + The status should be success + The output should include 'Artifactory accepted credentials after 1 attempt(s)' + End + End + + Describe 'retry then success' + setup_retry() { + CURL_ATTEMPT_FILE=$(mktemp) + echo 0 > "$CURL_ATTEMPT_FILE" + export CURL_ATTEMPT_FILE + return 0 + } + cleanup_retry() { + rm -f "$CURL_ATTEMPT_FILE" + return 0 + } + Before 'setup_retry' + After 'cleanup_retry' + + Mock curl + count=$(cat "$CURL_ATTEMPT_FILE") + count=$((count + 1)) + echo "$count" > "$CURL_ATTEMPT_FILE" + if (( count < 2 )); then + echo -n '401' + else + echo -n '200' + fi + End + Mock sleep + : + End + + It 'retries until HTTP 200' + export REPOX_URL='https://repox-internal.dev.sonar.build' + export ARTIFACTORY_URL='https://repox-internal.dev.sonar.build/artifactory' + export ARTIFACTORY_USERNAME='user' + export ARTIFACTORY_ACCESS_TOKEN='token' + export ARTIFACTORY_TOKEN_SYNC_MAX_ATTEMPTS=5 + export ARTIFACTORY_TOKEN_SYNC_SLEEP_SECONDS=0 + When call wait_for_artifactory_token_sync + The status should be success + The output should include 'HTTP 401' + The output should include 'Artifactory accepted credentials after 2 attempt(s)' + End + End + + Describe 'timeout' + Mock curl + echo -n '401' + End + Mock sleep + : + End + + It 'fails after max attempts' + export REPOX_URL='https://repox-internal.dev.sonar.build' + export ARTIFACTORY_URL='https://repox-internal.dev.sonar.build/artifactory' + export ARTIFACTORY_USERNAME='user' + export ARTIFACTORY_ACCESS_TOKEN='token' + export ARTIFACTORY_TOKEN_SYNC_MAX_ATTEMPTS=2 + export ARTIFACTORY_TOKEN_SYNC_SLEEP_SECONDS=0 + When call wait_for_artifactory_token_sync + The status should be failure + The stderr should include 'Artifactory token sync timeout' + The output should include 'HTTP 401' + End + End +End + +Describe 'main()' + Mock curl + echo -n '200' + End + + It 'delegates to wait_for_artifactory_token_sync' + export REPOX_URL='https://repox-internal.dev.sonar.build' + export ARTIFACTORY_URL='https://repox-internal.dev.sonar.build/artifactory' + export ARTIFACTORY_USERNAME='user' + export ARTIFACTORY_ACCESS_TOKEN='token' + When call main + The status should be success + The output should include 'Artifactory accepted credentials' + End +End + +Describe 'script execution' + Mock curl + echo -n '200' + End + + It 'runs successfully as a script' + export REPOX_URL='https://repox.jfrog.io' + When run script shared/wait_artifactory_token_sync.sh + The status should be success + The output should include 'Skipping token sync wait for SaaS Repox' + End +End