Skip to content

Commit 9450796

Browse files
authored
Merge pull request #45 from trick77/feat/argocd-images-field
feat(argocd): require images[] as bridge to pipeline SHA
2 parents bb1876c + 98f9594 commit 9450796

8 files changed

Lines changed: 93 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ If `docker ps` fails, ask the user to start OrbStack.
2626
- **Raw payload always stored.** `payload JSONB` keeps the full request body even if fields are extracted into typed columns. Don't drop fields you don't currently use.
2727
- **`riptide.json` is config, not data.** `openshift/collector/riptide.json` is the in-repo sample; it declares teams (name + `group_email`) and org-wide automation rules. Edits go through PRs. The running pod hot-reloads via mtime in `RiptideConfigStore.maybe_reload()`. Do not propose moving the config into Postgres.
2828
- **Per-team bearer keys live in a separate file**: in production it is mounted from the `riptide-collector-team-keys` Secret (never committed); the in-repo `openshift/collector/team-keys.json` is a dev sample with deterministic test hashes (raw dev bearers documented in `compose.yaml`). Stored as sha256 hashes; `TeamKeysStore` hot-reloads it the same way as the config. The bearer **is** the team identity — every webhook is tagged with `team = caller_team`.
29-
- **No `service` column. No `service_id` on the wire.** Per-source aggregations group by `repo_full_name` / `pipeline_name` / `app_name` / `repo`; cross-source aggregations join on `commit_sha`; org-wide rollups group by `team`. Identifiers are lowercased at ingest (`commit_sha`, `revision`, `repo_full_name`, `branch_name`, `repo`) so SQL joins are case-stable. Do not propose adding a unified `service` column or `service_id` field — it served only single-pane labelling and was dropped.
29+
- **No `service` column. No `service_id` on the wire.** Per-source aggregations group by `repo_full_name` / `pipeline_name` / `app_name` / `repo`; org-wide rollups group by `team`. Cross-source joins for BB↔Pipeline use `commit_sha`; Argo CD joins are described in the next bullet. Identifiers are lowercased at ingest (`commit_sha`, `revision`, `repo_full_name`, `branch_name`, `repo`) so SQL joins are case-stable. Do not propose adding a unified `service` column or `service_id` field — it served only single-pane labelling and was dropped.
3030
- **`automation` is org-wide.** Bot definitions live at the config root, not per team.
3131
- **Metrics are computed on read, not at ingest.** Don't add aggregation tables or scheduled rollup jobs in v1. Schema additions should preserve raw events; new metrics are SQL queries against existing rows or future materialized views.
32-
- **Commit SHA is the universal join key.** All three sources record `commit_sha`/`revision`. Lead-time joins between `bitbucket_events`, `pipeline_events`, `argocd_events` happen on this column.
33-
- **`change_type` lives on Bitbucket events only.** Don't denormalise it onto pipeline / Argo rows; join via `commit_sha` at read time.
32+
- **Commit SHA joins Bitbucket↔Pipeline; Argo CD needs `payload->'images'`.** `bitbucket_events.commit_sha = pipeline_events.commit_sha` is a deterministic join (App-repo SHA on both sides). `argocd_events.revision` is the **GitOps-repo SHA** (proven empirically: four Apps for one service share one revision), so it does NOT directly match the other two. The App-repo SHA is embedded in image tags rendered via `.app.status.summary.images`; the receiver stores them in `payload->'images'`. A future correlator extracts SHAs from those tags to bridge Argo CD events to pipeline events. Do not propose adding a `service_id` or hand-coded service-name mappings to fix correlation — the image-tag SHA is the contract.
33+
- **`change_type` lives on Bitbucket events only.** Don't denormalise it onto pipeline / Argo rows; join Pipeline rows via `commit_sha` and Argo rows via the image-tag SHA in `payload->'images'` at read time.
3434
- **CI events are source-tagged, not source-routed.** All pipeline events from any CI (Jenkins, Tekton, …) land in the single `pipeline_events` table via `POST /webhooks/pipeline`, distinguished by the `source` column. Do not add per-CI tables or endpoints. The dedup key is `source#pipeline_name#run_id#phase`.
3535
- **Noergler events carry finops + reviewer-precision only.** The `noergler_events` table is `event_type`-discriminated (`completed` | `feedback`) and is fed by `POST /webhooks/noergler` from optional noergler instances. Do not re-emit PR lifecycle from noergler — `bitbucket_events` already covers open / merged / declined. Dedup keys: `completed#<run_id>` and `feedback#<finding_id>#<verdict>`.
3636
- **Senders verify reachability + bearer at startup via `GET /auth/ping`.** Authenticated endpoint returning `{"status":"ok","team":"<caller_team>"}`. Use this from any sender (noergler, future ones) to fail-fast on a wrong token. Don't reuse `/health` (unauth liveness) or `/ready` (unauth readiness) for this — those answer different questions.

