diff --git a/.github/workflows/gitops-update.yml b/.github/workflows/gitops-update.yml index 5ad03832..ba0eb9f4 100644 --- a/.github/workflows/gitops-update.yml +++ b/.github/workflows/gitops-update.yml @@ -77,7 +77,11 @@ on: type: string default: '' kustomize_image_name: - description: 'Required when gitops_layout=kustomize. Image reference matched by `kustomize edit set image` (e.g. ghcr.io/lerianstudio/ungoliant-controller).' + description: 'Required when gitops_layout=kustomize, unless every image is covered by kustomize_image_mappings. Image reference matched by `kustomize edit set image` (e.g. ghcr.io/lerianstudio/ungoliant-controller). Also the fallback for artifacts absent from kustomize_image_mappings.' + type: string + default: '' + kustomize_image_mappings: + description: 'JSON object mapping artifact names to kustomize image references (e.g. {"ungoliant-api":"ghcr.io/lerianstudio/ungoliant-api"}). Use when one release publishes more than one image into the same kustomization.yaml — each downloaded artifact is named after the app that built it. Artifacts not listed fall back to kustomize_image_name. Kustomize layout only.' type: string default: '' kustomize_environments: @@ -267,14 +271,29 @@ jobs: env: KUSTOMIZE_BASE_PATH: ${{ inputs.kustomize_base_path }} KUSTOMIZE_IMAGE_NAME: ${{ inputs.kustomize_image_name }} + KUSTOMIZE_IMAGE_MAPPINGS: ${{ inputs.kustomize_image_mappings }} run: | set -euo pipefail if [[ -z "$KUSTOMIZE_BASE_PATH" ]]; then echo "::error::gitops_layout=kustomize requires kustomize_base_path." exit 1 fi - if [[ -z "$KUSTOMIZE_IMAGE_NAME" ]]; then - echo "::error::gitops_layout=kustomize requires kustomize_image_name." + # An empty or null mapping value would resolve to nothing and silently + # fall back to kustomize_image_name, writing the extra artifact's tag + # onto the primary image — the exact failure the mapping exists to + # prevent. Reject it instead of resolving it. + MAPPED_IMAGES=0 + if [[ -n "$KUSTOMIZE_IMAGE_MAPPINGS" ]]; then + if ! jq -e 'type == "object" and all(.[]; type == "string" and length > 0)' >/dev/null 2>&1 <<< "$KUSTOMIZE_IMAGE_MAPPINGS"; then + echo "::error::kustomize_image_mappings must be a JSON object whose every value is a non-empty image reference string." + exit 1 + fi + MAPPED_IMAGES=$(jq -r 'length' <<< "$KUSTOMIZE_IMAGE_MAPPINGS") + echo "kustomize_image_mappings:" + jq -r 'to_entries[] | " \(.key) -> \(.value)"' <<< "$KUSTOMIZE_IMAGE_MAPPINGS" + fi + if [[ -z "$KUSTOMIZE_IMAGE_NAME" && "$MAPPED_IMAGES" -eq 0 ]]; then + echo "::error::gitops_layout=kustomize requires kustomize_image_name or a non-empty kustomize_image_mappings." exit 1 fi @@ -469,6 +488,7 @@ jobs: GITOPS_LAYOUT: ${{ inputs.gitops_layout }} KUSTOMIZE_BASE_PATH: ${{ inputs.kustomize_base_path }} KUSTOMIZE_IMAGE_NAME: ${{ inputs.kustomize_image_name }} + KUSTOMIZE_IMAGE_MAPPINGS: ${{ inputs.kustomize_image_mappings }} KUSTOMIZE_ENVIRONMENTS: ${{ inputs.kustomize_environments }} MANIFEST_FILE: shared-workflows/${{ inputs.deployment_matrix_file }} UPDATE_SANDBOX: ${{ inputs.update_sandbox }} @@ -677,16 +697,37 @@ jobs: FILE_BEFORE_HASH=$(sha256sum "$KUSTOMIZATION_FILE" | cut -d' ' -f1) # Apply each available artifact tag via `kustomize edit set image`. - # The artifact value (TAG) is the only thing that varies; we set the - # same image_name for every artifact since kustomize layouts typically - # have a single image per kustomization.yaml. If an app has multiple - # images, callers can extend kustomize_image_name later (out of scope here). + # Each artifact file is named after the app that built it + # (build.yml writes gitops-tags/.tag), so that basename is the + # key used to resolve which image the tag belongs to: + # kustomize_image_mappings[], falling back to + # kustomize_image_name when the app is not listed. + # A release publishing more than one image into the same + # kustomization.yaml maps every extra app explicitly; single-image + # callers leave the mapping empty and every artifact resolves to + # kustomize_image_name, exactly as before. for artifact_file in .gitops-tags/*; do [[ -f "$artifact_file" ]] || continue TAG="$(cut -d= -f2 < "$artifact_file" | tr -d '[:space:]')" [[ -n "$TAG" ]] || continue - echo " kustomize edit set image ${KUSTOMIZE_IMAGE_NAME}=${KUSTOMIZE_IMAGE_NAME}:${TAG}" - ( cd "gitops/${TARGET_DIR}" && kustomize edit set image "${KUSTOMIZE_IMAGE_NAME}=${KUSTOMIZE_IMAGE_NAME}:${TAG}" ) + + ARTIFACT_APP="$(basename "$artifact_file")" + ARTIFACT_APP="${ARTIFACT_APP%.tag}" + + IMAGE_NAME="" + if [[ -n "$KUSTOMIZE_IMAGE_MAPPINGS" ]]; then + IMAGE_NAME=$(jq -r --arg app "$ARTIFACT_APP" '.[$app] // empty' <<< "$KUSTOMIZE_IMAGE_MAPPINGS") + fi + if [[ -z "$IMAGE_NAME" ]]; then + IMAGE_NAME="$KUSTOMIZE_IMAGE_NAME" + fi + if [[ -z "$IMAGE_NAME" ]]; then + echo "::warning::No kustomize image resolved for artifact '${ARTIFACT_APP}' — not in kustomize_image_mappings and kustomize_image_name is empty. Skipping." + continue + fi + + echo " ${ARTIFACT_APP}: kustomize edit set image ${IMAGE_NAME}=${IMAGE_NAME}:${TAG}" + ( cd "gitops/${TARGET_DIR}" && kustomize edit set image "${IMAGE_NAME}=${IMAGE_NAME}:${TAG}" ) done FILE_AFTER_HASH=$(sha256sum "$KUSTOMIZATION_FILE" | cut -d' ' -f1) diff --git a/.github/workflows/go-release.yml b/.github/workflows/go-release.yml index e8200ad7..335a44a4 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: @@ -209,8 +221,12 @@ on: description: 'Required when gitops_layout=kustomize. Path within the gitops repo to the kustomization folder (e.g. environments/anacleto/kustomize/ungoliant-controller). Supports ${SERVER} and ${ENV} placeholders.' type: string default: '' + kustomize_image_mappings: + description: 'JSON object mapping artifact names to kustomize image references (e.g. {"ungoliant-api":"ghcr.io/lerianstudio/ungoliant-api"}). Use when extra_builds groups publish additional images into the same kustomization.yaml — artifacts not listed fall back to kustomize_image_name. Requires gitops_artifact_pattern to also match the extra groups (the default only matches the repository name).' + type: string + default: '' kustomize_image_name: - description: 'Required when gitops_layout=kustomize. Image reference matched by `kustomize edit set image` (e.g. ghcr.io/lerianstudio/ungoliant-controller).' + description: 'Required when gitops_layout=kustomize, unless every image is covered by kustomize_image_mappings. Image reference matched by `kustomize edit set image` (e.g. ghcr.io/lerianstudio/ungoliant-controller).' type: string default: '' kustomize_environments: @@ -383,6 +399,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 @@ -686,6 +705,7 @@ jobs: gitops_layout: ${{ inputs.gitops_layout }} kustomize_base_path: ${{ inputs.kustomize_base_path }} kustomize_image_name: ${{ inputs.kustomize_image_name }} + kustomize_image_mappings: ${{ inputs.kustomize_image_mappings }} kustomize_environments: ${{ inputs.kustomize_environments }} kustomize_version: ${{ inputs.kustomize_version }} argocd_app_name_template: ${{ inputs.argocd_app_name_template }} diff --git a/.github/workflows/go-security.yml b/.github/workflows/go-security.yml index 56e07ede..c9cb0d60 100644 --- a/.github/workflows/go-security.yml +++ b/.github/workflows/go-security.yml @@ -192,7 +192,7 @@ jobs: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.95.9 + uses: trufflesecurity/trufflehog@6f3c981e7b77f235fd2702dd74af25fc4b72bf11 # v3.96.0 with: path: ./ base: ${{ github.event.repository.default_branch }} 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/gitops-update-workflow.md b/docs/gitops-update-workflow.md index ca33807b..097513d3 100644 --- a/docs/gitops-update-workflow.md +++ b/docs/gitops-update-workflow.md @@ -92,6 +92,35 @@ Notes: - Use `${SERVER}` / `${ENV}` placeholders in `kustomize_base_path` for multi-cluster / multi-env kustomize layouts (e.g. `environments/${SERVER}/kustomize/${ENV}/my-app`). - `configmap_updates` is ignored under `gitops_layout=kustomize` (out of scope for v1). +### Kustomize with More Than One Image + +When a single release publishes several images into the same `kustomization.yaml` — for example a controller and a slimmer read API built from the same module — map each extra image to the artifact that carries its tag. Every downloaded artifact file is named after the app that built it (`build.yml` writes `gitops-tags/.tag`), and that name is the mapping key. + +```yaml +update_gitops: + needs: build + if: needs.build.result == 'success' + uses: LerianStudio/github-actions-shared-workflows/.github/workflows/gitops-update.yml@v1.0.0 + with: + gitops_repository: LerianStudio/midaz-firmino-gitops + gitops_layout: kustomize + kustomize_base_path: environments/anacleto/kustomize/ungoliant-controller + kustomize_image_name: ghcr.io/lerianstudio/ungoliant-controller + kustomize_image_mappings: | + { + "ungoliant-api": "ghcr.io/lerianstudio/ungoliant-api" + } + artifact_pattern: 'gitops-tags-*' + argocd_app_name_template: '{server}-{app}' + secrets: inherit +``` + +Notes: +- Artifacts absent from the mapping fall back to `kustomize_image_name`, so single-image callers need no mapping at all. +- An artifact that resolves to neither (empty mapping entry and empty `kustomize_image_name`) is skipped with a warning instead of being written under the wrong image name. +- `artifact_pattern` must match the extra apps too. The default derived from the repository name only matches the primary app, so a second image named after a component needs a broader pattern such as `gitops-tags-*`. +- All images resolve into the same `kustomization.yaml` (`kustomize_base_path`). A second image living under a different path is not supported yet. + ### Multi-Component Example (Midaz) ```yaml @@ -135,7 +164,8 @@ update_gitops: | `configmap_updates` | string | - | JSON object mapping artifact names to configmap keys. Helmfile layout only; ignored for kustomize | | `gitops_layout` | string | `helmfile` | GitOps layout strategy: `helmfile` (default) or `kustomize` | | `kustomize_base_path` | string | - | Required when `gitops_layout=kustomize`. Path within the gitops repo to the kustomization folder. Supports `${SERVER}` / `${ENV}` placeholders | -| `kustomize_image_name` | string | - | Required when `gitops_layout=kustomize`. Image reference matched by `kustomize edit set image` | +| `kustomize_image_name` | string | - | Required when `gitops_layout=kustomize`, unless every image is covered by `kustomize_image_mappings`. Image reference matched by `kustomize edit set image`; also the fallback for artifacts absent from the mapping | +| `kustomize_image_mappings` | string | - | JSON object mapping artifact names to kustomize image references. Use when one release publishes more than one image into the same `kustomization.yaml`. Kustomize layout only | | `kustomize_environments` | string | - | Optional space-separated env list overriding the default tag-based env loop when `gitops_layout=kustomize`. Leave empty for layouts without env split | | `kustomize_version` | string | `v5.4.3` | Version of kustomize CLI to install (only when `gitops_layout=kustomize`) | | `argocd_app_name_template` | string | `{server}-{app}-{env}` | Template for the ArgoCD application name. Supports `{server}`, `{app}`, `{env}`. For kustomize layouts without env split, use e.g. `{server}-{app}` | diff --git a/docs/go-release-workflow.md b/docs/go-release-workflow.md index 08eaf83a..9aa89ecb 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` | @@ -69,7 +72,8 @@ A third layout needs `release_single_app: true`: **one semantic-release tag for | `enable_docker_login` | Log in to DockerHub in the gitops-update job | boolean | `false` | | `gitops_layout` | GitOps layout strategy: `helmfile` (default, current behavior) or `kustomize` | string | `helmfile` | | `kustomize_base_path` | Required when `gitops_layout=kustomize`. Path within the gitops repo to the kustomization folder (supports `${SERVER}`/`${ENV}` placeholders) | string | `''` | -| `kustomize_image_name` | Required when `gitops_layout=kustomize`. Image reference matched by `kustomize edit set image` | string | `''` | +| `kustomize_image_name` | Required when `gitops_layout=kustomize`, unless every image is covered by `kustomize_image_mappings`. Image reference matched by `kustomize edit set image`; also the fallback for artifacts absent from the mapping | string | `''` | +| `kustomize_image_mappings` | JSON object mapping artifact names to kustomize image references, for `extra_builds` groups publishing additional images into the same `kustomization.yaml` (see [Multiple build groups](#multiple-build-groups)) | string | `''` | | `kustomize_environments` | Optional space-separated env list overriding the default tag-based env loop when `gitops_layout=kustomize` | string | `''` | | `kustomize_version` | Version of kustomize CLI to install (used only when `gitops_layout=kustomize`) | string | `v5.4.3` | | `argocd_app_name_template` | Template for the ArgoCD application name. Placeholders `{server}`, `{app}`, `{env}`. For kustomize layouts without env split use e.g. `{server}-{app}` | string | `{server}-{app}-{env}` | @@ -212,6 +216,26 @@ Extra builds run on tag push (beta/rc, and stable when `build_on_release` is off > **Important** — `update_gitops` downloads artifacts by `gitops_artifact_pattern` (default `gitops-tags-*`). When an extra build produces an image whose name does **not** start with the repo name (e.g. `mock-btg-server`), set `gitops_artifact_pattern` to a wildcard that captures every image (e.g. `gitops-tags-*`), otherwise that image's tag artifact is skipped. +> **Kustomize layout** — under `gitops_layout: kustomize` the single top-level `kustomize_image_name` used to be applied to *every* downloaded artifact, so an extra build's tag could never reach its own image: both `newTag`s resolved to the primary image name. Map each extra image to the artifact that carries its tag with `kustomize_image_mappings`, keyed by app name (`build.yml` writes `gitops-tags/.tag`). Artifacts not listed still fall back to `kustomize_image_name`, and one that resolves to neither is skipped with a warning rather than written under the wrong image. Set `gitops_artifact_pattern: 'gitops-tags-*'` as well, per the note above — the default pattern only matches the repository name. All images must live in the same `kustomization.yaml`; a second image under a different `kustomize_base_path` is not supported yet. +> +> ```yaml +> kustomize_image_name: ghcr.io/lerianstudio/ungoliant-controller +> kustomize_image_mappings: | +> { +> "ungoliant-api": "ghcr.io/lerianstudio/ungoliant-api" +> } +> gitops_artifact_pattern: 'gitops-tags-*' +> extra_builds: | +> [ +> { +> "filter_paths": "components/api", +> "app_name_overrides": "components/api:ungoliant-api", +> "shared_paths": "go.mod\ngo.sum\ncmd/\ninternal/\ncomponents/api/", +> "enable_gitops_artifacts": true +> } +> ] +> ``` + > **Separate ArgoCD Application** — the wildcard-pattern approach above only works when the extra image is deployed *within the same app's* `values.yaml` (different nested key, same `app_name`/directory). If the extra build's component is its own, independent ArgoCD Application (its own `environments/*/helmfile/applications//values.yaml`), the single `update_gitops` job can't target it — `app_name` is one value per job call. Add a second, dedicated job in your caller `release.yml` that calls `gitops-update.yml` directly with that component's own `app_name`, `artifact_pattern`, and `yaml_key_mappings`, independent of the primary `update_gitops`. The primary `update_gitops` job only runs when the primary build actually produced artifacts (`needs.build.outputs.has_builds == 'true'`), so it skips cleanly — instead of failing — on releases where only an `extra_builds` group changed. ```yaml 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