From c897df55b9c572d0f0b29f2df818ed4dc7bd9c4f Mon Sep 17 00:00:00 2001 From: Julien Carsique Date: Thu, 30 Apr 2026 18:08:12 +0200 Subject: [PATCH] BUILD-11107 Switch release workflow to workflow_dispatch Replace the release:published event trigger with workflow_dispatch so the SBOM can be attached before the release is published, avoiding GitHub Release Immutability restrictions (HTTP 422 on asset upload to published releases). The workflow now creates the git tag at HEAD, delegates SBOM generation to SonarSource/gh-action_sbom (which auto-creates a draft release), then publishes the release once all assets are attached. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/release.yml | 56 +++++++++++++++++++++++++---------- DEVELOPER.md | 26 ++++++++++++++++ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 079c49e..f32ba39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,12 @@ name: Release on: - release: - types: - - published + workflow_dispatch: + inputs: + tag_name: + description: "Release tag (format: {major}.{minor}.{patch}.{build}_{scanner_major}.{scanner_minor}.{scanner_patch})" + required: true + type: string jobs: release: @@ -26,27 +29,41 @@ jobs: development/kv/data/slack webhook | slack_webhook; - name: Get the version id: get_version + shell: bash + env: + TAG_NAME: ${{ inputs.tag_name }} run: | - full_image_tag=${{ github.event.release.tag_name }} - if [[ ! ${full_image_tag} =~ ^[1-9][0-9]+.[0-9]+.[0-9]+.[0-9]+_[0-9]+.[0-9]+.[0-9]+ ]]; then + full_image_tag=$TAG_NAME + if [[ ! ${full_image_tag} =~ ^[1-9][0-9]*\.[0-9]+\.[0-9]+\.[0-9]+_[0-9]+\.[0-9]+\.[0-9]+ ]]; then echo "The release tag should be in the format of {major}.{minor}.{patch}.{buildnumber}_{scanner_major}.{scanner_minor}.{scanner_patch} but it was ${full_image_tag}" exit 1 fi - + IFS=. read docker_major docker_minor docker_patch build_and_scanner <<<"${full_image_tag}" IFS=_ read buildnumber scanner_version <<<"${build_and_scanner}" echo "major_version=${docker_major}" >> $GITHUB_OUTPUT echo "major_minor=${docker_major}.${docker_minor}" >> $GITHUB_OUTPUT echo "full_image_tag=${full_image_tag}" >> $GITHUB_OUTPUT echo "buildnumber=${buildnumber}" >> $GITHUB_OUTPUT - shell: bash - - uses: actions/checkout@v2 - with: - ref: ${{ github.event.release.tag_name }} - - uses: actions/checkout@v2 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 with: repository: SonarSource/sonar-scanning-examples path: target_repository + - name: Create and push release tag + env: + TAG_NAME: ${{ inputs.tag_name }} + run: | + if git ls-remote --exit-code --tags origin "$TAG_NAME" > /dev/null 2>&1; then + echo "Tag '$TAG_NAME' already exists on origin. To retry, clean up the partial release first:" + echo " 1. Delete the tag: git push origin :refs/tags/$TAG_NAME" + echo " 2. If a draft GitHub release exists for '$TAG_NAME', delete it before re-dispatching." + exit 1 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "$TAG_NAME" -m "Release $TAG_NAME" + git push origin "$TAG_NAME" - name: Pull staged image run: | docker login repox-sonarsource-docker-builds.jfrog.io --username ${{ fromJSON(steps.secrets.outputs.vault).repox_username }} --password-stdin <<< "${{ fromJSON(steps.secrets.outputs.vault).repox_access_token }}" @@ -58,21 +75,23 @@ jobs: filename: "sonar-scanner-cli-docker-${{ steps.get_version.outputs.full_image_tag }}-bom.json" upload-artifact: true upload-release-assets: true + release-tag: ${{ inputs.tag_name }} env: GPG_PRIVATE_KEY_PASSPHRASE: ${{ fromJSON(steps.secrets.outputs.vault).gpg_passphrase }} GPG_PRIVATE_KEY_BASE64: ${{ fromJSON(steps.secrets.outputs.vault).gpg_key }} - name: Promote the staged build env: ARTIFACTORY_URL: https://repox.jfrog.io/repox + TAG_NAME: ${{ inputs.tag_name }} run: | source_repo_key=sonarsource-docker-builds target_repo_key=sonarsource-docker-releases docker_image=sonarsource/sonar-scanner-cli buildnumber=${{ steps.get_version.outputs.buildnumber }} - full_image_tag=${{ github.event.release.tag_name }} + full_image_tag=$TAG_NAME DATA_JSON="{ \"targetRepo\": \"${target_repo_key}\", \"dockerRepository\": \"${docker_image}\", \"tag\": \"${buildnumber}\", \"targetTag\": \"${full_image_tag}\", \"copy\": true }" HTTP_CODE=$(curl -s -o /dev/null -w %{http_code} -H "Content-Type: application/json" -H "Authorization: Bearer ${{ fromJSON(steps.secrets.outputs.vault).repox_access_token }}" -X POST "$ARTIFACTORY_URL/api/docker/$source_repo_key/v2/promote" --data "$DATA_JSON") - if [ "$HTTP_CODE" != "200" ]; then + if [[ "$HTTP_CODE" != "200" ]]; then echo "Cannot promote ${docker_image}#${full_image_tag}: ($HTTP_CODE)" exit 1 else @@ -81,7 +100,7 @@ jobs: - name: Push image to Docker Hub run: | buildnumber=${{ steps.get_version.outputs.buildnumber }} - + docker tag "repox-sonarsource-docker-builds.jfrog.io/sonarsource/sonar-scanner-cli:${buildnumber}" "sonarsource/sonar-scanner-cli:latest" docker tag "repox-sonarsource-docker-builds.jfrog.io/sonarsource/sonar-scanner-cli:${buildnumber}" "sonarsource/sonar-scanner-cli:${{ steps.get_version.outputs.major_version }}" docker tag "repox-sonarsource-docker-builds.jfrog.io/sonarsource/sonar-scanner-cli:${buildnumber}" "sonarsource/sonar-scanner-cli:${{ steps.get_version.outputs.major_minor }}" @@ -93,6 +112,11 @@ jobs: docker push sonarsource/sonar-scanner-cli:${{ steps.get_version.outputs.major_version }} docker push sonarsource/sonar-scanner-cli:${{ steps.get_version.outputs.major_minor }} docker push sonarsource/sonar-scanner-cli:${{ steps.get_version.outputs.full_image_tag }} + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ inputs.tag_name }} + run: gh release edit "$TAG_NAME" --draft=false - name: Notify success on Slack uses: slackapi/slack-github-action@v2.0.0 with: @@ -104,7 +128,7 @@ jobs: "attachments": [ { "color": "#00ff00", - "text": "Release `${{ github.event.release.tag_name }}` successful for `${{ github.repository }}` by `${{ github.actor }}`.\n <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Link to workflow run>" + "text": "Release `${{ inputs.tag_name }}` successful for `${{ github.repository }}` by `${{ github.actor }}`.\n <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Link to workflow run>" } ] } @@ -120,7 +144,7 @@ jobs: "attachments": [ { "color": "#ff0000", - "text": "Release `${{ github.event.release.tag_name }}` failed for `${{ github.repository }}` by `${{ github.actor }}`.\n <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Link to workflow run>" + "text": "Release `${{ inputs.tag_name }}` failed for `${{ github.repository }}` by `${{ github.actor }}`.\n <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Link to workflow run>" } ] } diff --git a/DEVELOPER.md b/DEVELOPER.md index ef76812..ec6db77 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -75,3 +75,29 @@ The QA process is handled on `.cirrus.yml`, which is responsible for the followi - build the image - test the image by running a scan on a sample project - run scans to find potential vulnerabilities + +## Releasing + +Releases are triggered manually through the `Release` workflow (`.github/workflows/release.yml`). + +### How to release + +1. Go to the [Release workflow](../../actions/workflows/release.yml) on GitHub Actions. +2. Click **Run workflow** and select the branch to release from (see [Branch to dispatch from](#branch-to-dispatch-from) below). +3. Provide the `tag_name` input in the format `{major}.{minor}.{patch}.{build}_{scanner_major}.{scanner_minor}.{scanner_patch}` (e.g. `4.8.0.2699_6.2.1`). + +The workflow validates the tag format, creates and pushes the git tag at HEAD of the dispatched branch, generates the SBOM, promotes the staged Docker image, pushes it to Docker Hub, and finally publishes the GitHub release. + +### Branch to dispatch from + +The git tag is created at HEAD of the branch the workflow is dispatched from. Choose the branch accordingly: + +- **Latest release**: dispatch from `master`. +- **Maintenance release on a long-lived branch** (e.g. `branch-4.8`): dispatch from that `branch-*` branch, **not** from `master`. Dispatching from `master` would tag a commit that does not belong to the maintenance line. + +### Recovering from a failed release + +If the workflow fails after the git tag has been pushed, re-dispatching with the same tag will fail at the pre-flight check. To recover: + +1. Delete the tag from the remote: `git push origin :refs/tags/` +2. If a draft GitHub release was created for that tag, delete it from the [Releases page](../../releases) before re-dispatching.