# CI/CD integration

API version: `2026-09-08`. This is an engineering release gate over recorded evidence, not a CRA compliance verdict. No automation endpoint records an approval, changes product facts, decides Article 14 outcomes, signs a declaration or changes billing.

## Setup and scope

Create the workspace and product in ConformOps and assign active Continuous coverage. In **Product settings > CI/CD**, an Owner or Admin creates an expiring credential for exactly that product. Copy the value once into a masked CI secret named `CONFORMOPS_TOKEN`. Keep it out of shell arguments, command tracing, build artifacts and logs. Set `CONFORMOPS_PRODUCT_ID` and `CONFORMOPS_URL` to the product's UUID and the HTTPS origin of the intended environment. Staging credentials and endpoints are separate from production.

Credentials are random 256-bit bearer values stored only as SHA-256 hashes. They expire in 1-90 days, can be revoked immediately, and are limited to 20 active credentials per product and 120 requests per minute per credential. Every call rechecks the issuer's current Owner/Admin membership, verified profile, original membership identity, product organization, erasure/lifecycle state and active Continuous capability. A credential grants no access to other products, even in the same workspace. Removal and re-invitation do not reactivate the old membership's credential. Permission changes can suspend access; explicitly revoke credentials when retiring an integration.

| Scope | Permitted operations |
| --- | --- |
| `evidence:write` | Start, upload, inspect, seal, retry or cancel source acquisition; supply an SBOM for a named release |
| `assessments:write` | Start an assessment against an explicit release and source snapshot |
| `results:read` | Read exact-run machine summaries and the current gate evaluation |
| `events:read` | Poll the durable event feed |

Creating or changing credentials, release policy and webhooks requires the signed-in Owner/Admin UI or its session-authenticated `/api/projects/{productId}/automation` endpoint. Bearer credentials cannot change their own permissions or gate policy.

## CLI