docs/argocd-notification-template.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ data:
88
webhook:
99
riptide:
1010
method: POST
11+
# `toJson` / `default` / `list` are sprig functions (notifications-engine
12+
# registers sprig.TxtFuncMap in pkg/templates/service.go), so they're
13+
# available here. `toJson` serialises the image slice as a valid JSON
14+
# array and escapes any quote/backslash inside tag strings; a manual
15+
# {{range}} loop would emit invalid JSON if a tag ever contained a `"`.
16+
# `default (list) ...` collapses a nil summary into `[]` so the body
17+
# is always valid JSON, even before Argo has populated the summary
18+
# (which would otherwise render as `null` and fail the receiver schema).
1119
body: |
1220
{
1321
"app_name": "{{.app.metadata.name}}",
@@ -16,7 +24,8 @@ data:
1624
"operation_phase": "{{.app.status.operationState.phase}}",
1725
"started_at": "{{.app.status.operationState.startedAt}}",
1826
"finished_at": "{{.app.status.operationState.finishedAt}}",
19-
"destination_namespace": "{{.app.spec.destination.namespace}}"
27+
"destination_namespace": "{{.app.spec.destination.namespace}}",
28+
"images": {{toJson (default (list) .app.status.summary.images)}}
2029
}
2130
2231
trigger.on-deployed: |

