# yamllint disable rule:line-length --- name: Release reusable workflow on: workflow_call: inputs: version: type: string description: Full version including build number (e.g. 1.2.3.456). Required for v7 draft-first flow. required: true dryRun: type: boolean description: Flag to enable the dry-run execution default: false required: false createDraftRelease: type: boolean description: Create the draft release when it does not already exist default: true required: false publishToBinaries: type: boolean description: Flag to enable the publication to binaries default: false required: false publishJavadoc: type: boolean description: Flag to enable the javadoc publication default: false required: false javadocDestinationDirectory: type: string description: Name of the directory to use at https://javadocs.sonarsource.org/ when publishJavadoc is set to true required: false publicRelease: type: boolean description: For a mixed-privacy release (both org.sonarsource.* and com.sonarsource.* javadoc exist), also publish the private half; ignored otherwise -- a single-privacy build's javadoc is always published required: false default: false binariesS3Bucket: type: string description: Target bucket default: downloads-cdn-eu-central-1-prod required: false slackChannel: type: string description: Channel to post notifications default: build required: false vaultAddr: type: string description: Custom vault installation default: https://vault.sonar.build:8200 required: false artifactoryRoleSuffix: type: string description: Artifactory promoter suffix specified in Vault repo config default: promoter required: false mavenCentralSync: type: boolean description: Flag to enable synchronization to Maven Central default: false required: false mavenCentralSyncRequired: type: boolean description: If true, a Maven Central validation failure aborts and revokes the release; otherwise the release completes on Repox and binaries, and the job fails with the manual re-sync command default: false required: false mavenCentralSyncExclusions: type: string description: Deprecated, use mavenCentralSyncExclude. Glob pattern (matched against each artifact id) to exclude from the Maven Central sync default: "-" required: false mavenCentralSyncExclude: type: string description: "Comma-separated patterns of files left out of the Maven Central sync (replaces mavenCentralSyncExclusions): [group][:artifact[:classifier]][@extension] or artifactId=..:classifier=..:extension=.." default: "-" required: false mavenCentralSyncPlaceholderJavadoc: type: string description: Comma-separated [groupId]:artifactId patterns of the artifacts whose Javadoc jar is replaced by a placeholder jar in the Maven Central sync default: "-" required: false publishToPyPI: type: boolean description: Publish Python artifacts to https://pypi.org default: false required: false publishToTestPyPI: type: boolean description: Publish Python artifacts to https://test.pypi.org default: false required: false publishToNpmJS: type: boolean description: Publish Javascript artifacts to https://www.npmjs.com default: false required: false useNpmTrustedPublisher: type: boolean description: Use npm Trusted Publishers (OIDC) instead of Vault token for npm publish default: false required: false publishToCratesIo: type: boolean description: Publish Rust crates to https://crates.io default: false required: false skipPythonReleasabilityChecks: type: boolean description: Skip releasability checks for Python projects default: false required: false skipJavascriptReleasabilityChecks: type: boolean description: Skip releasability checks for javascript projects default: false required: false pushToDatadog: type: boolean description: Push results to datadog for monitoring default: true required: false isDummyProject: type: boolean description: Indicate whether this is a dummy project. default: false required: false githubEnvironment: type: string description: > GitHub Environment to set on OIDC-using jobs. Required when the calling repository configures OIDC subject claims to include 'environment' (include_claim_keys: [repo, environment]). Leave empty (default) for repositories using the standard subject claim format. required: false runnerLabel: type: string description: Runner label override for all jobs (empty preserves each job's existing runner) default: '' required: false jobs: release: name: Release runs-on: ${{ inputs.runnerLabel || 'github-ubuntu-latest-s' }} environment: ${{ inputs.githubEnvironment }} permissions: id-token: write # to authenticate via OIDC contents: write # to revert a github release timeout-minutes: 60 if: ${{ inputs.dryRun == true || inputs.version != '' }} outputs: releasability: ${{ steps.release.outputs.releasability }} promote: ${{ steps.release.outputs.promote }} publish_to_binaries: ${{ steps.release.outputs.publish_to_binaries }} release: ${{ steps.release.outputs.release }} project_name: ${{ steps.release.outputs.project_name }} maven_central_deployment_id: ${{ steps.release.outputs.maven_central_deployment_id }} maven_central_sync_pending: ${{ steps.release.outputs.maven_central_sync_pending }} releasabilityCheckDependencies: ${{ steps.releasability.outputs.releasabilityCheckDependencies }} releasabilityQA: ${{ steps.releasability.outputs.releasabilityQA }} releasabilityJira: ${{ steps.releasability.outputs.releasabilityJira }} releasabilityCheckPeacheeLanguagesStatistics: ${{ steps.releasability.outputs.releasabilityCheckPeacheeLanguagesStatistics }} releasabilityQualityGate: ${{ steps.releasability.outputs.releasabilityQualityGate }} releasabilityParentPOM: ${{ steps.releasability.outputs.releasabilityParentPOM }} releasabilityGitHub: ${{ steps.releasability.outputs.releasabilityGitHub }} releasabilityCheckManifestValues: ${{ steps.releasability.outputs.releasabilityCheckManifestValues }} steps: # Clone gh-action_release repository to run actions locally - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: ${{ job.workflow_repository }} ref: 1e9ca4fc6cd59b09cce9858064e99a55f1372a94 path: gh-action_release # Clone the calling repo for checking releasability prerequisites - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: path: ${{ github.event.repository.name }} - name: Create or reuse draft release id: draft if: ${{ inputs.dryRun != true }} env: GH_TOKEN: ${{ github.token }} VERSION: ${{ inputs.version }} CREATE_DRAFT_RELEASE: ${{ inputs.createDraftRelease }} run: | set -euo pipefail if release_details="$(gh release view "$VERSION" --repo "${{ github.repository }}" --json id,isDraft 2>/dev/null)"; then release_id=$(jq -r '.id' <<< "$release_details") release_is_draft=$(jq -r '.isDraft' <<< "$release_details") if [[ "$release_is_draft" == "true" ]]; then echo "Reusing existing draft release $VERSION" else echo "::error::Release $VERSION already exists and is NOT a draft. Aborting to avoid duplicate publish." exit 1 fi elif [[ "$CREATE_DRAFT_RELEASE" == "false" ]]; then echo "::error::Draft release $VERSION does not exist and createDraftRelease=false. Create the draft release before running this workflow." exit 1 else gh release create "$VERSION" \ --repo "${{ github.repository }}" \ --draft --generate-notes \ --target "$GITHUB_SHA" echo "Created draft release $VERSION" release_id=$(gh release view "$VERSION" --repo "${{ github.repository }}" --json id -q '.id') fi echo "release_id=$release_id" >> "$GITHUB_OUTPUT" - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-region: eu-central-1 role-to-assume: "arn:aws:iam::064493320159:role/deploymentroles/ReleasbilityChecksCICDRoleV2" - name: Vault Secrets id: secrets if: ${{ inputs.dryRun != true }} uses: SonarSource/vault-action-wrapper@0a3114fe1230b784c35b53b099f9ab1f1e538cc7 # 3.5.0 with: url: ${{ inputs.vaultAddr }} secrets: | development/artifactory/token/{REPO_OWNER_NAME_DASH}-${{ inputs.artifactoryRoleSuffix }} access_token | artifactory_access_token; development/kv/data/slack token | slack_api_token; development/kv/data/slack webhook | slack_webhook; development/kv/data/repox url | artifactory_url; - name: Vault Binaries AWS Secrets id: secrets-binaries-aws if: ${{ inputs.dryRun != true && inputs.publishToBinaries }} uses: SonarSource/vault-action-wrapper@0a3114fe1230b784c35b53b099f9ab1f1e538cc7 # 3.5.0 with: url: ${{ inputs.vaultAddr }} secrets: | development/aws/sts/downloads access_key | binaries_aws_access_key_id; development/aws/sts/downloads secret_key | binaries_aws_secret_access_key; development/aws/sts/downloads security_token | binaries_aws_security_token; - name: Vault Maven Central Secrets id: secrets-maven-central if: ${{ inputs.dryRun != true && inputs.mavenCentralSync }} uses: SonarSource/vault-action-wrapper@0a3114fe1230b784c35b53b099f9ab1f1e538cc7 # 3.5.0 with: url: ${{ inputs.vaultAddr }} secrets: | development/kv/data/ossrh token | central_token; - name: Parse Vault Output id: parse_vault env: DUMMY_VALUES: ${{ inputs.dryRun }} SECRET_OUTPUTS: ${{ toJson(steps.secrets.outputs) }} SECRET_AWS_OUTPUTS: ${{ toJson(steps.secrets-binaries-aws.outputs) }} SECRET_MAVEN_CENTRAL_OUTPUTS: ${{ toJson(steps.secrets-maven-central.outputs) }} run: | if [[ "${DUMMY_VALUES}" == "true" ]]; then { echo "artifactory_access_token=test" echo "slack_api_token=test" echo "binaries_aws_access_key_id=test" echo "binaries_aws_secret_access_key=test" echo "binaries_aws_security_token=test" } >> "${GITHUB_OUTPUT}" exit 0 fi echo "${SECRET_OUTPUTS}" | jq -r '.vault | fromjson | to_entries[] | (.key + "<> "${GITHUB_OUTPUT}" if [[ "${SECRET_AWS_OUTPUTS}" != "{}" ]]; then echo "${SECRET_AWS_OUTPUTS}" | jq -r '.vault | fromjson | to_entries[] | (.key + "<> "${GITHUB_OUTPUT}" fi if [[ "${SECRET_MAVEN_CENTRAL_OUTPUTS}" != "{}" ]]; then echo "${SECRET_MAVEN_CENTRAL_OUTPUTS}" \ | jq -r '.vault | fromjson | to_entries[] | (.key + "<> "${GITHUB_OUTPUT}" fi - name: Releasability check prerequisites id: releasability_prerequisites env: SKIP_PYTHON_RELEASABILITY_CHECKS: ${{ inputs.skipPythonReleasabilityChecks }} SKIP_JAVASCRIPT_RELEASABILITY_CHECKS: ${{ inputs.skipJavascriptReleasabilityChecks }} run: | # Skip releasability checks if the project contains pyproject.toml file if test -f "${{ github.event.repository.name }}/pyproject.toml" && "$SKIP_PYTHON_RELEASABILITY_CHECKS" == "true" ; then echo "skip_releasability_checks=true" >> "${GITHUB_OUTPUT}" elif test -f "${{ github.event.repository.name }}/package.json" && "$SKIP_JAVASCRIPT_RELEASABILITY_CHECKS" == "true" ; then echo "skip_releasability_checks=true" >> "${GITHUB_OUTPUT}" else echo "skip_releasability_checks=false" >> "${GITHUB_OUTPUT}" fi - uses: SonarSource/gh-action_releasability@52f09917764eac5a80045d103bfa91e7eaf0c8d6 # 3.0.5 id: releasability if: ${{ steps.releasability_prerequisites.outputs.skip_releasability_checks != 'true' && inputs.dryRun != true }} with: branch: ${{ github.ref_name }} commit-sha: ${{ github.sha }} organization: ${{ github.repository_owner }} repository: ${{ github.event.repository.name }} version: ${{ inputs.version }} - name: Release rollback notice if: failure() && steps.releasability.outcome == 'failure' env: VERSION: ${{ inputs.version }} run: | echo "::warning::Release $VERSION saved as draft. Fix the root cause and 'Re-run jobs'. No need for a new build." - name: Send Slack notification on releasability check failure if: failure() && steps.releasability.outcome == 'failure' uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 with: webhook: ${{ steps.parse_vault.outputs.slack_webhook }} webhook-type: incoming-webhook payload: | { "channel": "${{ inputs.slackChannel }}", "attachments": [ { "color": "#ff0000", "text": "Releasability checks failed in `${{ github.repository }}`. Fix the root cause and retry without rebuilding.\n*To retry:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Rerun release workflow `${{ inputs.version }}`>\n*Draft:* <${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.version }}|View draft release `${{ inputs.version }}`>\n*To abandon:* `gh release delete ${{ inputs.version }} --cleanup-tag --yes`" } ] } - name: Release ${{ inputs.version }} id: release uses: ./gh-action_release/main with: publish_to_binaries: ${{ inputs.publishToBinaries }} # Used only if the binaries are delivered to customers slack_channel: ${{ inputs.slackChannel }} dry_run: ${{ inputs.dryRun }} maven_central_sync: ${{ inputs.mavenCentralSync }} maven_central_sync_required: ${{ inputs.mavenCentralSyncRequired }} maven_central_sync_exclusions: ${{ inputs.mavenCentralSyncExclusions }} maven_central_sync_exclude: ${{ inputs.mavenCentralSyncExclude }} maven_central_sync_placeholder_javadoc: ${{ inputs.mavenCentralSyncPlaceholderJavadoc }} is_dummy_project: ${{ inputs.isDummyProject }} env: PYTHONUNBUFFERED: 1 INPUT_VERSION: ${{ inputs.version }} ARTIFACTORY_ACCESS_TOKEN: ${{ steps.parse_vault.outputs.artifactory_access_token }} BINARIES_AWS_DEPLOY: ${{ inputs.binariesS3Bucket }} # Required for pushing the binaries GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SLACK_API_TOKEN: ${{ steps.parse_vault.outputs.slack_api_token }} BINARIES_AWS_ACCESS_KEY_ID: ${{ steps.parse_vault.outputs.binaries_aws_access_key_id }} BINARIES_AWS_SECRET_ACCESS_KEY: ${{ steps.parse_vault.outputs.binaries_aws_secret_access_key }} BINARIES_AWS_SESSION_TOKEN: ${{ steps.parse_vault.outputs.binaries_aws_security_token }} BINARIES_AWS_DEFAULT_REGION: eu-central-1 CENTRAL_TOKEN: ${{ steps.parse_vault.outputs.central_token }} - name: Release action results if: always() run: | echo "${{ steps.releasability.outputs.status }}" echo "${{ steps.release.outputs.promote }}" echo "${{ steps.release.outputs.publish_to_binaries }}" echo "${{ steps.release.outputs.release }}" javadocPublication: name: Javadoc publication needs: release if: ${{ !cancelled() && inputs.publishJavadoc && inputs.dryRun != true && (needs.release.result == 'success' || needs.release.outputs.maven_central_sync_pending == 'true') }} uses: ./.github/workflows/javadoc-publication.yaml with: version: ${{ inputs.version }} dryRun: ${{ inputs.dryRun }} vaultAddr: ${{ inputs.vaultAddr }} artifactoryRoleSuffix: ${{ inputs.artifactoryRoleSuffix }} javadocDestinationDirectory: ${{ inputs.javadocDestinationDirectory }} publicRelease: ${{ inputs.publicRelease }} githubEnvironment: ${{ inputs.githubEnvironment }} runnerLabel: ${{ inputs.runnerLabel }} testPypi: name: TestPyPI needs: release if: ${{ !cancelled() && inputs.publishToTestPyPI && inputs.dryRun != true && (needs.release.result == 'success' || needs.release.outputs.maven_central_sync_pending == 'true') }} uses: ./.github/workflows/pypi.yaml with: version: ${{ inputs.version }} vaultAddr: ${{ inputs.vaultAddr }} vaultTokenKey: pypi-test artifactoryRoleSuffix: ${{ inputs.artifactoryRoleSuffix }} pypiRepoUrl: "https://test.pypi.org/legacy/" slackChannel: ${{ inputs.slackChannel }} githubEnvironment: ${{ inputs.githubEnvironment }} runnerLabel: ${{ inputs.runnerLabel }} pypi: name: PyPi needs: release if: ${{ !cancelled() && inputs.publishToPyPI && inputs.dryRun != true && (needs.release.result == 'success' || needs.release.outputs.maven_central_sync_pending == 'true') }} uses: ./.github/workflows/pypi.yaml with: version: ${{ inputs.version }} vaultAddr: ${{ inputs.vaultAddr }} artifactoryRoleSuffix: ${{ inputs.artifactoryRoleSuffix }} slackChannel: ${{ inputs.slackChannel }} githubEnvironment: ${{ inputs.githubEnvironment }} runnerLabel: ${{ inputs.runnerLabel }} npmjs: name: NpmJS needs: release if: ${{ !cancelled() && inputs.publishToNpmJS && inputs.dryRun != true && (needs.release.result == 'success' || needs.release.outputs.maven_central_sync_pending == 'true') }} uses: ./.github/workflows/npmjs.yaml with: version: ${{ inputs.version }} vaultAddr: ${{ inputs.vaultAddr }} artifactoryRoleSuffix: ${{ inputs.artifactoryRoleSuffix }} slackChannel: ${{ inputs.slackChannel }} useNpmTrustedPublisher: ${{ inputs.useNpmTrustedPublisher }} githubEnvironment: ${{ inputs.githubEnvironment }} runnerLabel: ${{ inputs.runnerLabel }} cratesIo: name: crates.io needs: release if: ${{ !cancelled() && inputs.publishToCratesIo && inputs.dryRun != true && (needs.release.result == 'success' || needs.release.outputs.maven_central_sync_pending == 'true') }} uses: ./.github/workflows/cratesio.yaml with: version: ${{ inputs.version }} vaultAddr: ${{ inputs.vaultAddr }} artifactoryRoleSuffix: ${{ inputs.artifactoryRoleSuffix }} slackChannel: ${{ inputs.slackChannel }} githubEnvironment: ${{ inputs.githubEnvironment }} runnerLabel: ${{ inputs.runnerLabel }} publish: name: Publish draft release runs-on: ${{ inputs.runnerLabel || 'sonar-xs' }} needs: - release - javadocPublication - testPypi - pypi - npmjs - cratesIo if: >- ${{ !cancelled() && inputs.dryRun != true && (needs.release.result == 'success' || needs.release.outputs.maven_central_sync_pending == 'true') && needs.javadocPublication.result != 'failure' && needs.testPypi.result != 'failure' && needs.pypi.result != 'failure' && needs.npmjs.result != 'failure' && needs.cratesIo.result != 'failure' }} permissions: contents: write steps: - name: Promote draft to published (immutable) env: GH_TOKEN: ${{ github.token }} INPUT_VERSION: ${{ inputs.version }} run: | gh release edit "$INPUT_VERSION" --repo "${{ github.repository }}" --draft=false datadog: name: Push results to datadog runs-on: ${{ inputs.runnerLabel || 'github-ubuntu-latest-s' }} environment: ${{ inputs.githubEnvironment }} needs: - release - javadocPublication - testPypi - pypi - npmjs - cratesIo - publish if: ${{ always() && inputs.pushToDatadog && inputs.dryRun != true }} steps: # Clone gh-action_release repository to run actions locally - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: ${{ job.workflow_repository }} ref: 1e9ca4fc6cd59b09cce9858064e99a55f1372a94 path: gh-action_release - name: Vault id: secrets uses: SonarSource/vault-action-wrapper@0a3114fe1230b784c35b53b099f9ab1f1e538cc7 # 3.5.0 with: url: ${{ inputs.vaultAddr }} secrets: development/kv/data/datadog token | datadog_token; - name: Push to datadog id: datadog env: datadog_token: ${{ fromJSON(steps.secrets.outputs.vault).datadog_token }} releasabilityCheckDependencies: ${{ needs.release.outputs.releasabilityCheckDependencies }} releasabilityQA: ${{ needs.release.outputs.releasabilityQA }} releasabilityJira: ${{ needs.release.outputs.releasabilityJira }} releasabilityCheckPeacheeLanguagesStatistics: ${{ needs.release.outputs.releasabilityCheckPeacheeLanguagesStatistics }} releasabilityQualityGate: ${{ needs.release.outputs.releasabilityQualityGate }} releasabilityParentPOM: ${{ needs.release.outputs.releasabilityParentPOM }} releasabilityGitHub: ${{ needs.release.outputs.releasabilityGitHub }} releasabilityCheckManifestValues: ${{ needs.release.outputs.releasabilityCheckManifestValues }} release_passed: ${{ needs.release.result }} maven_central_published: >- ${{ needs.release.outputs.maven_central_deployment_id != '' && 'success' || (inputs.mavenCentralSync && 'failure' || 'skipped') }} javadoc_published: ${{ needs.javadocPublication.result }} testpypi_published: ${{ needs.testPypi.result }} pypi_published: ${{ needs.pypi.result }} npmjs_published: ${{ needs.npmjs.result }} cratesio_published: ${{ needs.cratesIo.result }} status: >- ${{ job.status == 'success' && needs.release.result == 'success' && needs.javadocPublication.result != 'failure' && needs.testPypi.result != 'failure' && needs.pypi.result != 'failure' && needs.npmjs.result != 'failure' && needs.cratesIo.result != 'failure' && needs.publish.result != 'failure' }} repo: ${{ github.repository }} run_id: ${{ github.run_id }} is_dummy_project: ${{ inputs.isDummyProject }} run: python ./gh-action_release/datadog-ingest/main.py