Download the [standalone CLI](https://conformops.eu/downloads/conformops-cli/2026-09-08/conformops.mjs) and [SHA-256 checksums](https://conformops.eu/downloads/conformops-cli/2026-09-08/sha256.txt) from the same release. Use Node 22.23.2 or later. Verify the checksum, review and pin the downloaded file in your CI tooling, and keep it outside the assessed source directory. It requires no package installation or access to the ConformOps application repository. Invoke `node conformops.mjs --help` for the command list. The [complete reference](https://conformops.eu/downloads/conformops-cli/2026-09-08/reference.md) is available without signing in.

With the three environment variables above supplied by your CI secret/settings store, run from the directory holding the CLI:

```sh
node conformops.mjs pipeline \
  --directory /workspace/product-source \
  --release 2.4.0 \
  --key release-2.4.0-build-123 \
  --sbom /workspace/artifacts/release.cdx.json \
  --timeout-seconds 1800
```

The CLI inspects the directory before creating an upload, refuses symlinks and colliding paths, skips generated/vendor directories using the same path policy as the web importer, and reads files sequentially. It never executes repository code. Raw archives are not uploaded. Supported source is sent over HTTPS and sanitized on the server before persistence. Binary/non-UTF-8 and oversized evidence exclusions are recorded; oversized relevant files make coverage partial. The existing bounds apply: 250,000 inspected entries, 25,000 evidence files, 512 MiB text, 2 MiB per source file and 2,000 supplied SBOM components. One source file is sent per API call, paced below the credential's rate limit. Large repositories need a longer timeout, up to 86,400 seconds, and sufficient CI job time.

The command creates or recovers the named source ingestion, uploads immutable file paths, supplies the optional CycloneDX JSON or SPDX JSON document to that release **before sealing**, waits for the ingestion's run, then evaluates that exact run. A source upload references the bytes collected by the pipeline; it does not independently attest a Git commit or build. To reassess source already held by ConformOps, use the release and snapshot IDs from a previous result. There is no vendor-specific scanner or arbitrary-URL import.

```sh
node conformops.mjs results --run RUN_UUID
node conformops.mjs assess --release-id RELEASE_UUID --snapshot SNAPSHOT_UUID
node conformops.mjs gate --run RUN_UUID
node conformops.mjs source --source INGESTION_UUID
node conformops.mjs retry --source INGESTION_UUID
node conformops.mjs cancel --source INGESTION_UUID
node conformops.mjs events --after 0
```

`pipeline` and `gate` exit with 0 only for `passed`, 2 for `blocked`, 3 for `degraded`, 4 for `unknown`, 5 for `pending`, and 1 for a request, timeout or CLI error. Other commands return 0 when their API operation succeeds, regardless of any gate shown in a read response. CLI stdout contains machine summaries; stderr contains fixed error codes, never request contents or exception text. Do not use `continue-on-error` on the release-gating step. Human approvals can leave a pipeline pending; an authorized person records the decision in ConformOps and the pipeline repeats `gate --run` afterward.

## API contract

Use `Authorization: Bearer <credential>` over HTTPS. Responses use `Cache-Control: no-store`. All paths below begin `/api/v1/projects/{productId}/`. POST bodies are JSON. Unknown operations and fields are refused.

| Method and path | Body or behavior |
| --- | --- |
| `POST sources` | `{ "releaseVersion": "2.4.0", "productComponentId": "optional UUID", "buildFingerprint": "optional 64-character lowercase SHA-256" }`. Requires `Idempotency-Key`; returns `ingestionId`. |
| `GET sources/{id}` | IDs, phase, state, aggregate counts and a fixed failure code. Includes the release ID for the supplied SBOM. |
| `POST sources/{id}/files` | `{ "path": "package.json", "content": "...", "originalSizeBytes": 123 }`. One UTF-8 file, relative path, exact decoded UTF-8 byte length. Same path/content is idempotent; different content at the same path is refused. |
| `POST sboms` | `{ "releaseId": "UUID", "content": "JSON document text" }`. Requires `Idempotency-Key`. At most 8,000,000 UTF-8 bytes, including any UTF-8 BOM; the JSON request itself is bounded to 16,100,000 bytes. Returns both supplied and sanitized hashes, ID and structural usability. |
| `POST sources/{id}/complete` | `{ "skipped": { "binary_or_invalid_utf8": 1 }, "oversizedPaths": [] }`. Optional bounded exclusions. Seals and dispatches durable ingestion, which triggers an assessment. Repeated seals return its existing state. |
| `POST sources/{id}/retry` | `{}`. Only retryable acquisitions can retry. |
| `POST sources/{id}/cancel` | `{}`. Requests cancellation of unfinished acquisition. |
| `POST assessments` | `{ "releaseId": "UUID", "sourceSnapshotId": "UUID" }`. References a snapshot held by ConformOps. Content-based run deduplication is server controlled; callers cannot create duplicate work with random keys. |
| `GET runs/{id}` | Exact run, lineage identifiers, complete requirement states, recorded gate states, bounded counts, freshness facts and current policy verdict. No source paths, component/advisory names, excerpts or document bodies. |
| `GET results` | Same projection for the latest attempt. `baselineRunId` separately names the last completed baseline. Failed or running attempts never become a usable result. |
| `GET events?after=0` | Up to 100 ordered events, `nextCursor` and `hasMore`. Persist the cursor only after processing the page. |

Source and SBOM request keys must be 8-128 ASCII letters, digits, dots, underscores, colons or hyphens, unique per product and logical operation. Reuse the key and identical request after response loss. The server stores only key/request hashes and fixed resource metadata. Conflicting request parameters return 409. An interrupted request is recovered by its reserved resource ID if committed; otherwise it returns `REQUEST_PENDING` rather than starting potentially overlapping work. Inspect the product and retry the same key; if it remains pending, contact the operator to reconcile it. A definitively failed request returns `REQUEST_FAILED_USE_NEW_KEY`. Never change a key blindly after a network error. Upload sessions expire after 24 hours. Source files are immutable after seal, and completed release labels cannot be reused for different releases.

The pipeline command reuses an upload on retry. The CLI includes a fingerprint of inspected source bytes, exclusions and supplied SBOM bytes in its creation request, and checks file hashes again before upload. Changed builds cannot reuse the same key. The optional API buildFingerprint is client-supplied retry identity, not a server attestation of a build or commit. Always keep source bytes and the SBOM immutable for a build key. If the directory or SBOM changes, use a new release/build key; rerunning an unchanged completed key refers to the prior release. Derived SBOM keys are separately hashed and bounded. An SBOM supplied after the initial run invalidates that baseline and needs an explicit repeat assessment; it never silently changes the old run. SBOM presence, structural usability and actual completeness remain separate statements.

401 refuses absent, expired or revoked credentials. 403 refuses missing scopes or authority. 402 requires Continuous. 409 reports lifecycle, identity or concurrent-work conflicts. 413 refuses oversized requests, 429 supplies `Retry-After: 60`, and 503 reports unavailable infrastructure or schema expansion. Do not interpret any failure as a passed gate.

## Gate semantics

Owner/Admin policy can set maximum unresolved requirement count, maximum vulnerability match count, maximum age (1-720 hours), whether all conformity gates must have recorded human approval, and whether a structurally usable supplied SBOM is mandatory. Defaults are zero unresolved requirements, zero matches, 24 hours, all human approvals required, and no mandatory supplied SBOM.

Unresolved means the complete requirement register's `partial`, `gap` and `needs_review` states. This is not the UI's manually dismissed task count. Matches count all OSV observations rather than treating a human affectedness assertion as removal. A finalized monitoring check for that same run updates the count and intelligence time; a degraded check can never establish absence.

Hard conditions cannot be switched off: the run must be terminal complete, the latest product attempt and usable baseline, uninvalidated, within the age limit, have complete recorded source coverage, and have known correlatable inventory and fresh complete OSV intelligence. Unknown parser coverage, native build incompleteness, unmatched inventory entries, partial coverage and `not_applicable` intelligence do not pass. A supplied inventory must be structurally usable and have no repository-only divergence to count as known, but the gate still does not validate completeness. Active or retryable source acquisition and queued/running assessment work prevent a pass. Disabling the human-approval check only excludes it from this engineering policy; it cannot grant approval or make unsigned drafts final.

All refusal reasons are returned. State precedence is `blocked`, `degraded`, `unknown`, then `pending`; `passed` is possible only when none applies. These are point-in-time evaluations. Record the run ID, source snapshot, input hash, policy version and evaluated time with your build. A later source change, policy change, approval change or intelligence update can change the verdict.

## Webhooks and event delivery

The private background Worker's existing one-minute cron reconciles durable facts. It drains unseen completed runs rather than checking only the latest run. Events start when pipeline configuration is first created; old project history is not backfilled. Gate outcomes are observed on API evaluation and background reconciliation; intermediate gate states between observations are not a decision history. The canonical approval history remains in ConformOps.

- `assessment.completed`: a run reached terminal completion.
- `gaps.changed`: a completed assessment changed recorded requirement or gap facts relative to its predecessor.
- `vulnerability_intelligence.changed`: new assessment intelligence or a monitoring check's complete/degraded outcome is available, including new, revised or withdrawn matches. This is an invalidation signal; it is not a claim that every check found a new vulnerability.
- `gate.evaluated`: the observed policy version, run, verdict or refusal reasons changed.

Payload allowlist: `apiVersion`, `id`, `sequence`, `type`, `projectId`, `runId`, `releaseId`, `state`, `occurredAt`. Never tokens, names, emails, source/evidence contents, paths, SBOM or advisory details. Fetch current detail with the appropriate scoped credential. Polling works without a webhook.

Operators configure `AUTOMATION_WEBHOOK_HOSTS` as a comma-separated list of explicitly reviewed public HTTPS destination hosts and `AUTOMATION_WEBHOOK_SIGNING_KEY` as a random secret of at least 32 characters on **both** web and background Workers. Use the same signing key within an environment and a different key across environments. No new Cloudflare resources or GitHub write permissions are required. A missing configuration disables webhook setup, while the UI explains that polling remains available. Review destination ownership and that a host is not a proxy into a private network before allowing it. URLs cannot carry userinfo, a query, fragment or nonstandard port; redirects are never followed. This deployment allowlist is the SSRF boundary.

The product can configure one HTTPS receiver. Saving the endpoint rotates its derived signing secret, shown once; configure the receiver before relying on deliveries. Removing or replacing an endpoint cancels its queued deliveries rather than sending old events to a new destination. Issuer authority, membership and coverage are rechecked before every outbound attempt. An in-flight request cannot be recalled by revocation.

Verify `ConformOps-Signature: t=<unix-seconds>,v1=<hex-hmac>` by calculating HMAC-SHA256 with the shown signing secret over `<t>.<exact raw request body>`. Compare equal-length signatures in constant time, reject timestamps more than five minutes from your clock, verify the expected product, and deduplicate `id` before acting. `ConformOps-Event-Id` repeats that ID. The payload is serialized once for signing and sending. Return 2xx only after durably accepting the event. Webhook order is not guaranteed; `sequence` supports ordering and feed recovery.

Delivery is at least once: failures retry with exponential delays starting at two minutes, capped at one day, for eight total attempts. A 60-second lease permits recovery after Worker interruption, and each attempt has a five-second network timeout. No response body or exception text is retained. Product settings show recent attempt counts, HTTP status, delivered/failed/cancelled state and a retry action for failed deliveries. Receivers must tolerate duplicates when a response or acknowledgement is lost. Poll `events` to recover older or missed notifications.

## Operations and rollout

Migration `0045_groovy_morbius.sql` adds credentials, configuration, idempotency records, events and delivery state. Deploy through the existing expansion/migration/strict pipeline; the expansion contract is schema 0044, and the new routes/tick refuse work until strict mode. Product/account erasure explicitly removes credentials, webhook destinations and event/delivery state. Completed evidence and human approval records retain their existing lineage rules.

Engine 3.5.0 and redaction policy secrets@2.1.0 recognize both ConformOps credential formats explicitly. Supplied-SBOM parser 1.3.0 accepts a UTF-8 BOM while preserving the retained bytes and supplied-file hash. Historical sanitized snapshots are not rewritten.

Maintainers edit `scripts/conformops.ts` and this reference, then run `npm run cli:release`. `npm run cli:check` verifies the standalone bundle, reference and checksums against their source. The bundle uses the locked Vite toolchain and permits only Node built-in imports. After publishing this release, use a new dated directory for changes so CI jobs can retain a reviewed version.

Run `npm run test:automation`, `npm run test:deployment`, `npm run test:schema`, `npm run test:erasure`, lint, TypeScript and the full test suite. Webhook delivery claims require matching signing-key/host configuration and a live receiver test in the chosen environment after deployment. A local test is not deployed-runtime proof.
