diff --git a/.github/workflows/chart-release.yml b/.github/workflows/chart-release.yml index 1a1e2c3..3903665 100644 --- a/.github/workflows/chart-release.yml +++ b/.github/workflows/chart-release.yml @@ -233,6 +233,23 @@ jobs: helm package "${DIR}" helm push "${NAME}-${version}.tgz" "oci://${ECR_REGISTRY}/charts" + # Alias the pushed chart with a sha-suffixed tag. Dispatch's + # ECR-push backstop recovers commit provenance only from an + # X.Y.Z- tag (platform-gitops#1201); a plain-version + # event carries no commit and is ignored. Repo immutability + # blocks re-pushing existing tags but allows aliasing the + # same manifest under a new one. Warn-only: without the + # alias the direct CI notification below still covers the + # release. + SHORT_SHA="${GITHUB_SHA:0:7}" + manifest=$(aws ecr batch-get-image --repository-name "charts/${NAME}" \ + --image-ids imageTag="${version}" \ + --query 'images[0].imageManifest' --output text) + aws ecr put-image --repository-name "charts/${NAME}" \ + --image-tag "${version}-${SHORT_SHA}" \ + --image-manifest "$manifest" >/dev/null \ + || echo "::warning::sha alias tag failed for charts/${NAME}:${version}-${SHORT_SHA}" + # Tag immediately after successful push (retry up to 3 times). # If permanent failure the chart sits in ECR with no git tag — # the next run's recovery loop will reclaim it. @@ -261,6 +278,20 @@ jobs: mkdir -p release-info echo "${NAME} ${version}" > "release-info/${NAME}.txt" + echo "version=${version}" >> "$GITHUB_OUTPUT" + + # Direct publish signal to Dispatch (platform-gitops#1201) — the + # replacement for the create-webhook on the legacy git tag. Never + # fails the release; the EventBridge ECR-push backstop covers a + # missed notification. + - name: Notify Dispatch + if: steps.release.outcome == 'success' + uses: pinpredict/.github/actions/notify-dispatch@main + with: + kind: chart + service: ${{ matrix.name }} + version: ${{ steps.release.outputs.version }} + secret: ${{ secrets.CI_WEBHOOK_SECRET }} - name: Upload release info if: steps.release.outcome == 'success' diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index fbf3cc0..d03da6f 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -226,6 +226,20 @@ jobs: mkdir -p release-info echo "${NAME} ${version}" > "release-info/${NAME}.txt" + echo "version=${version}" >> "$GITHUB_OUTPUT" + + # Direct publish signal to Dispatch (platform-gitops#1201) — the + # replacement for the create-webhook on the legacy git tag. Never + # fails the release; the EventBridge ECR-push backstop covers a + # missed notification. + - name: Notify Dispatch + if: steps.release.outcome == 'success' + uses: pinpredict/.github/actions/notify-dispatch@main + with: + kind: image + service: ${{ matrix.name }} + version: ${{ steps.release.outputs.version }} + secret: ${{ secrets.CI_WEBHOOK_SECRET }} - name: Upload release info if: steps.release.outcome == 'success' diff --git a/AGENTS.md b/AGENTS.md index e363158..55b567b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,9 @@ Both workflows use a three-shot retry pattern (try / sleep 30 / retry / sleep 60 - **ECR is the version record** (platform-gitops#1201): both release workflows resolve the next version as the highest strict-`X.Y.Z` tag in the service's ECR repo (image or `charts/` OCI) plus one, then probe-and-bump past any existing candidate (ECR tags are immutable). Git tags are never read for versioning. - **Release marker refs**: `refs/releases/image/` and `refs/releases/chart/` — one mutable ref per service, force-advanced to the released SHA on every successful push. They are the change-detection baseline for `discover-services` and chart-release's prepare job (legacy `image|chart//X.Y.Z` tag is the fallback until a service releases once with the marker in place). Not tags, so they don't feed Kargo's tag enumeration or the GitHub `create` webhook. Readers must fetch them explicitly (`+refs/releases/*:refs/releases/*`). -- Per-service image tag: `image//X.Y.Z` (immutable git tag, pushed after successful ECR push). **Legacy mirror** — still minted only because Dispatch correlates on the `create` webhook; goes away once Dispatch is notified directly (#1201 dependency 2). +- **Dispatch publish notification**: after each successful push, both release workflows call `actions/notify-dispatch` — a signed POST (service, version, full SHA, run URL) through the public webhook-forwarder `/dispatch/ci` route, HMAC'd with the org `CI_WEBHOOK_SECRET` (reaches reusable workflows via `secrets: inherit`). Warn-only on failure: the EventBridge ECR-push backstop covers a missed call. +- **Chart sha alias**: chart-release aliases the pushed OCI chart as `X.Y.Z-` (via `aws ecr put-image` on the same manifest — allowed under tag immutability) so the ECR-push backstop can recover commit provenance for charts, mirroring the image tag pair. Semver-prerelease form, so Kargo chart Warehouses ignore it. +- Per-service image tag: `image//X.Y.Z` (immutable git tag, pushed after successful ECR push). **Legacy mirror** — the direct notification above supersedes the `create`-webhook correlation; the tags are removed in #1201's final step once the new signals are verified. - Per-chart tag: `chart//X.Y.Z` — same legacy status. - Per-service config tag (Kargo freight for `-config` Warehouse): `vX.Y.Z+` — semver **build metadata** form (`+`), not pre-release (`-`). The `+` form passes `semverConstraint: ">=0.0.0"` cleanly; the pre-release form would require `>=0.0.0-0`. **This tag family stays** — it is genuine Kargo freight. diff --git a/README.md b/README.md index d2e83cf..6b47b98 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Why `.github` and not a dedicated `github-actions` repo: `.github` is *the* GitH | Action | Purpose | |---|---| +| `notify-dispatch` | POSTs a signed publish notification (service, version, SHA, run URL) to Dispatch's public `/dispatch/ci` route after a successful ECR push. Called by both release workflows; warn-only on failure (the EventBridge ECR-push backstop covers a missed call). | | `discover-services` | Reads `.platform/services/*.yaml` and emits a docker matrix of services whose docker-relevant files changed since their last release (baseline = `refs/releases/image/` marker ref; legacy `image//*` tag fallback). Also emits `charts_changed`. | | `validate-platform-service` | Pre-merge static + render check for added/modified `.platform/services/*.yaml`. Renders each via `charts/service-template` for every env in `environments[]` with all `renderXxx` flags forced on; verifies `repositories.chart` resolves to a real `charts//Chart.yaml`. Closes the gap from platform-gitops#544 — every dis-opticodds-props-streamer failure mode would have failed CI here. | | `validate-reusable-inputs` | Cross-repo input validation for callers of `pinpredict/.github` reusable workflows. Diffs every `with:` block against the referenced workflow's `on.workflow_call.inputs` map; fails on unknown keys or missing-required keys. Closes the gap left by stock `actionlint`, which can't fetch remote reusable workflows (platform-gitops#1045). Runs automatically as a sibling job in `actionlint.yml`, so any consumer that already `uses:` that reusable workflow inherits it. | diff --git a/actions/notify-dispatch/action.yml b/actions/notify-dispatch/action.yml new file mode 100644 index 0000000..5eb003b --- /dev/null +++ b/actions/notify-dispatch/action.yml @@ -0,0 +1,63 @@ +name: notify-dispatch +description: >- + Notify Dispatch that an artifact was published (platform-gitops#1201). + POSTs a signed publish notification through the public webhook-forwarder + /dispatch/ci route so the deploy flow's publish stage fills without the + legacy image/*+chart/* git-tag webhook. Never fails the release: a missed + notification is covered by the EventBridge ECR-push backstop. + +inputs: + kind: + description: '"image" or "chart"' + required: true + service: + description: Service / chart name (the tag's middle segment) + required: true + version: + description: Released version (X.Y.Z) + required: true + secret: + description: >- + HMAC secret (org Actions secret CI_WEBHOOK_SECRET, reaches reusable + workflows via `secrets: inherit`). Empty skips the notification with + a warning rather than failing — the ECR-push backstop still covers + deploy tracking. + required: true + url: + description: Public /dispatch/ci endpoint (webhook-forwarder API Gateway, dev) + required: false + default: "https://gd7tapt7j8.execute-api.us-east-1.amazonaws.com/dispatch/ci" + +runs: + using: composite + steps: + - name: Notify Dispatch + shell: bash + env: + KIND: ${{ inputs.kind }} + SERVICE: ${{ inputs.service }} + VERSION: ${{ inputs.version }} + SECRET: ${{ inputs.secret }} + URL: ${{ inputs.url }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + if [ -z "${SECRET}" ]; then + echo "::warning::CI_WEBHOOK_SECRET not available — skipping Dispatch notification (ECR-push backstop covers deploy tracking)" + exit 0 + fi + + body=$(printf '{"repo":"%s","kind":"%s","service":"%s","version":"%s","sha":"%s","run_url":"%s"}' \ + "${GITHUB_REPOSITORY}" "${KIND}" "${SERVICE}" "${VERSION}" "${GITHUB_SHA}" "${RUN_URL}") + sig=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "${SECRET}" | awk '{print $NF}') + + status=$(curl -sS -o /tmp/dispatch-resp.json -w '%{http_code}' -X POST "${URL}" \ + -H "Content-Type: application/json" \ + -H "X-Dispatch-Signature: ${sig}" \ + --data "$body" || echo "000") + + if [ "$status" -ge 200 ] && [ "$status" -lt 300 ]; then + echo "Dispatch notified: ${KIND} ${SERVICE} ${VERSION}" + else + echo "::warning::Dispatch notification returned ${status}: $(cat /tmp/dispatch-resp.json 2>/dev/null || true) — ECR-push backstop covers this release" + fi