From bc07dfe23382c4cf82de96e73e5e9a30d1d583d4 Mon Sep 17 00:00:00 2001 From: Jayadeep Kinavoor Madam Date: Thu, 28 May 2026 17:54:09 +0200 Subject: [PATCH 1/4] BUILD-11462: Add README for update-release-channel action --- update-release-channel/README.md | 191 +++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 update-release-channel/README.md diff --git a/update-release-channel/README.md b/update-release-channel/README.md new file mode 100644 index 00000000..ace9326e --- /dev/null +++ b/update-release-channel/README.md @@ -0,0 +1,191 @@ +# Update release channel + +GitHub Action that maintains per-channel JSON pointer files on `binaries.sonarsource.com` so consumers can discover the +currently published version for each release channel of a product. + +For example, a release pipeline can call this action after publishing `Distribution/sonarqube-cli/0.9.0.977/` to +update `Distribution/sonarqube-cli/latest.json` so install scripts, homebrew formulas, and the CLI's own auto-update +check can resolve the current version with a single HTTP GET. + +The action writes one JSON file per channel (atomic per channel, single S3 `PutObject`). Lifecycle operations like +rollback, delayed promotion, and backfill are first-class — they're a single `workflow_dispatch` invocation away, +without re-running the release pipeline. + +The JSON body follows the [v1 schema](./schema/v1.json); see [schema/README.md](./schema/README.md) for the field +contract. + +## Inputs + +| Input | Required | Default | Description | +| --------- | -------- | ---------------------------------------- | -------------------------------------------------------------------------- | +| `version` | yes | — | Version the channel should point at (e.g. `0.9.0.977`). | +| `channel` | no | `latest` | Release channel name. One of `latest`, `stable`, `beta`, `rc`. | +| `prefix` | no | `Distribution` | S3 key prefix under the bucket. Other values warn but are accepted. | +| `product` | no | `${{ github.event.repository.name }}` | Product folder name on S3. | +| `dryRun` | no | `false` | Resolve and validate inputs, print the planned `PutObject`, skip the call. | + +### When to set `product:` explicitly + +The default (`${{ github.event.repository.name }}`) works whenever the GitHub repository name matches the product +folder on S3. Set `product:` explicitly when they differ — for example, `sonar-dummy-gradle-oss` publishes to +`Distribution/sonar-dummy-gradle-oss-plugin/` and must pass `product: sonar-dummy-gradle-oss-plugin`. + +## Outputs + +| Output | Description | +| -------- | ------------------------------------------------------------------------------------------- | +| `bucket` | S3 bucket the channel pointer was (or would be) written to. | +| `key` | S3 key of the channel pointer (e.g. `Distribution//.json`). | +| `url` | Public URL of the channel pointer. | +| `body` | JSON body written (or that would be written) to S3. Useful for schema validation in tests. | + +## Usage + +### Release-workflow follow-up job (automated) + +The standard pattern: append an `update-channel` job to the release workflow so the `latest` channel is promoted +automatically after a successful publish. No environment gate — the gate is the release itself. + +```yaml +# .github/workflows/release.yml +jobs: + release: + uses: SonarSource/gh-action_release/.github/workflows/main.yaml@7.0.1 + with: { ... } + + update-channel: + needs: release + if: ${{ !inputs.dryRun }} + runs-on: sonar-xs + permissions: + id-token: write # OIDC → Vault + contents: read + steps: + - uses: SonarSource/ci-github-actions/update-release-channel@v1 + with: + version: ${{ inputs.version }} + # channel defaults to "latest"; prefix to "Distribution"; product to the repo name. +``` + +### Standalone `workflow_dispatch` (manual ops) + +A separate workflow for rollback, delayed promotion, backfill, and channel migration. Gated by a GitHub Environment +with required reviewers so every manual write is an approved action. + +```yaml +# .github/workflows/update-release-channel.yml +on: + workflow_dispatch: + inputs: + version: { required: true, type: string } + channel: { required: true, type: choice, options: [stable, beta, rc] } + +jobs: + update-channel: + runs-on: sonar-xs + environment: release-channel-admin # required reviewers — see below + permissions: + id-token: write + contents: read + steps: + - uses: SonarSource/ci-github-actions/update-release-channel@v1 + with: + version: ${{ inputs.version }} + channel: ${{ inputs.channel }} +``` + +The `latest` channel is intentionally omitted from the manual choices — promote `latest` only via the automated +release follow-up job. + +## Required GitHub Environment + +The manual-ops workflow MUST be gated by a GitHub Environment configured with **required reviewers**. Without that +gate, anyone with write access to the repo could re-point a production channel. + +### How to create the environment + +1. In the repo, go to **Settings → Environments → New environment**. +2. Name it `release-channel-admin`. +3. Under **Deployment protection rules**, enable **Required reviewers** and add the reviewer allowlist (see below). +4. Save. + +Optional: restrict the environment to specific branches under **Deployment branches** if your manual-ops workflow +should only be dispatchable from `master` / `main`. + +### Recommended reviewer allowlist + +- Release captains / squad leads of the consuming product +- Plus a backup from the platform / release engineering team for breakglass + +### Rationale + +The action's STS credentials are broadly scoped (the Vault preset `development/aws/sts/downloads` can write anywhere +under `downloads-cdn-eu-central-1-prod`). The in-action destination-prefix guardrail mitigates path traversal, but a +mistaken `version` or `channel` in a manual dispatch can still corrupt a production pointer. Requiring an approver +ensures a second pair of eyes on every non-automated write, and ensures no Vault token is even fetched until the +dispatch is approved in the GitHub UI. + +## Operational recipes + +All manual operations use the standalone `workflow_dispatch` workflow above. Dispatch from **Actions → Update release +channel → Run workflow**, fill the inputs, and wait for the environment approver to click **Approve**. + +### Rollback + +To re-point a channel at the previously-known-good version: + +1. Identify the prior version (from release notes, `git tag`, or the previous `.json` body cached locally). +2. Dispatch with `version: ` and `channel: `. + +Because writes are atomic per channel (single `PutObject`), rollback affects only the targeted channel; other +channels are untouched. The `max-age=60` cache header ensures consumers pick up the rollback within a minute. + +### Delayed promotion + +To promote `stable` (or any non-`latest` channel) some time after the release published `latest`: + +- Dispatch with `version: ` and `channel: stable` whenever the team is ready. + +This is just the manual-ops workflow's normal use — no special handling required. + +### Backfill + +When onboarding the action against an existing product that has prior releases, backfill the channel pointer for the +currently-published version with one dispatch (`version: `, `channel: `). Repeat per channel. + +### Channel migration + +To deprecate a channel name (e.g. retire `rc` in favour of `beta`), backfill `beta.json` to match `rc.json`'s current +value via one dispatch. The action does not delete channel files; if you need a channel JSON removed, do it +out-of-band via the AWS console / CLI. + +## Limitations + +### Publish and channel update are not transactional + +The release pipeline (publish to `Distribution///`) and the `update-release-channel` job run as +separate steps. If publish succeeds but the channel update fails (for example, transient AWS error, expired Vault +token), the artifacts are live at the versioned URL but `.json` still points at the previous version. + +Consumers reading `.json` keep seeing the old version until the channel update is retried — they don't see +a half-promoted state, just the previous one. + +### Recovery + +Re-run the channel update via the manual-ops workflow: + +1. Dispatch with the same `version` and `channel` that the failed automated job tried. +2. Approve the environment gate. + +Because the JSON body is regenerated on each invocation, repeating the operation is idempotent. The artifacts at +`Distribution///` are already published; only the pointer needs to catch up. + +## Implementation details + +- **Bucket:** `downloads-cdn-eu-central-1-prod` (served at `https://binaries.sonarsource.com/`) +- **Vault preset:** `development/aws/sts/downloads` (the same preset used by `gh-action_release`) +- **Cache-Control:** `max-age=60` on every write — short enough that rollbacks propagate quickly, long enough to keep + the CDN happy. +- **Destination guardrail:** the script refuses to write outside `//.json` and validates + `` against `^[a-z0-9][a-z0-9._-]*$`. A custom `prefix` other than `Distribution` is accepted with a loud + warning. From ce99ca18f40d84d635296b43c742beb656a2364e Mon Sep 17 00:00:00 2001 From: Jayadeep Kinavoor Madam Date: Thu, 28 May 2026 18:00:23 +0200 Subject: [PATCH 2/4] BUILD-11462: Add 'dogfood' to allowed channels --- update-release-channel/README.md | 2 +- update-release-channel/action.yml | 2 +- update-release-channel/update-release-channel.sh | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/update-release-channel/README.md b/update-release-channel/README.md index ace9326e..cbe17a56 100644 --- a/update-release-channel/README.md +++ b/update-release-channel/README.md @@ -19,7 +19,7 @@ contract. | Input | Required | Default | Description | | --------- | -------- | ---------------------------------------- | -------------------------------------------------------------------------- | | `version` | yes | — | Version the channel should point at (e.g. `0.9.0.977`). | -| `channel` | no | `latest` | Release channel name. One of `latest`, `stable`, `beta`, `rc`. | +| `channel` | no | `latest` | Release channel name. One of `latest`, `stable`, `beta`, `rc`, `dogfood`. | | `prefix` | no | `Distribution` | S3 key prefix under the bucket. Other values warn but are accepted. | | `product` | no | `${{ github.event.repository.name }}` | Product folder name on S3. | | `dryRun` | no | `false` | Resolve and validate inputs, print the planned `PutObject`, skip the call. | diff --git a/update-release-channel/action.yml b/update-release-channel/action.yml index 29fe16b7..1ee88c62 100644 --- a/update-release-channel/action.yml +++ b/update-release-channel/action.yml @@ -8,7 +8,7 @@ inputs: description: Version that the channel should be updated to point at (e.g. `0.9.0.977`). required: true channel: - description: Release channel name. One of `latest`, `stable`, `beta`, `rc`. + description: Release channel name. One of `latest`, `stable`, `beta`, `rc`, `dogfood`. default: latest prefix: description: S3 key prefix under the bucket. Defaults to `Distribution` (existing layout convention). diff --git a/update-release-channel/update-release-channel.sh b/update-release-channel/update-release-channel.sh index ed4dfeec..2f0ea2d3 100755 --- a/update-release-channel/update-release-channel.sh +++ b/update-release-channel/update-release-channel.sh @@ -21,8 +21,8 @@ set -euo pipefail readonly BUCKET="downloads-cdn-eu-central-1-prod" readonly PUBLIC_BASE_URL="https://binaries.sonarsource.com" -[[ "$CHANNEL" =~ ^(latest|stable|beta|rc)$ ]] \ - || { echo "::error::Invalid channel '$CHANNEL'. Must be one of: latest, stable, beta, rc." >&2; exit 1; } +[[ "$CHANNEL" =~ ^(latest|stable|beta|rc|dogfood)$ ]] \ + || { echo "::error::Invalid channel '$CHANNEL'. Must be one of: latest, stable, beta, rc, dogfood." >&2; exit 1; } [[ "$PRODUCT" =~ ^[a-z0-9][a-z0-9._-]*$ ]] \ || { echo "::error::Invalid product '$PRODUCT'. Must match ^[a-z0-9][a-z0-9._-]*\$." >&2; exit 1; } [[ "$PREFIX" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] \ From e7c2fb046a2573924527fd3eef1c7533f33e9301 Mon Sep 17 00:00:00 2001 From: Jayadeep Kinavoor Madam Date: Thu, 28 May 2026 18:09:06 +0200 Subject: [PATCH 3/4] BUILD-11462: Align action README with repo conventions; add to root README - Restructure update-release-channel/README.md to mirror other actions: H2 sections (Requirements/Usage/Inputs/Outputs), H3 subsections, Required GitHub Permissions / Vault Permissions / Other Dependencies. - Add update-release-channel to the actions list and full reference section in the root README, linking to update-release-channel/README.md for operational recipes and limitations. --- README.md | 69 ++++++++++++++ update-release-channel/README.md | 153 ++++++++++++++----------------- 2 files changed, 138 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 5f876dbb..ffd74285 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ These badges show the status of workflows in dummy repositories that use (or sho - [`pr_cleanup`](#pr_cleanup) - [`code-signing`](#code-signing) - [`check-sca`](#check-sca) +- [`update-release-channel`](#update-release-channel) --- @@ -1447,6 +1448,74 @@ jobs: --- +## `update-release-channel` + +Maintain per-channel JSON pointer files on `binaries.sonarsource.com` so consumers can discover the currently published +version for each release channel of a product. Writes one JSON file per channel at +`//.json` (atomic per channel, single S3 `PutObject`). + +See [`update-release-channel/README.md`](update-release-channel/README.md) for the full reference: operational +recipes (rollback, delayed promotion, backfill, channel migration), the recommended GitHub Environment with reviewer +gate, and the publish-vs-promote atomicity caveat. + +### Requirements + +#### Required GitHub Permissions + +- `id-token: write` +- `contents: read` + +#### Required Vault Permissions + +- `development/aws/sts/downloads`: STS credentials to write to `s3://downloads-cdn-eu-central-1-prod`. This is the same + preset already provisioned for `SonarSource/gh-action_release`. + +#### Other Dependencies + +The action installs the AWS CLI on demand via `mise` — no other tooling needs to be pre-installed on the runner. + +### Usage + +```yaml +jobs: + release: + uses: SonarSource/gh-action_release/.github/workflows/main.yaml@7.0.1 + with: { ... } + + update-channel: + needs: release + if: ${{ !inputs.dryRun }} + runs-on: sonar-xs + permissions: + id-token: write + contents: read + steps: + - uses: SonarSource/ci-github-actions/update-release-channel@v1 + with: + version: ${{ inputs.version }} +``` + +### Inputs + +| Input | Description | Default | +|-----------|------------------------------------------------------------------------------------------------------------|---------------------------------------| +| `version` | Version the channel should point at (e.g. `0.9.0.977`). Required. | — | +| `channel` | Release channel name. One of `latest`, `stable`, `beta`, `rc`, `dogfood`. | `latest` | +| `prefix` | S3 key prefix under the bucket. Other values warn but are accepted. | `Distribution` | +| `product` | Product folder name on S3. Set explicitly when the S3 folder differs from the GitHub repo name. | `${{ github.event.repository.name }}` | +| `dryRun` | Resolve and validate inputs, print the planned `PutObject`, skip Vault + AWS calls. | `false` | + +### Outputs + +| Output | Description | +|----------|--------------------------------------------------------------------------------------------| +| `bucket` | S3 bucket the channel pointer was (or would be) written to. | +| `key` | S3 key of the channel pointer (e.g. `Distribution//.json`). | +| `url` | Public URL of the channel pointer. | +| `body` | JSON body written (or that would be written) to S3. Useful for schema validation in tests. | + +--- + ## Deployment Strategy All build actions (`build-maven`, `build-gradle`, `build-npm`, `build-yarn`, `build-poetry`) share the same branch-based deployment and diff --git a/update-release-channel/README.md b/update-release-channel/README.md index cbe17a56..c073ec92 100644 --- a/update-release-channel/README.md +++ b/update-release-channel/README.md @@ -1,53 +1,39 @@ -# Update release channel +# `update-release-channel` -GitHub Action that maintains per-channel JSON pointer files on `binaries.sonarsource.com` so consumers can discover the -currently published version for each release channel of a product. +Maintain per-channel JSON pointer files on `binaries.sonarsource.com` so consumers can discover the currently published +version for each release channel of a product. -For example, a release pipeline can call this action after publishing `Distribution/sonarqube-cli/0.9.0.977/` to -update `Distribution/sonarqube-cli/latest.json` so install scripts, homebrew formulas, and the CLI's own auto-update -check can resolve the current version with a single HTTP GET. +The action writes one JSON file per channel (atomic per channel, single S3 `PutObject`) at +`//.json`. The body follows the [v1 schema](./schema/v1.json); see +[schema/README.md](./schema/README.md) for the field contract. -The action writes one JSON file per channel (atomic per channel, single S3 `PutObject`). Lifecycle operations like -rollback, delayed promotion, and backfill are first-class — they're a single `workflow_dispatch` invocation away, -without re-running the release pipeline. +Lifecycle operations like rollback, delayed promotion, and backfill are first-class — they're a single +`workflow_dispatch` invocation away, without re-running the release pipeline. -The JSON body follows the [v1 schema](./schema/v1.json); see [schema/README.md](./schema/README.md) for the field -contract. +## Requirements -## Inputs +### Required GitHub Permissions -| Input | Required | Default | Description | -| --------- | -------- | ---------------------------------------- | -------------------------------------------------------------------------- | -| `version` | yes | — | Version the channel should point at (e.g. `0.9.0.977`). | -| `channel` | no | `latest` | Release channel name. One of `latest`, `stable`, `beta`, `rc`, `dogfood`. | -| `prefix` | no | `Distribution` | S3 key prefix under the bucket. Other values warn but are accepted. | -| `product` | no | `${{ github.event.repository.name }}` | Product folder name on S3. | -| `dryRun` | no | `false` | Resolve and validate inputs, print the planned `PutObject`, skip the call. | +- `id-token: write` +- `contents: read` -### When to set `product:` explicitly +### Required Vault Permissions -The default (`${{ github.event.repository.name }}`) works whenever the GitHub repository name matches the product -folder on S3. Set `product:` explicitly when they differ — for example, `sonar-dummy-gradle-oss` publishes to -`Distribution/sonar-dummy-gradle-oss-plugin/` and must pass `product: sonar-dummy-gradle-oss-plugin`. +- `development/aws/sts/downloads`: STS credentials to write to `s3://downloads-cdn-eu-central-1-prod`. This is the same + preset already provisioned for `SonarSource/gh-action_release`. -## Outputs +### Other Dependencies -| Output | Description | -| -------- | ------------------------------------------------------------------------------------------- | -| `bucket` | S3 bucket the channel pointer was (or would be) written to. | -| `key` | S3 key of the channel pointer (e.g. `Distribution//.json`). | -| `url` | Public URL of the channel pointer. | -| `body` | JSON body written (or that would be written) to S3. Useful for schema validation in tests. | +The action installs the AWS CLI on demand via `mise` — no other tooling needs to be pre-installed on the runner. ## Usage -### Release-workflow follow-up job (automated) +### As a release-workflow follow-up job (automated) -The standard pattern: append an `update-channel` job to the release workflow so the `latest` channel is promoted -automatically after a successful publish. No environment gate — the gate is the release itself. +Append an `update-channel` job to the release workflow so the `latest` channel is promoted automatically after a +successful publish. ```yaml -# .github/workflows/release.yml jobs: release: uses: SonarSource/gh-action_release/.github/workflows/main.yaml@7.0.1 @@ -58,7 +44,7 @@ jobs: if: ${{ !inputs.dryRun }} runs-on: sonar-xs permissions: - id-token: write # OIDC → Vault + id-token: write contents: read steps: - uses: SonarSource/ci-github-actions/update-release-channel@v1 @@ -67,23 +53,23 @@ jobs: # channel defaults to "latest"; prefix to "Distribution"; product to the repo name. ``` -### Standalone `workflow_dispatch` (manual ops) +### As a standalone `workflow_dispatch` (manual ops) -A separate workflow for rollback, delayed promotion, backfill, and channel migration. Gated by a GitHub Environment -with required reviewers so every manual write is an approved action. +Add a separate workflow for rollback, delayed promotion, backfill, and channel migration. Reference the +`release-channel-admin` GitHub Environment so every manual write is reviewed (see +[Required GitHub Environment](#required-github-environment)). ```yaml -# .github/workflows/update-release-channel.yml on: workflow_dispatch: inputs: version: { required: true, type: string } - channel: { required: true, type: choice, options: [stable, beta, rc] } + channel: { required: true, type: choice, options: [stable, beta, rc, dogfood] } jobs: update-channel: runs-on: sonar-xs - environment: release-channel-admin # required reviewers — see below + environment: release-channel-admin permissions: id-token: write contents: read @@ -94,96 +80,95 @@ jobs: channel: ${{ inputs.channel }} ``` -The `latest` channel is intentionally omitted from the manual choices — promote `latest` only via the automated -release follow-up job. +## Inputs + +| Input | Description | Default | +|-----------|------------------------------------------------------------------------------------------------------------|---------------------------------------| +| `version` | Version the channel should point at (e.g. `0.9.0.977`). Required. | — | +| `channel` | Release channel name. One of `latest`, `stable`, `beta`, `rc`, `dogfood`. | `latest` | +| `prefix` | S3 key prefix under the bucket. Other values warn but are accepted. | `Distribution` | +| `product` | Product folder name on S3. Set explicitly when the S3 folder differs from the GitHub repo name. | `${{ github.event.repository.name }}` | +| `dryRun` | Resolve and validate inputs, print the planned `PutObject`, skip Vault + AWS calls. | `false` | + +## Outputs + +| Output | Description | +|----------|--------------------------------------------------------------------------------------------| +| `bucket` | S3 bucket the channel pointer was (or would be) written to. | +| `key` | S3 key of the channel pointer (e.g. `Distribution//.json`). | +| `url` | Public URL of the channel pointer. | +| `body` | JSON body written (or that would be written) to S3. Useful for schema validation in tests. | ## Required GitHub Environment The manual-ops workflow MUST be gated by a GitHub Environment configured with **required reviewers**. Without that gate, anyone with write access to the repo could re-point a production channel. -### How to create the environment +To create the environment: 1. In the repo, go to **Settings → Environments → New environment**. 2. Name it `release-channel-admin`. -3. Under **Deployment protection rules**, enable **Required reviewers** and add the reviewer allowlist (see below). +3. Under **Deployment protection rules**, enable **Required reviewers** and add the reviewer allowlist. 4. Save. -Optional: restrict the environment to specific branches under **Deployment branches** if your manual-ops workflow -should only be dispatchable from `master` / `main`. - -### Recommended reviewer allowlist +Recommended reviewer allowlist: - Release captains / squad leads of the consuming product - Plus a backup from the platform / release engineering team for breakglass -### Rationale - -The action's STS credentials are broadly scoped (the Vault preset `development/aws/sts/downloads` can write anywhere -under `downloads-cdn-eu-central-1-prod`). The in-action destination-prefix guardrail mitigates path traversal, but a +The action's STS credentials are broadly scoped (the Vault preset can write anywhere under +`downloads-cdn-eu-central-1-prod`). The in-action destination-prefix guardrail mitigates path traversal, but a mistaken `version` or `channel` in a manual dispatch can still corrupt a production pointer. Requiring an approver -ensures a second pair of eyes on every non-automated write, and ensures no Vault token is even fetched until the -dispatch is approved in the GitHub UI. +ensures a second pair of eyes on every non-automated write, and ensures no Vault token is fetched until the dispatch +is approved. ## Operational recipes -All manual operations use the standalone `workflow_dispatch` workflow above. Dispatch from **Actions → Update release -channel → Run workflow**, fill the inputs, and wait for the environment approver to click **Approve**. +All manual operations use the standalone `workflow_dispatch` workflow shown above. Dispatch from **Actions → Update +release channel → Run workflow**, fill the inputs, wait for the environment approver to click **Approve**. ### Rollback -To re-point a channel at the previously-known-good version: - -1. Identify the prior version (from release notes, `git tag`, or the previous `.json` body cached locally). +1. Identify the prior version (release notes, `git tag`, or the previous `.json` body cached locally). 2. Dispatch with `version: ` and `channel: `. -Because writes are atomic per channel (single `PutObject`), rollback affects only the targeted channel; other -channels are untouched. The `max-age=60` cache header ensures consumers pick up the rollback within a minute. +Writes are atomic per channel; rollback affects only the targeted channel. `Cache-Control: max-age=60` means +consumers pick up the rollback within a minute. ### Delayed promotion -To promote `stable` (or any non-`latest` channel) some time after the release published `latest`: - -- Dispatch with `version: ` and `channel: stable` whenever the team is ready. - -This is just the manual-ops workflow's normal use — no special handling required. +Dispatch with `version: ` and `channel: stable` (or any non-`latest` channel) whenever the team is +ready. No special handling — this is the manual-ops workflow's normal use. ### Backfill -When onboarding the action against an existing product that has prior releases, backfill the channel pointer for the -currently-published version with one dispatch (`version: `, `channel: `). Repeat per channel. +When onboarding the action against an existing product, backfill the channel pointer for the currently-published +version with one dispatch (`version: `, `channel: `). Repeat per channel. ### Channel migration To deprecate a channel name (e.g. retire `rc` in favour of `beta`), backfill `beta.json` to match `rc.json`'s current -value via one dispatch. The action does not delete channel files; if you need a channel JSON removed, do it -out-of-band via the AWS console / CLI. +value via one dispatch. The action does not delete channel files; remove channel JSON out-of-band via the AWS console +or CLI. ## Limitations ### Publish and channel update are not transactional The release pipeline (publish to `Distribution///`) and the `update-release-channel` job run as -separate steps. If publish succeeds but the channel update fails (for example, transient AWS error, expired Vault -token), the artifacts are live at the versioned URL but `.json` still points at the previous version. - -Consumers reading `.json` keep seeing the old version until the channel update is retried — they don't see -a half-promoted state, just the previous one. +separate steps. If publish succeeds but the channel update fails (transient AWS error, expired Vault token), the +artifacts are live at the versioned URL but `.json` still points at the previous version. Consumers see the +previous version until the channel update is retried — no half-promoted state. ### Recovery -Re-run the channel update via the manual-ops workflow: - -1. Dispatch with the same `version` and `channel` that the failed automated job tried. -2. Approve the environment gate. - -Because the JSON body is regenerated on each invocation, repeating the operation is idempotent. The artifacts at -`Distribution///` are already published; only the pointer needs to catch up. +Re-run the channel update via the manual-ops workflow with the same `version` and `channel`. The JSON body is +regenerated on each invocation, so repeating the operation is idempotent. ## Implementation details - **Bucket:** `downloads-cdn-eu-central-1-prod` (served at `https://binaries.sonarsource.com/`) -- **Vault preset:** `development/aws/sts/downloads` (the same preset used by `gh-action_release`) +- **Vault preset:** `development/aws/sts/downloads` (shared with `gh-action_release`) - **Cache-Control:** `max-age=60` on every write — short enough that rollbacks propagate quickly, long enough to keep the CDN happy. - **Destination guardrail:** the script refuses to write outside `//.json` and validates From 3258541459c972c3de3c874e122e999ae480425a Mon Sep 17 00:00:00 2001 From: Jayadeep Kinavoor Madam Date: Thu, 28 May 2026 18:14:06 +0200 Subject: [PATCH 4/4] BUILD-11462: Trim action README; add workflow_dispatch example to root --- README.md | 53 ++++-- update-release-channel/README.md | 176 ------------------ .../update-release-channel.sh | 2 +- 3 files changed, 42 insertions(+), 189 deletions(-) delete mode 100644 update-release-channel/README.md diff --git a/README.md b/README.md index ffd74285..f05fa1c1 100644 --- a/README.md +++ b/README.md @@ -1450,13 +1450,11 @@ jobs: ## `update-release-channel` -Maintain per-channel JSON pointer files on `binaries.sonarsource.com` so consumers can discover the currently published +Updates a per-channel JSON pointer file on `binaries.sonarsource.com` so consumers can discover the currently published version for each release channel of a product. Writes one JSON file per channel at -`//.json` (atomic per channel, single S3 `PutObject`). - -See [`update-release-channel/README.md`](update-release-channel/README.md) for the full reference: operational -recipes (rollback, delayed promotion, backfill, channel migration), the recommended GitHub Environment with reviewer -gate, and the publish-vs-promote atomicity caveat. +`//.json` (atomic, single S3 `PutObject`). The body follows the +[v1 schema](update-release-channel/schema/v1.json); see [schema/README.md](update-release-channel/schema/README.md) for the +field contract. ### Requirements @@ -1476,6 +1474,8 @@ The action installs the AWS CLI on demand via `mise` — no other tooling needs ### Usage +As a release-workflow follow-up job (automated `latest` promotion): + ```yaml jobs: release: @@ -1495,6 +1495,35 @@ jobs: version: ${{ inputs.version }} ``` +As a standalone `workflow_dispatch` (manual ops — rollback, delayed promotion, backfill): + +```yaml +on: + workflow_dispatch: + inputs: + version: { required: true, type: string } + channel: { required: true, type: choice, options: [latest, stable, beta, rc, dogfood] } + +jobs: + update-channel: + runs-on: sonar-xs + environment: release-channel-admin # recommended; see note below + permissions: + id-token: write + contents: read + steps: + - uses: SonarSource/ci-github-actions/update-release-channel@v1 + with: + version: ${{ inputs.version }} + channel: ${{ inputs.channel }} +``` + +Referencing a `release-channel-admin` GitHub Environment (configured with required reviewers) is recommended for +manual-ops workflows so every manual write requires an approver. The action runs without it — the gate is opt-in, +set up per consuming repo. Environments at SonarSource are managed in +[`re-service-config`](https://github.com/SonarSource/re-service-config) via the `github_repository_environment` +Terraform resource; add the environment for your repo there alongside the existing examples. + ### Inputs | Input | Description | Default | @@ -1507,12 +1536,12 @@ jobs: ### Outputs -| Output | Description | -|----------|--------------------------------------------------------------------------------------------| -| `bucket` | S3 bucket the channel pointer was (or would be) written to. | -| `key` | S3 key of the channel pointer (e.g. `Distribution//.json`). | -| `url` | Public URL of the channel pointer. | -| `body` | JSON body written (or that would be written) to S3. Useful for schema validation in tests. | +| Output | Description | +|----------|---------------------------------------------------------------------------------| +| `bucket` | S3 bucket of the JSON pointer file. | +| `key` | S3 key of the JSON pointer file (e.g. `Distribution//.json`). | +| `url` | Public URL of the JSON pointer file. | +| `body` | Content of the JSON pointer file. | --- diff --git a/update-release-channel/README.md b/update-release-channel/README.md deleted file mode 100644 index c073ec92..00000000 --- a/update-release-channel/README.md +++ /dev/null @@ -1,176 +0,0 @@ -# `update-release-channel` - -Maintain per-channel JSON pointer files on `binaries.sonarsource.com` so consumers can discover the currently published -version for each release channel of a product. - -The action writes one JSON file per channel (atomic per channel, single S3 `PutObject`) at -`//.json`. The body follows the [v1 schema](./schema/v1.json); see -[schema/README.md](./schema/README.md) for the field contract. - -Lifecycle operations like rollback, delayed promotion, and backfill are first-class — they're a single -`workflow_dispatch` invocation away, without re-running the release pipeline. - -## Requirements - -### Required GitHub Permissions - -- `id-token: write` -- `contents: read` - -### Required Vault Permissions - -- `development/aws/sts/downloads`: STS credentials to write to `s3://downloads-cdn-eu-central-1-prod`. This is the same - preset already provisioned for `SonarSource/gh-action_release`. - -### Other Dependencies - -The action installs the AWS CLI on demand via `mise` — no other tooling needs to be pre-installed on the runner. - -## Usage - -### As a release-workflow follow-up job (automated) - -Append an `update-channel` job to the release workflow so the `latest` channel is promoted automatically after a -successful publish. - -```yaml -jobs: - release: - uses: SonarSource/gh-action_release/.github/workflows/main.yaml@7.0.1 - with: { ... } - - update-channel: - needs: release - if: ${{ !inputs.dryRun }} - runs-on: sonar-xs - permissions: - id-token: write - contents: read - steps: - - uses: SonarSource/ci-github-actions/update-release-channel@v1 - with: - version: ${{ inputs.version }} - # channel defaults to "latest"; prefix to "Distribution"; product to the repo name. -``` - -### As a standalone `workflow_dispatch` (manual ops) - -Add a separate workflow for rollback, delayed promotion, backfill, and channel migration. Reference the -`release-channel-admin` GitHub Environment so every manual write is reviewed (see -[Required GitHub Environment](#required-github-environment)). - -```yaml -on: - workflow_dispatch: - inputs: - version: { required: true, type: string } - channel: { required: true, type: choice, options: [stable, beta, rc, dogfood] } - -jobs: - update-channel: - runs-on: sonar-xs - environment: release-channel-admin - permissions: - id-token: write - contents: read - steps: - - uses: SonarSource/ci-github-actions/update-release-channel@v1 - with: - version: ${{ inputs.version }} - channel: ${{ inputs.channel }} -``` - -## Inputs - -| Input | Description | Default | -|-----------|------------------------------------------------------------------------------------------------------------|---------------------------------------| -| `version` | Version the channel should point at (e.g. `0.9.0.977`). Required. | — | -| `channel` | Release channel name. One of `latest`, `stable`, `beta`, `rc`, `dogfood`. | `latest` | -| `prefix` | S3 key prefix under the bucket. Other values warn but are accepted. | `Distribution` | -| `product` | Product folder name on S3. Set explicitly when the S3 folder differs from the GitHub repo name. | `${{ github.event.repository.name }}` | -| `dryRun` | Resolve and validate inputs, print the planned `PutObject`, skip Vault + AWS calls. | `false` | - -## Outputs - -| Output | Description | -|----------|--------------------------------------------------------------------------------------------| -| `bucket` | S3 bucket the channel pointer was (or would be) written to. | -| `key` | S3 key of the channel pointer (e.g. `Distribution//.json`). | -| `url` | Public URL of the channel pointer. | -| `body` | JSON body written (or that would be written) to S3. Useful for schema validation in tests. | - -## Required GitHub Environment - -The manual-ops workflow MUST be gated by a GitHub Environment configured with **required reviewers**. Without that -gate, anyone with write access to the repo could re-point a production channel. - -To create the environment: - -1. In the repo, go to **Settings → Environments → New environment**. -2. Name it `release-channel-admin`. -3. Under **Deployment protection rules**, enable **Required reviewers** and add the reviewer allowlist. -4. Save. - -Recommended reviewer allowlist: - -- Release captains / squad leads of the consuming product -- Plus a backup from the platform / release engineering team for breakglass - -The action's STS credentials are broadly scoped (the Vault preset can write anywhere under -`downloads-cdn-eu-central-1-prod`). The in-action destination-prefix guardrail mitigates path traversal, but a -mistaken `version` or `channel` in a manual dispatch can still corrupt a production pointer. Requiring an approver -ensures a second pair of eyes on every non-automated write, and ensures no Vault token is fetched until the dispatch -is approved. - -## Operational recipes - -All manual operations use the standalone `workflow_dispatch` workflow shown above. Dispatch from **Actions → Update -release channel → Run workflow**, fill the inputs, wait for the environment approver to click **Approve**. - -### Rollback - -1. Identify the prior version (release notes, `git tag`, or the previous `.json` body cached locally). -2. Dispatch with `version: ` and `channel: `. - -Writes are atomic per channel; rollback affects only the targeted channel. `Cache-Control: max-age=60` means -consumers pick up the rollback within a minute. - -### Delayed promotion - -Dispatch with `version: ` and `channel: stable` (or any non-`latest` channel) whenever the team is -ready. No special handling — this is the manual-ops workflow's normal use. - -### Backfill - -When onboarding the action against an existing product, backfill the channel pointer for the currently-published -version with one dispatch (`version: `, `channel: `). Repeat per channel. - -### Channel migration - -To deprecate a channel name (e.g. retire `rc` in favour of `beta`), backfill `beta.json` to match `rc.json`'s current -value via one dispatch. The action does not delete channel files; remove channel JSON out-of-band via the AWS console -or CLI. - -## Limitations - -### Publish and channel update are not transactional - -The release pipeline (publish to `Distribution///`) and the `update-release-channel` job run as -separate steps. If publish succeeds but the channel update fails (transient AWS error, expired Vault token), the -artifacts are live at the versioned URL but `.json` still points at the previous version. Consumers see the -previous version until the channel update is retried — no half-promoted state. - -### Recovery - -Re-run the channel update via the manual-ops workflow with the same `version` and `channel`. The JSON body is -regenerated on each invocation, so repeating the operation is idempotent. - -## Implementation details - -- **Bucket:** `downloads-cdn-eu-central-1-prod` (served at `https://binaries.sonarsource.com/`) -- **Vault preset:** `development/aws/sts/downloads` (shared with `gh-action_release`) -- **Cache-Control:** `max-age=60` on every write — short enough that rollbacks propagate quickly, long enough to keep - the CDN happy. -- **Destination guardrail:** the script refuses to write outside `//.json` and validates - `` against `^[a-z0-9][a-z0-9._-]*$`. A custom `prefix` other than `Distribution` is accepted with a loud - warning. diff --git a/update-release-channel/update-release-channel.sh b/update-release-channel/update-release-channel.sh index 2f0ea2d3..5c555c65 100755 --- a/update-release-channel/update-release-channel.sh +++ b/update-release-channel/update-release-channel.sh @@ -5,7 +5,7 @@ # # Required environment variables: # VERSION - Version the channel should point at (e.g. "0.9.0.977") -# CHANNEL - Channel name (latest|stable|beta|rc) +# CHANNEL - Channel name (latest|stable|beta|rc|dogfood) # PREFIX - S3 key prefix (default in action.yml: "Distribution") # PRODUCT - Product folder on S3 (default in action.yml: GitHub repo name) # DRY_RUN - "true" to skip the AWS call and just print the planned PutObject