docs/setup-argocd-notification.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,25 @@ oc -n argocd apply -f docs/argocd-notification-template.yaml
9191
```
9292

9393
This adds:
94-
- `template.app-deployed-riptide`
94+
- `template.app-deployed-riptide` — the webhook body includes `app_name`,
95+
`revision`, `sync_status`, `operation_phase`, `started_at`, `finished_at`,
96+
`destination_namespace`, and `images` (a JSON array rendered from
97+
`.app.status.summary.images`). `images` is the bridge for joining Argo CD
98+
events to pipeline events: `revision` is the GitOps-repo SHA, but image
99+
tags typically embed the App-repo commit SHA that the pipeline reports.
95100
- `trigger.on-deployed`, `trigger.on-sync-succeeded`, `trigger.on-sync-failed`
96101
(riptide-flavored)
97102

98-
If you are upgrading from an earlier riptide release, **re-apply this
99-
ConfigMap** so the template body includes the new `destination_namespace`
100-
field — riptide derives the `environment` column (and the prod-vs-non-prod
101-
metric filters) from the namespace suffix.
103+
> **Required field, hard cutover.** `images` is required on the receiver
104+
> side — webhooks rendered by an outdated ConfigMap will be rejected with
105+
> HTTP 422. Apply this ConfigMap **before** rolling out a receiver that
106+
> expects `images`, then restart the notifications controller so it
107+
> re-reads the template:
108+
> ```bash
109+
> oc -n argocd apply -f docs/argocd-notification-template.yaml
110+
> oc -n argocd rollout restart deploy/argocd-notifications-controller
111+
> ```
112+
> Reversing the order strands events between deploy and ConfigMap-apply.
102113

103114
## 3) Route teams to their service via AppProject defaults
104115

@@ -218,5 +229,17 @@ suffix counts as "production" is configured in
218229
default `prod`). To keep the database small, list non-prod stage suffixes
219230
in `environments.ignored_stages` (e.g. `["dev", "entw", "syst", "stage"]`)
220231
— matching events return `202 {"status":"ignored"}` and are dropped before
221-
insert. Aggregations group by `app_name`; cross-source joins (Pipeline,
222-
Bitbucket, Noergler) use `revision = commit_sha`.
232+
insert. Aggregations group by `app_name`.
233+
234+
The rendered image list lives in `payload->'images'`:
235+
236+
```sql
237+
SELECT app_name, payload->'images' AS images FROM argocd_events
238+
ORDER BY created_at DESC LIMIT 5;
239+
```
240+
241+
`revision` is the **GitOps-repo SHA**, not the App-repo SHA — direct joins
242+
to `pipeline_events.commit_sha` or `bitbucket_events.commit_sha` will not
243+
match. The App-repo SHA is typically embedded in the image tag (e.g.
244+
`registry/app:abc1234`); a future reader/correlator pulls SHAs out of
245+
`payload->'images'` to bridge to pipeline events.

src/riptide_collector/schemas/argocd.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ class ArgoCDWebhook(BaseModel):
1818
default=None,
1919
description="kubernetes namespace the app deployed into; suffix drives `environment`",
2020
)
21+
images: list[str] = Field(
22+
...,
23+
description=(
24+
"rendered images from the synced manifests (Argo CD field path "
25+
"`.app.status.summary.images`). may be empty for apps without pods. "
26+
"the App-Repo commit SHA is typically embedded in the image tag and "
27+
"bridges argocd.revision (GitOps-repo SHA) to pipeline.commit_sha "
28+
"(App-repo SHA) — correlation logic is built on top of this field."
29+
),
30+
)
2131

2232
@field_validator("started_at", "finished_at")
2333
@classmethod

tests/fixtures/argocd_synced.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@
55
"operation_phase": "Succeeded",
66
"started_at": "2026-04-28T10:09:00Z",
77
"finished_at": "2026-04-28T10:09:45Z",
8-
"destination_namespace": "payments-prod"
8+
"destination_namespace": "payments-prod",
9+
"images": [
10+
"quay.io/team/payments-api:abc1234"
11+
]
912
}

tests/test_auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ async def test_argocd_token_accepted_on_argocd(self, client: AsyncClient) -> Non
133133
json={
134134
"app_name": "x",
135135
"revision": "abc1234567890abc1234567890abc1234567890a",
136+
"images": [],
136137
},
137138
headers=_bearer(CHECKOUT_ARGOCD),
138139
)

tests/test_webhooks.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,40 @@ async def test_missing_revision_returns_422(self, client: AsyncClient) -> None:
320320
response = await client.post("/webhooks/argocd", json=payload, headers=ARGOCD_AUTH)
321321
assert response.status_code == 422
322322

323+
async def test_missing_images_returns_422(self, client: AsyncClient) -> None:
324+
payload = _load("argocd_synced.json")
325+
del payload["images"]
326+
response = await client.post("/webhooks/argocd", json=payload, headers=ARGOCD_AUTH)
327+
assert response.status_code == 422
328+
329+
async def test_empty_images_list_accepted(self, client: AsyncClient) -> None:
330+
payload = _load("argocd_synced.json")
331+
payload["images"] = []
332+
response = await client.post("/webhooks/argocd", json=payload, headers=ARGOCD_AUTH)
333+
assert response.status_code == 202
334+
335+
factory = TestBitbucketWebhook._fresh_session_factory(client)
336+
async with factory() as session:
337+
row = (await session.execute(select(ArgoCDEvent))).scalar_one()
338+
assert row.payload["images"] == []
339+
340+
async def test_images_persisted_in_payload(self, client: AsyncClient) -> None:
341+
payload = _load("argocd_synced.json")
342+
payload["images"] = [
343+
"quay.io/team/payments-api:abc1234",
344+
"quay.io/team/payments-worker:abc1234",
345+
]
346+
response = await client.post("/webhooks/argocd", json=payload, headers=ARGOCD_AUTH)
347+
assert response.status_code == 202
348+
349+
factory = TestBitbucketWebhook._fresh_session_factory(client)
350+
async with factory() as session:
351+
row = (await session.execute(select(ArgoCDEvent))).scalar_one()
352+
assert row.payload["images"] == [
353+
"quay.io/team/payments-api:abc1234",
354+
"quay.io/team/payments-worker:abc1234",
355+
]
356+
323357
async def test_ignored_stage_is_dropped(self, client_with_ignored_stages: AsyncClient) -> None:
324358
payload = _load("argocd_synced.json")
325359
payload["destination_namespace"] = "checkout-dev"

tests/test_webhooks_extra.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ async def test_argocd_phase_transitions_create_distinct_rows(
7878
"revision": "abc1234567890abc1234567890abc1234567890a",
7979
"sync_status": "Synced",
8080
"started_at": "2026-04-28T10:09:00Z",
81+
"images": ["quay.io/team/payments-api:abc1234"],
8182
}
8283
running = {**base, "operation_phase": "Running"}
8384
succeeded = {**base, "operation_phase": "Succeeded", "finished_at": "2026-04-28T10:09:45Z"}

0 commit comments

Comments
 (0)