Pushing candidates from CI¶
Polling only sees the published ref. To show a decision record while it is still under review, for example on a merge-request branch, a CI job can push it to Adriano as a candidate. Reviewers can then accept or reject it before it is published. When the identical file later appears on the published ref, the next poll recognises it and the earlier decision carries over.
Push works for every source type, folders included. Any job or script that can send an HTTP request can push.
Pushes never change published records and never touch the approval history.
Setting up a registration for pushes¶
Allow pushes and name the token variables in the catalog file:
- id: central # ... other fields ... intake: [poll, push] push_tokens_env: [ADRIANO_PUSH_TOKEN_PLATFORM]
Generate a token and add it to the container environment (
ADRIANO_BACKEND_ENV_FILE,.envby default):echo "ADRIANO_PUSH_TOKEN_PLATFORM=$(openssl rand -hex 32)" >> .env
Recreate the container:
docker compose up -d adriano(Kubernetes: update the Secret, thenkubectl rollout restart deployment/adriano).Check with
adriano check-config: there must be no warningpush token env var ... is not set.Store the same token as a masked, protected CI variable in the projects (or group) that push, for example
ADRIANO_PUSH_TOKEN.
A push token is valid for its own registration only and gives no read access to the catalog.
Rotating a token¶
List two variable names, so old and new token are valid at the same time:
Add
ADRIANO_PUSH_TOKEN_PLATFORM_NEXTtopush_tokens_envand to the environment, then recreate the container.Switch the CI variable to the new token.
Remove the old name and its variable, then recreate the container again.
The push request¶
PUT /api/v1/candidates with the push token as bearer token and a
JSON body:
source_idrequiredThe registration
id.projectrequiredThe project path, as Adriano knows it (for GitLab, the full project path; for a folder source, the name of the subfolder below
root). It must be inside the registration’sscope.adr_uuidrequiredThe
uuidfrom the record’s frontmatter. It must match the content.refrequiredThe ref the content comes from, for example a branch. It may not be the registration’s
published_ref.contentrequiredThe complete Markdown file, at most 1 MiB.
pathoptionalThe file’s path inside the project, ending in
.md. Send it, so the candidate shows the right location and the title or date can be taken from the file name.delivery_idoptionalA unique ID of this CI run, such as the pipeline ID. A retried job with the same ID gets the earlier answer instead of a second submission.
authoroptionalThe author’s email address. Send it if the approval policy forbids self-approval: it is the only way Adriano can tell that a reviewer is the author.
PUT creates the candidate or replaces the previous content for
the same project, record and ref.
POST with the same body only creates, and answers
409 candidate_exists if the candidate is already there.
Example script¶
The source tree ships samples/push-candidate.sh, which builds the
JSON body correctly (including escaping) and sends it:
export ADRIANO_API_BASE_URL=https://adriano.example.com
export ADRIANO_PUSH_TOKEN_CENTRAL=<push token>
samples/push-candidate.sh \
doc/decisions/2026-01-05-use-redis-for-caching.md \
platform feature/redis-cache central "$CI_PIPELINE_ID" \
doc/decisions/2026-01-05-use-redis-for-caching.md
Its arguments are the file, project, ref, and optionally the
registration ID, delivery ID and path.
It reads the token from ADRIANO_PUSH_TOKEN_<REGISTRATION ID>
(upper case); a registration ID containing - or . therefore
needs your own script.
Example: GitLab CI job¶
The same request from a GitLab CI job. Other CI systems work the same way: send the same body, or call the example script above.
push-adr-candidate:
stage: review
rules:
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
changes: [doc/decisions/**/*.md]
script:
- |
python3 - <<'PY'
import json, os, pathlib, urllib.request
path = pathlib.Path("doc/decisions/2026-01-05-use-redis-for-caching.md")
body = json.dumps({
"source_id": "gitlab-platform",
"project": os.environ["CI_PROJECT_PATH"],
"adr_uuid": "b87a5fc5-a48f-49cb-a029-ac1f7c56c717",
"ref": os.environ["CI_COMMIT_REF_NAME"],
"content": path.read_text(encoding="utf-8"),
"path": str(path),
"delivery_id": os.environ["CI_PIPELINE_ID"],
"author": os.environ["GITLAB_USER_EMAIL"],
}).encode("utf-8")
req = urllib.request.Request(
"https://adriano.example.com/api/v1/candidates",
data=body,
method="PUT",
headers={
"Authorization": f"Bearer {os.environ['ADRIANO_PUSH_TOKEN']}",
"Content-Type": "application/json",
},
)
print(urllib.request.urlopen(req, timeout=10).read().decode())
PY
Pushing an erratum¶
An erratum (a correction to a published decision) is pushed the same
way.
Use the erratum file’s own uuid as adr_uuid, not the uuid of
the decision it corrects,
and send its real path (for example under errata/),
so the candidate sits where the erratum will be once published.
What the answer means¶
201A new candidate was created.
200The candidate already existed and was replaced or was unchanged; with
"deduplicated": true, thedelivery_idwas seen before and nothing was submitted again.
A successful answer looks like this:
{"record_id": "0b6c2f4e-...", "digest": "sha256-v1:a0b7e2...",
"created": true, "updated": false, "deduplicated": false,
"issues": [], "catalog_path": "/records/0b6c2f4e-..."}
catalog_path is the record’s page in the web UI.
issues lists problems in the file that do not block the push,
such as a broken link; reviewers see them too.
Errors carry a code and a hint:
401 not_authenticatedNo token was sent.
401 invalid_push_tokenThe token is not a push token of this registration, or the registration is unknown, disabled or has no token set.
403 push_disabledThe registration does not have
pushinintake.403 project_out_of_scopeThe project is not in the registration’s
scope.409 candidate_existsPOSTonly: the candidate already exists; usePUT.413 payload_too_large,413 content_too_largeThe request or the file is too large (file limit 1 MiB).
422 invalid_requestThe body is not valid JSON, misses a field or has an unknown one.
422 published_ref_not_allowedrefis the published ref; candidates come from other refs.422 invalid_documentThe file has no usable frontmatter or uuid; the answer lists the problems.
422 uuid_mismatchadr_uuiddiffers from the uuid in the file.422 bad_encodingThe content is not valid text.
Note
For a gitlab source with published_ref: "@project-default",
Adriano does not know
each project’s default branch when a push arrives,
so a push from the default branch is accepted as a candidate.
Limit the CI job to other branches, as in the example above.