diff --git a/.github/workflows/go-release.yml b/.github/workflows/go-release.yml index e8200ad7..55d9b198 100644 --- a/.github/workflows/go-release.yml +++ b/.github/workflows/go-release.yml @@ -49,6 +49,18 @@ on: description: 'Merge main into a prerelease branch (develop/release-candidate) before calculating its next version. Defaults to false (opt-in) — set to true to enable this pre-version-calculation sync, which can skip/block a release on those branches when the merge cannot complete directly. The post-release backmerge on main after a stable release is unaffected by this input either way.' type: boolean default: false + enable_release_announcement: + description: 'Announce the published release to the repository Slack channel after a successful release' + type: boolean + default: true + announcement_product_name: + description: 'Product name displayed in the announcement. Defaults to the repository name.' + type: string + default: '' + announcement_slack_channel: + description: 'Slack channel that receives the announcement. Defaults to the RELEASE_SLACK_CHANNEL repository variable; the announcement is skipped when both are empty.' + type: string + default: '' # ----------------- Build (build.yml) ----------------- enable_dockerhub: @@ -383,6 +395,9 @@ jobs: stable_releases_only: ${{ inputs.stable_releases_only }} changelog_bot_ignore_list: ${{ inputs.changelog_bot_ignore_list }} prerelease_backmerge_sync_enabled: ${{ inputs.prerelease_backmerge_sync_enabled }} + enable_release_announcement: ${{ inputs.enable_release_announcement }} + announcement_product_name: ${{ inputs.announcement_product_name }} + announcement_slack_channel: ${{ inputs.announcement_slack_channel }} shared_paths: ${{ inputs.shared_paths }} filter_paths: ${{ !inputs.release_single_app && inputs.filter_paths || '' }} secrets: inherit diff --git a/.github/workflows/js-release.yml b/.github/workflows/js-release.yml index 7c7d42f2..75260953 100644 --- a/.github/workflows/js-release.yml +++ b/.github/workflows/js-release.yml @@ -73,6 +73,18 @@ on: description: 'Directory depth level to extract app name (e.g., 2 -> "apps/agent")' type: string default: '2' + enable_release_announcement: + description: 'Announce the published release to the repository Slack channel after a successful release' + type: boolean + default: true + announcement_product_name: + description: 'Product name displayed in the announcement. Defaults to the repository name.' + type: string + default: '' + announcement_slack_channel: + description: 'Slack channel that receives the announcement. Defaults to the RELEASE_SLACK_CHANNEL repository variable; the announcement is skipped when both are empty.' + type: string + default: '' # ----------------- Build (typescript-build.yml) ----------------- enable_dockerhub: @@ -298,6 +310,9 @@ jobs: backmerge_mode: ${{ inputs.backmerge_mode }} shared_paths: ${{ inputs.shared_paths }} path_level: ${{ inputs.path_level }} + enable_release_announcement: ${{ inputs.enable_release_announcement }} + announcement_product_name: ${{ inputs.announcement_product_name }} + announcement_slack_channel: ${{ inputs.announcement_slack_channel }} filter_paths: ${{ !inputs.release_single_app && inputs.filter_paths || '' }} secrets: inherit diff --git a/.github/workflows/release-notification.yml b/.github/workflows/release-notification.yml index c554dac4..39daa9cf 100644 --- a/.github/workflows/release-notification.yml +++ b/.github/workflows/release-notification.yml @@ -12,6 +12,11 @@ on: required: false type: string default: "" + release_tag: + description: Release tag to announce. When empty, resolves from the release event or the latest release. + required: false + type: string + default: "" discord_color: description: Discord embed color (decimal) required: false @@ -130,8 +135,16 @@ jobs: id: release env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + INPUT_TAG: ${{ inputs.release_tag }} + EVENT_TAG: ${{ github.event.release.tag_name }} run: | - TAG='${{ github.event.release.tag_name }}' + TAG="$INPUT_TAG" + if [[ -n "$TAG" ]]; then + echo "Using release tag provided by the caller: $TAG" + fi + if [[ -z "$TAG" ]]; then + TAG="$EVENT_TAG" + fi if [[ -z "$TAG" ]]; then echo "No release event tag — falling back to gh release list" TAG=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 1 --json tagName --jq '.[0].tagName') @@ -147,28 +160,51 @@ jobs: if: ${{ inputs.dry_run }} env: RESOLVED_RUNNER: ${{ vars.GENERAL_RUNNERS || 'blacksmith-4vcpu-ubuntu-2404' }} + PRODUCT_NAME: ${{ inputs.product_name }} + RELEASE_TAG: ${{ steps.release.outputs.tag }} + DISCORD_COLOR: ${{ inputs.discord_color }} + DISCORD_USERNAME: ${{ inputs.discord_username }} + DISCORD_CONTENT: ${{ inputs.discord_content }} + SKIP_BETA_DISCORD: ${{ inputs.skip_beta_discord }} + SLACK_CHANNEL: ${{ inputs.slack_channel }} + SLACK_COLOR: ${{ inputs.slack_color }} + SLACK_ICON_EMOJI: ${{ inputs.slack_icon_emoji }} run: | - ENABLE_DISCORD="${{ env.DISCORD_WEBHOOK_URL != '' && 'true' || 'false' }}" - ENABLE_SLACK="${{ env.SLACK_WEBHOOK_URL != '' && inputs.slack_channel != '' && 'true' || 'false' }}" + DISCORD_STATE="not set" + ENABLE_DISCORD=false + if [[ -n "$DISCORD_WEBHOOK_URL" ]]; then + DISCORD_STATE="configured" + ENABLE_DISCORD=true + fi + + SLACK_STATE="not set" + ENABLE_SLACK=false + if [[ -n "$SLACK_WEBHOOK_URL" ]]; then + SLACK_STATE="configured" + if [[ -n "$SLACK_CHANNEL" ]]; then + ENABLE_SLACK=true + fi + fi + echo "::notice::DRY RUN — no notifications will be sent" echo " runner : $RESOLVED_RUNNER" - echo " product_name : ${{ inputs.product_name }}" - echo " release_tag : ${{ steps.release.outputs.tag }}" - echo " discord_webhook : ${{ env.DISCORD_WEBHOOK_URL != '' && 'configured' || 'not set' }}" - echo " discord_color : ${{ inputs.discord_color }}" - echo " discord_username : ${{ inputs.discord_username }}" - echo " discord_content : ${{ inputs.discord_content }}" - echo " skip_beta_discord: ${{ inputs.skip_beta_discord }}" - echo " enable_discord : ${ENABLE_DISCORD}" - echo " slack_webhook : ${{ env.SLACK_WEBHOOK_URL != '' && 'configured' || 'not set' }}" - echo " slack_channel : ${{ inputs.slack_channel }}" - echo " slack_color : ${{ inputs.slack_color }}" - echo " slack_icon_emoji : ${{ inputs.slack_icon_emoji }}" - echo " enable_slack : ${ENABLE_SLACK}" + echo " product_name : $PRODUCT_NAME" + echo " release_tag : $RELEASE_TAG" + echo " discord_webhook : $DISCORD_STATE" + echo " discord_color : $DISCORD_COLOR" + echo " discord_username : $DISCORD_USERNAME" + echo " discord_content : $DISCORD_CONTENT" + echo " skip_beta_discord: $SKIP_BETA_DISCORD" + echo " enable_discord : $ENABLE_DISCORD" + echo " slack_webhook : $SLACK_STATE" + echo " slack_channel : $SLACK_CHANNEL" + echo " slack_color : $SLACK_COLOR" + echo " slack_icon_emoji : $SLACK_ICON_EMOJI" + echo " enable_slack : $ENABLE_SLACK" - name: Discord notification if: ${{ env.DISCORD_WEBHOOK_URL != '' }} - uses: LerianStudio/github-actions-shared-workflows/src/notify/discord-release@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/notify/discord-release@v1 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} release-tag: ${{ steps.release.outputs.tag }} @@ -180,7 +216,7 @@ jobs: - name: Slack notification if: ${{ env.SLACK_WEBHOOK_URL != '' && inputs.slack_channel != '' }} - uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-release@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-release@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} channel: ${{ inputs.slack_channel }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8e32254..5913a870 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,6 +68,23 @@ on: type: string default: '' + # ----------------- Release Announcement ----------------- + enable_release_announcement: + description: 'Announce the published release to the repository Slack channel after a successful release' + required: false + type: boolean + default: true + announcement_product_name: + description: 'Product name displayed in the announcement. Defaults to the repository name.' + required: false + type: string + default: '' + announcement_slack_channel: + description: 'Slack channel that receives the announcement. Defaults to the RELEASE_SLACK_CHANNEL repository variable; the announcement is skipped when both are empty.' + required: false + type: string + default: '' + # ----------------- Backmerge ----------------- backmerge_enabled: description: 'Backmerge the release branch into the target branch after a successful release' @@ -606,6 +623,34 @@ jobs: echo "ℹ️ No stable vX.Y.Z release found — leaving Latest untouched" fi + # ----------------- Release Announcement ----------------- + # Announces the published release to the channel owned by the calling + # repository. Routing is per-repo: the channel comes from + # announcement_slack_channel or the RELEASE_SLACK_CHANNEL variable, and the + # webhook from the RELEASE_WEBHOOK_URL (or legacy + # RELEASE_WEBHOOK_NOTIFICATION_URL) repository secret. Discord is not wired + # here because SethCohen/github-releases-to-discord reads the `release` event + # payload, which is absent on the push event that drives this workflow — + # Discord announcements stay on release-notification.yml with on: release. + announce_release: + name: Announce Release + needs: [publish_release, publish_release_status, generate_changelog, backmerge, update_major_tag, enforce_latest] + if: >- + always() && + inputs.enable_release_announcement && + needs.publish_release_status.outputs.release_published == 'true' && + (inputs.announcement_slack_channel != '' || vars.RELEASE_SLACK_CHANNEL != '') + uses: ./.github/workflows/release-notification.yml + with: + product_name: ${{ inputs.announcement_product_name != '' && inputs.announcement_product_name || github.event.repository.name }} + slack_channel: ${{ inputs.announcement_slack_channel != '' && inputs.announcement_slack_channel || vars.RELEASE_SLACK_CHANNEL }} + release_tag: ${{ needs.publish_release_status.outputs.release_git_tag }} + dry_run: ${{ inputs.dry_run }} + secrets: + APP_ID: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }} + APP_PRIVATE_KEY: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_PRIVATE_KEY }} + SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_WEBHOOK_URL || secrets.RELEASE_WEBHOOK_NOTIFICATION_URL }} + # Slack notification notify: name: Notify diff --git a/docs/go-release-workflow.md b/docs/go-release-workflow.md index 08eaf83a..31a75bdb 100644 --- a/docs/go-release-workflow.md +++ b/docs/go-release-workflow.md @@ -32,6 +32,9 @@ A third layout needs `release_single_app: true`: **one semantic-release tag for | `enable_changelog` | Generate CHANGELOG.md via GPT after a successful release | boolean | `false` | | `enable_major_tag` | Force-update the floating major tag (e.g. `v1`) | boolean | `false` | | `stable_releases_only` | Only generate changelogs for stable releases | boolean | `true` | +| `enable_release_announcement` | Announce the published release to the repository Slack channel (see [release-workflow](release-workflow.md#release-announcement)) | boolean | `true` | +| `announcement_product_name` | Product name displayed in the announcement. Empty → repository name | string | `''` | +| `announcement_slack_channel` | Slack channel for the announcement. Empty → `RELEASE_SLACK_CHANNEL` repository variable; skipped when both are empty | string | `''` | | `enable_dockerhub` | Push image to DockerHub | boolean | `true` | | `enable_ghcr` | Push image to GitHub Container Registry (requires `MANAGE_TOKEN`) | boolean | `true` | | `enable_gitops_artifacts` | Upload GitOps artifacts for the downstream update | boolean | `false` | diff --git a/docs/js-release.md b/docs/js-release.md index f7f65c8d..2f565437 100644 --- a/docs/js-release.md +++ b/docs/js-release.md @@ -34,6 +34,9 @@ Mirrors the [`go-release`](./go-release-workflow.md) umbrella for Go services | `filter_paths` | Path prefixes to filter (empty = single-app repo) | string | `''` | | `shared_paths` | Path patterns that trigger a release/build for all components | string | `''` | | `path_level` | Directory depth level to extract app name | string | `2` | +| `enable_release_announcement` | Announce the published release to the repository Slack channel (see [release-workflow](release-workflow.md#release-announcement)) | boolean | `true` | +| `announcement_product_name` | Product name displayed in the announcement. Empty → repository name | string | `''` | +| `announcement_slack_channel` | Slack channel for the announcement. Empty → `RELEASE_SLACK_CHANNEL` repository variable; skipped when both are empty | string | `''` | | `enable_dockerhub` | Push image to DockerHub | boolean | `false` | | `enable_ghcr` | Push image to GitHub Container Registry | boolean | `true` | | `enable_gitops_artifacts` | Upload GitOps artifacts for the downstream update | boolean | `false` | diff --git a/docs/release-notification.md b/docs/release-notification.md index 535108b3..2c054039 100644 --- a/docs/release-notification.md +++ b/docs/release-notification.md @@ -5,7 +5,12 @@ -Reusable workflow that sends release notifications to Discord and Slack. Fetches the latest release tag via GitHub CLI and dispatches to channel-specific composite actions. +Reusable workflow that sends release notifications to Discord and Slack. Resolves the release tag from `release_tag`, then the release event, then the latest release via GitHub CLI, and dispatches to channel-specific composite actions. + +The [release workflow](release-workflow.md) calls this workflow directly from its +`announce_release` job (Slack only), so most repositories do not need a standalone caller — +see [Release Announcement](release-workflow.md#release-announcement). A dedicated caller with +`on: release` is still required for Discord. ## Architecture @@ -21,6 +26,7 @@ release-notification.yml |---|---|:---:|---|---| | `product_name` | `string` | Yes | — | Product name displayed in notifications | | `slack_channel` | `string` | No | `""` | Slack channel name | +| `release_tag` | `string` | No | `""` | Release tag to announce. When empty, resolves from the release event and then from the latest release | | `discord_color` | `string` | No | `2105893` | Discord embed color (decimal) | | `discord_username` | `string` | No | `Release Changelog` | Bot username in Discord | | `discord_content` | `string` | No | `""` | Discord message content (e.g. role mentions) | diff --git a/docs/release-workflow.md b/docs/release-workflow.md index 3a950dce..fc5c4220 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -90,6 +90,56 @@ jobs: | `dry_run` | boolean | `false` | Run semantic-release in dry-run mode (no tags/releases) and preview the backmerge instead of applying it | | `prerelease_branches` | string | `develop,release-candidate` | Comma-separated list of branches treated as prerelease lines (beta/rc) | | `prerelease_backmerge_sync_enabled` | boolean | `false` | Merge `backmerge_source` into a prerelease branch before calculating its next version. Independent of `backmerge_enabled`, which also gates the separate post-release backmerge on `backmerge_source` itself. Opt-in — set to `true` to enable this pre-version-calculation sync, which can skip/block a release on prerelease branches when the merge cannot complete directly | +| `enable_release_announcement` | boolean | `true` | Announce the published release to the repository Slack channel after a successful release | +| `announcement_product_name` | string | `''` | Product name displayed in the announcement. Defaults to the repository name | +| `announcement_slack_channel` | string | `''` | Slack channel that receives the announcement. Defaults to the `RELEASE_SLACK_CHANNEL` repository variable; the announcement is skipped when both are empty | + +## Release Announcement + +After a successful release, the `announce_release` job calls +[`release-notification.yml`](release-notification.md) to post the published tag to the +channel owned by the calling repository. Routing is per-repo — no shared workflow change +is needed to add a repository or change its channel. + +### Setup in the consuming repository + +``` +Settings → Secrets and variables → Actions → New repository secret +Name: RELEASE_WEBHOOK_URL +Value: https://hooks.slack.com/services/xxx/yyy/zzz + +Settings → Secrets and variables → Actions → Variables → New repository variable +Name: RELEASE_SLACK_CHANNEL +Value: lerian-product-release +``` + +`RELEASE_WEBHOOK_NOTIFICATION_URL` is still accepted as a fallback for repositories that +already use that name. + +### Behavior + +| Condition | Result | +|---|---| +| Channel and webhook configured | Announcement is sent for the published tag | +| No channel (input and `RELEASE_SLACK_CHANNEL` both empty) | Job is skipped | +| Channel set but no webhook secret | Job runs, notification step is skipped (non-fatal) | +| `dry_run: true` | Payload is printed, nothing is sent | +| No release published in the run | Job is skipped | + +The announced tag comes from `publish_release_status.outputs.release_git_tag`, so monorepo +runs announce the tag actually published by the last matrix leg instead of the newest +release in the repository. + +### Discord + +Discord is intentionally not wired into this job. The underlying action +(`SethCohen/github-releases-to-discord`) reads the `release` event payload, which is absent +on the `push` event that drives this workflow. Keep Discord announcements on a dedicated +caller workflow that triggers `release-notification.yml` with `on: release`. + +> Repositories that already announce releases through a separate `on: release` workflow will +> get two messages once this job is active. Remove the standalone Slack announcement there, +> or set `enable_release_announcement: false`. ## Secrets @@ -109,6 +159,7 @@ jobs: | Secret | Description | |--------|-------------| | `NPM_TOKEN` | npm registry auth token, forwarded to the `Semantic Release` step. Only needed when the caller's own `.releaserc` includes `@semantic-release/npm` (a package with independent semver that publishes to an npm registry). Omit for repos that do not publish to npm. | +| `RELEASE_WEBHOOK_URL` | Slack webhook that receives the release announcement. Falls back to `RELEASE_WEBHOOK_NOTIFICATION_URL`; the announcement step is skipped when both are empty. | ## Outputs