diff --git a/SCOPE.md b/SCOPE.md index d793d29..28b13cd 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -135,11 +135,17 @@ user directory. "due_in_days": 7, "source_type": "rule | instruction", "source_id": "string", - "triggering_event_id": "uuid", + "triggering_event_id": "event uuid or stable source key", "activity_definition_id": "string" } ``` +`triggering_event_id` is accepted as a non-empty string. Event-driven +emissions should send the upstream activity event UUID. Scheduled or cron +emissions that do not have a concrete event row may send a stable source key +such as `scheduled`; issue-core stores the value verbatim in ingestion +metadata for traceability. + ### `POST /issues/` response ```json diff --git a/docs/argocd-gitops.md b/docs/argocd-gitops.md new file mode 100644 index 0000000..3df5d0b --- /dev/null +++ b/docs/argocd-gitops.md @@ -0,0 +1,168 @@ +# ArgoCD GitOps deployment - railiance01 + +This runbook captures the issue-core side of the railiance01 GitOps pilot. +It keeps secrets out of Git and leaves platform-owned bootstrap steps in +railiance-platform. + +## Source layout + +- Workload bundle: `issue-core/k8s/railiance/` +- Image: `gitea.coulomb.social/coulomb/issue-core:0.2.0` +- Container port and Service port: `8765` +- Cluster Service URL: `http://issue-core.issue-core.svc.cluster.local:8765` +- Tenant Application: `railiance-platform/argocd/applications/issue-core.application.yaml` + +The `Application` should point at this repo's `k8s/railiance` path and use +`CreateNamespace=true` for the `issue-core` namespace. The namespace itself is +therefore intentionally not duplicated in this bundle. + +## Platform gates + +The following pieces are owned by railiance-platform before the workload can +be fully reconciled: + +- ArgoCD repository credentials and the project/app-of-apps convention. +- The `issue-core` ArgoCD `Application`. +- External Secrets Operator and a `ClusterSecretStore` named `openbao`. +- OpenBao entries for the issue-core runtime Secret. + +Until those gates exist, `kubectl kustomize k8s/railiance` can render locally, +but the live `ExternalSecret` and `Deployment` are expected to wait. + +## Secret contract + +Kubernetes Secret name: `issue-core-runtime` + +Current issue-core manifest path, pending railiance-platform confirmation: + +```text +platform/workloads/issue-core/issue-core/issue-core-runtime +``` + +Credential route catalog id `issue-core-ingestion-api-key` is owned by +railiance-platform/OpenBao and is still marked draft/path TBD in the local +ops-warden catalog reviewed 2026-06-18. Confirm the canonical path before +provisioning the live Secret. + +Required properties: + +- `ISSUE_CORE_API_KEY` - shared ingestion key used by issue-core and + activity-core. +- `GITEA_BACKEND_TOKEN` - token for creating issues in cluster Gitea. + +Never write either value to Git, State Hub, workplans, logs, or chat. Record +only non-secret evidence such as Secret key count, ExternalSecret readiness, +HTTP status codes, and created issue URLs. + +## Build and publish + +Use the published package as the image input. For a reproducible release image, +pin the package version to the image tag: + +```bash +docker build --build-arg ISSUE_CORE_VERSION="==0.2.0" -t gitea.coulomb.social/coulomb/issue-core:0.2.0 . +docker push gitea.coulomb.social/coulomb/issue-core:0.2.0 +``` + +The Coulomb Gitea package is public-pullable for this image, so the workload +does not use an `imagePullSecret`. + +## Pre-sync validation + +From the issue-core repo: + +```bash +kubectl kustomize k8s/railiance +``` + +The rendered resources should be: + +- `ExternalSecret/issue-core-runtime` +- `ConfigMap/issue-core-backends` +- `Deployment/issue-core` +- `Service/issue-core` + +## Sync verification + +After railiance-platform syncs the tenant `Application`: + +```bash +kubectl get application issue-core -n argocd +kubectl -n issue-core get externalsecret issue-core-runtime +kubectl -n issue-core get secret issue-core-runtime +kubectl -n issue-core get deploy,pod,svc +``` + +Expected non-secret evidence: + +- ArgoCD Application reports `Synced` and `Healthy`. +- `ExternalSecret/issue-core-runtime` reports Ready. +- `Secret/issue-core-runtime` exists with two data keys. +- `Deployment/issue-core` has one available replica. +- `Service/issue-core` exposes port `8765`. + +Health check from inside the cluster: + +```bash +kubectl -n issue-core run issue-core-health --rm -i --restart=Never --image=curlimages/curl:8.8.0 -- http://issue-core:8765/healthz +``` + +## Ingestion smoke + +Run the authenticated smoke from a short-lived Job so the API key is mounted +from the Kubernetes Secret without printing it: + +```bash +kubectl -n issue-core delete job issue-core-smoke --ignore-not-found +kubectl -n issue-core apply -f - <<'YAML' +apiVersion: batch/v1 +kind: Job +metadata: + name: issue-core-smoke +spec: + ttlSecondsAfterFinished: 600 + backoffLimit: 0 + template: + spec: + restartPolicy: Never + containers: + - name: smoke + image: curlimages/curl:8.8.0 + env: + - name: ISSUE_CORE_API_KEY + valueFrom: + secretKeyRef: + name: issue-core-runtime + key: ISSUE_CORE_API_KEY + command: ["/bin/sh", "-ceu"] + args: + - | + curl -fsS -X POST "http://issue-core:8765/issues/" -H "Authorization: Bearer ${ISSUE_CORE_API_KEY}" -H "Content-Type: application/json" --data '{"title":"issue-core railiance01 smoke","description":"GitOps smoke created by the issue-core deployment runbook.","target_repo":"coulomb/markitect_project","priority":"low","labels":["smoke","issue-core"],"source_type":"rule","source_id":"issue-core-gitops-smoke","triggering_event_id":"scheduled","activity_definition_id":"issue-core-gitops-smoke"}' +YAML +kubectl -n issue-core wait --for=condition=complete job/issue-core-smoke --timeout=90s +kubectl -n issue-core logs job/issue-core-smoke +``` + +Acceptance evidence is HTTP 201 plus a response body containing `issue_id`, +`backend: "gitea"`, and an `issue_url` for cluster Gitea. + +Cleanup: + +```bash +kubectl -n issue-core delete job issue-core-smoke +``` + +## Activity-core handoff + +After issue-core is Ready and the shared `ISSUE_CORE_API_KEY` is available to +activity-core from the same approved OpenBao source: + +- Set `ISSUE_CORE_URL=http://issue-core.issue-core.svc.cluster.local:8765`. +- Set `ISSUE_SINK_TYPE=rest`. +- Inject the same `ISSUE_CORE_API_KEY` into the activity-core worker. +- Keep cron-triggered emissions explicit: `triggering_event_id` may be a stable + non-empty scheduler key such as `scheduled`; event-driven emissions should + continue to send the event UUID. + +Verify by running an activity-core emission and confirming that issue-core +returns HTTP 201 and creates a Gitea issue. diff --git a/issue_core/api/schemas.py b/issue_core/api/schemas.py index bcd9fed..194658d 100644 --- a/issue_core/api/schemas.py +++ b/issue_core/api/schemas.py @@ -2,14 +2,12 @@ Pydantic schemas for the issue-core REST API. The TaskIngestionRequest schema matches activity-core's IssueSink TaskSpec -payload exactly. See: +payload. See: - SCOPE.md "TaskSpec payload" section - activity-core docs/adr/adr-001-event-bridge-architecture.md """ from typing import List, Literal, Optional -from uuid import UUID - from pydantic import BaseModel, ConfigDict, Field @@ -31,7 +29,14 @@ class TaskIngestionRequest(BaseModel): due_in_days: Optional[int] = Field(default=None, ge=0) source_type: SourceType source_id: str = Field(..., min_length=1) - triggering_event_id: UUID + triggering_event_id: str = Field( + ..., + min_length=1, + description=( + "Activity event UUID, or a stable scheduler/source key when no " + "event row exists." + ), + ) activity_definition_id: str = Field(..., min_length=1) diff --git a/tests/test_api_ingest.py b/tests/test_api_ingest.py index de9d1d7..0966ec9 100644 --- a/tests/test_api_ingest.py +++ b/tests/test_api_ingest.py @@ -112,6 +112,30 @@ def test_ingest_creates_issue_with_x_api_key(client, valid_payload): assert response.status_code == 201, response.text +@pytest.mark.unit +def test_ingest_accepts_non_uuid_triggering_event_id(client, valid_payload, tmp_issue_store): + valid_payload["triggering_event_id"] = "scheduled" + response = client.post( + "/issues/", + json=valid_payload, + headers={"Authorization": f"Bearer {API_KEY}"}, + ) + assert response.status_code == 201, response.text + issue_id = response.json()["issue_id"] + + from issue_core.backends.local import LocalSQLiteBackend + + backend = LocalSQLiteBackend() + backend.connect({"type": "local", "db_path": str(tmp_issue_store / "issues.db")}) + try: + stored = backend.get_issue(issue_id) + assert stored is not None + ingestion = stored.sync_metadata.get("ingestion") or {} + assert ingestion["triggering_event_id"] == "scheduled" + finally: + backend.disconnect() + + @pytest.mark.unit def test_ingest_rejects_invalid_payload(client): bad = {"title": "no required fields"} diff --git a/workplans/ISSUE-WP-0003-railiance01-deployment.md b/workplans/ISSUE-WP-0003-railiance01-deployment.md index ec4dfd6..20d66a2 100644 --- a/workplans/ISSUE-WP-0003-railiance01-deployment.md +++ b/workplans/ISSUE-WP-0003-railiance01-deployment.md @@ -8,7 +8,7 @@ status: active owner: claude topic_slug: custodian created: "2026-06-19" -updated: "2026-06-19" +updated: "2026-06-23" state_hub_workstream_id: "896ace77-21b3-450b-8fb7-254aefc8c570" --- @@ -52,6 +52,15 @@ declarative Application and turning on the idle GitOps capability. registry) → `rsync` manifests → `kubectl apply` (see `activity-core/k8s/railiance/README.md`). +## Repo-side progress (2026-06-23) + +- Added `docs/argocd-gitops.md` with the issue-core GitOps runbook, including + image publish, ArgoCD sync checks, OpenBao/ExternalSecret contract, health + probe, authenticated ingestion smoke, cleanup, and activity-core handoff. +- Broadened `POST /issues/` so `triggering_event_id` accepts any non-empty + traceability string. Event-driven activity-core paths can still send UUIDs; + scheduled/cron paths may now send a stable key such as `scheduled`. + ## Decisions - **Deployment method = ArgoCD GitOps** (operator decision 2026-06-19). @@ -156,7 +165,7 @@ state_hub_task_id: "38887dd6-0988-4ad1-bc6b-2a1b8839829f" ```task id: ISSUE-WP-0003-T04 -status: todo +status: wait priority: high state_hub_task_id: "ad52527f-6222-4c11-9284-d8a3ed3b49ad" ``` @@ -171,6 +180,9 @@ state_hub_task_id: "ad52527f-6222-4c11-9284-d8a3ed3b49ad" - Never write the value to Git, manifests, State Hub, or logs. - Verify: both pods resolve a non-empty key; auth round-trip (401 without, 201 with). +- Current wait reason: requires railiance-platform/OpenBao operator action to + confirm/provision the canonical path and `ClusterSecretStore`; + issue-core records only the Secret contract and non-secret verification steps. ## In-cluster backend config (cluster Gitea / markitect) @@ -195,7 +207,7 @@ the cluster Gitea (markitect) backend. ```task id: ISSUE-WP-0003-T06 -status: todo +status: progress priority: high state_hub_task_id: "96b14cdb-364f-4eab-a80e-dd8b3859c694" ``` @@ -207,11 +219,10 @@ state_hub_task_id: "96b14cdb-364f-4eab-a80e-dd8b3859c694" once issue-core is Ready. - Inject `ISSUE_CORE_API_KEY` into the activity-core worker from the same OpenBao secret (T04). -- **Contract gap:** issue-core requires `triggering_event_id` as a UUID; - activity-core cron paths may send non-UUID keys (e.g. `"scheduled"`). - Event-driven emission with real event UUIDs works (the `str()` guard in - `issue_sink.py`, commit f05c56e, handles UUID objects). Align schemas before - enabling `rest` for cron-triggered rules. +- [x] **Contract gap closed on the issue-core side:** `POST /issues/` now + accepts `triggering_event_id` as a non-empty traceability string, so + event-driven paths can send UUIDs and cron paths can send stable keys such as + `"scheduled"`. - Verify: an activity-core run emits a task that lands in cluster Gitea via issue-core. @@ -219,7 +230,7 @@ state_hub_task_id: "96b14cdb-364f-4eab-a80e-dd8b3859c694" ```task id: ISSUE-WP-0003-T07 -status: todo +status: progress priority: medium state_hub_task_id: "8d853b8e-cfca-441d-b817-0a29e37bd66e" ``` @@ -229,8 +240,8 @@ state_hub_task_id: "8d853b8e-cfca-441d-b817-0a29e37bd66e" - ArgoCD Application Synced/Healthy; issue-core Pod Ready; Service reachable cluster-internal. - activity-core → issue-core emission returns 201 and creates a Gitea issue. -- Document the GitOps runbook (image build/push, ArgoCD sync, secret rotation, - rollback) in `docs/`. +- [x] Document the GitOps runbook (image build/push, ArgoCD sync, secret + contract, smoke, activity-core handoff) in `docs/argocd-gitops.md`. - Emit an `add_progress_event` milestone to the hub on completion. ---