Hydra tutorial series — Part 7: CI and deployment
Parts 5 and 6 ran Hydra on your own machine. This part zooms out to how Hydra runs unattended: flows that notice a labelled issue and start each stage, container images that GitHub Actions builds and pushes to GHCR, and brokered credentials with an owner named on every recorded stage. It lays out the three deployment models and marks honestly what is documented and what is infrastructure-only.
In part 5 you ran Hydra by hand: build the images, run a stage with dev-run.sh, label an issue. That is the right way to learn Hydra, but it is not how it runs day to day. In production nobody starts containers from a terminal. Instead a scheduled flow notices the labelled issue and has each stage run for it, with no one at the keyboard. This part explains that path, where the pre-built images fit in, and the three deployment models behind it.
Same pipeline, different trigger
The personas, the gates and the no-loop rule from part 2 are the same locally and in production. What differs is who pulls the trigger, where the work runs, and what gets recorded:
| Local (part 5) | CI / production (this part) | |
|---|---|---|
| Trigger | you run dev-run.sh or smoke-test.sh | the hydra-sequencer flow finds a queued label on its five-minute schedule |
| Where stages run | containers on your machine | the hermiq ExApp, as workload steps started by OpenRegister flows |
| Images | you docker build them | the workload steps name no image; the GHCR images (ghcr.io/conductionnl/hydra-*) serve the container paths: scripts on a host and the Kubernetes Jobs |
| Credentials | GH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN in your shell | brokered credential ids, separate ones for reading, pushing and the model; the secrets stay in the broker |
| Record | none | labels move, a PR is opened, and each stage is written to hydra.json |
So everything you learned about labels in parts 5 and 6 applies. You are just handing the steering wheel to the flows.
Trigger: flows, not Actions
Hydra's stage dispatch does not run in CI. It briefly did: hydra#448 (2026-07-31) ran the stage as an Actions job, hydra-stage.yml, and hydra#452 (2026-08-01) removed it again. (The older hydra-build.yml and hydra-review.yml had already been retired in May.) The people Hydra is built for have Nextcloud and LLM access on a Kubernetes environment someone else set up, and no forge account with Actions rights or cluster access of their own. So the stage now runs on what they already have: Nextcloud, OpenRegister flows and the hermiq ExApp, which runs each stage as claude -p in a checkout of the target repo. The flows live in hydra's flows/ folder:
hydra-sequenceris the pipeline. It runs every five minutes and does one unit of work per tick: it resumes a review stage that is in flight, or else takes the first open issue in theConductionNLorganisation labelledapplier:queued, thensecurity-review:queued, thencode-review:queued, thenbuild:queued. It searches the whole organisation in one call rather than walking the repos one by one, and claims a slot from a shared pool of three before it starts work.hydra-dispatchclaims a slot and runs one hermiq agent step for an issue it is handed. It does not search the forge and does not hand work to the sequencer.hydra-lock-reaperruns every ten minutes and clears stale per-issue locks.
Both hydra-sequencer and hydra-dispatch ship with enabled: false, so nothing runs until someone switches them on in the OpenRegister instance. And the flows check less than older docs claimed: no flow enforces a spec's depends_on list or keeps sibling specs from building at the same time.
What GitHub Actions still does is build the images, in .github/workflows/hydra-image.yml, on every push to main or development. Nothing else can build a container image, so that is the one CI job left.
The entry point is unchanged from part 1: a label on the issue (build:queued), not a person at a terminal. Same contract, different plumbing.
Images: the GitHub Container Registry
In a local run you build the images yourself (part 5). Wherever a stage runs as a container outside your laptop, that would waste minutes on every stage, so hydra-image.yml builds them once and pushes them to the GitHub Container Registry (GHCR): the hydra-nextcloud-test:stable32 base image first, then hydra-builder (which copies the Nextcloud tree out of that base), and hydra-reviewer and hydra-security alongside. The Kubernetes Jobs name these images directly. For the scripts on a host (manual-review.sh, the cron scripts), the naming is driven by two variables in secrets/.env:
# pull the published images; unset, the scripts use localhost/hydra and test (part 5)
HYDRA_IMAGE_PREFIX=ghcr.io/conductionnl/hydra
HYDRA_IMAGE_TAG=latest
dev-run.sh ignores both and always starts localhost/hydra-<stage>:test.
So the reviewer image resolves to ghcr.io/conductionnl/hydra-reviewer:latest, and the same for builder and security. (The applier image is not built by this workflow.) latest only moves when a release is published. Every push to main or development also publishes an immutable sha-<short> tag, and pinning HYDRA_IMAGE_TAG to one of those is how you freeze a known-good pipeline version instead of always tracking latest.
Credentials: a reference, not a token
Production Hydra never uses a shared org-admin token, and it never holds a forge token at all. Every issue, label, comment and pull-request call the flows make goes through one OpenConnector source, flows/hydra-forge.source.json. That source carries only a credentialRef: the id of a credential that lives in the broker (Doriath). The broker fills in the secret and sends the call, locked to api.github.com. What hydra stores is a uuid:
"authentication": {
"credentialRef": { "credentialId": "{brokeredGithubCredentialId}" }
}
When the reference cannot be resolved, the run is refused. Nothing falls back to another token. The workload steps work the same way: they carry separate credential ids for reading the repo, for pushing, and for the model, and the push is done by the pipeline, so the agent that writes the code never holds a forge credential.
The Claude side follows the personal-scope rule from part 5: a Claude Max or Pro subscription may serve its owner, not an organisation. Hydra's part in that is making every stage say whose credential ran it:
- The owner comes from the step that ran the stage. The hermiq workload step reports the stage's
owner, together withcredential_ownerandcredential_name. How hermiq resolves the owner is configured in hermiq, not in the hydra repo. - No owner, no record.
hydra-record-stagerefuses to record a stage without an owner, because "who ran this, on whose credential" cannot be recovered after the run.
hydra.json records those three fields on every stage entry. It never holds the token itself.
Until 2026 this section described secrets/credentials.json: a list of Claude tokens with automatic rotation on rate-limit, plus one git token per persona. The file, the token pool and the rotation were retired with the move to flows.
The three deployment models
Hydra's docs/deployment-models.md describes three ways to run it, in increasing operational weight.
Troubleshooting CI runs
Common CI-specific snags
Unattended dispatch is the hydra-sequencer flow, not an Actions workflow, so there is no workflow run to look for. Its definition ships with enabled: false: check that the flow is activated on the OpenRegister instance, then give it a few five-minute ticks. It does one unit of work per tick, review stages before builds, so a busy queue takes longer. The label has to be the bare build:queued; a prefixed label is never picked up.
This applies to the container paths (the scripts on a host, the Kubernetes Jobs); the flows' workload steps name no image. The tag is wrong or the image was never built. Confirm hydra-image.yml ran on GitHub Actions and pushed ghcr.io/conductionnl/hydra-<role> with the expected tag, and that the host has pull access to GHCR. A codeberg.org/conduction/hydra-* reference is stale configuration: no Hydra image was ever published there.
hydra-record-stage refused it as unattributed ("A stage result with no owner cannot be recorded"), or the record write failed. Neither stops the run: the labels and the PR are already there. For the first, check how the owner is resolved for that workload step in the hermiq configuration of your instance; the record flow only checks that one is present.
The credentialId in hydra-forge.source.json is instance-specific. When you apply the source, substitute the id of the credential in your own OpenRegister instance.
Test yourself
1. What actually changes between a local run (part 5) and a CI run, and what stays the same?
Hint
Think about the trigger and the runtime versus the pipeline itself.
Answer
What changes: the trigger (the scheduled hydra-sequencer flow finding the label instead of you starting a container), where stages run (hermiq workload steps instead of containers on your machine, so no image is pulled for them), where credentials live (brokered credential ids instead of variables in your shell), and what is recorded (labels, a PR and hydra.json entries, where a local run leaves no record).
What stays the same: the personas, the gate suite (whose run prints its own COVERAGE line rather than a fixed count), and the no-loop rule. The contract is one issue labelled build:queued.
2. Why does the forge source hold a credential id instead of a token?
Hint
Where can a secret leak from if it is never in hydra's files at all?
Answer
The secret never leaves the broker. Hydra's flows, config and logs hold only a uuid, so there is no token in them to leak. The broker refuses an authentication block that holds anything next to the reference, because a secret embedded beside the reference is exactly what the reference exists to prevent. And when the id cannot be resolved, the run is refused instead of falling back to some other token. A shared org-admin token stays explicitly forbidden.
3. Which deployment model no longer runs pipeline stages, and which one lacks a written operator runbook?
Hint
One lost its stage dispatch in August 2026. One runs on Kubernetes.
Answer
Model 2 (GitHub Actions) has not run stages since hydra#452 (2026-08-01); it only builds the images. Model 3 (Kubernetes and ArgoCD) has real manifests but no step-by-step operator runbook yet, so you read the manifests directly. Model 1 (local) is parts 5 and 6.
Done — the whole series
That is the full Hydra picture, from concept to production. Where to from here: