Skip to content
Back to skills

Hcn Citr Context

ASecurity

The CITR control loop as it lives in THIS repo — the NNN-<class>-<slug> numbering scheme, the Chewie call path (controllers acquire via 859 for SDPT/SDLT and hand 831/833 an allocation-id read back via 863; MDLT's 835 still acquires its own; 103's remote target acquires too), the release path through 225, the MDLT kickoff/monitor/publish/tag set (203/204/205/206/224), the wire contract fields HCN parses out of a Chewie allocation response, and the MATS/XTS/promotion surface — where XTS (226),...

  • 410 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
devopsrustgobashnodeexpresskubernetesgitapisecurityperformance

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add hiero-ledger/hiero-consensus-node --skill hcn-citr-context --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hcn Citr Context?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hcn Citr Context
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hiero-ledger-hcn-citr-context/badge)](https://www.skillsdirectory.com/skills/hiero-ledger-hcn-citr-context)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: hcn-citr-context
description: The CITR control loop as it lives in THIS repo — the NNN-<class>-<slug> numbering scheme, the Chewie call path (controllers acquire via 859 for SDPT/SDLT and hand 831/833 an allocation-id read back via 863; MDLT's 835 still acquires its own; 103's remote target acquires too), the release path through 225, the MDLT kickoff/monitor/publish/tag set (203/204/205/206/224), the wire contract fields HCN parses out of a Chewie allocation response, and the MATS/XTS/promotion surface — where XTS (226), the daily controllers (227) and the namespace prune (107) are dispatch workflows that report to Chewie, triggered today by the restored 900/901/903 crons (to be deprecated when Chewie owns workflow dispatching). Load this before touching any numbered workflow, the Chewie call chain, `.github/chewie.yaml`, or anything that assumes what a CITR workflow does.
---

# CITR in hiero-consensus-node

This is the native, local version of what used to be reconstructed from GitHub each time. It documents the CITR workflow set as it actually sits on `main`.

## Primary sources — read these first, they are maintained in-repo

Three files in this repo are the authoritative reference and are kept up to date by whoever changes the workflows. Prefer them over this skill wherever they disagree — this skill is a guide to reading them and the traps around them, not a replacement:

- **[`.github/workflows/docs/chewie.md`](/.github/workflows/docs/chewie.md)** — the Chewie integration: repository config keys, required secrets, per-test resource config, the allocation flow (split into an SDPT/SDLT path and an MDLT path), and a "Migration Notes" section listing what was removed.
- **[`.github/workflows/docs/workflow-manifest.md`](/.github/workflows/docs/workflow-manifest.md)** — the current-file ↔ deprecated-file ↔ deprecated-name table for every workflow, plus a "Numbering conventions" section (sub-blocks inside 800-899, where the next batch of numbers starts).
- **[`.github/workflows/docs/citr-test-config.md`](/.github/workflows/docs/citr-test-config.md)** — the suite catalogue (MATS, XTS, SDCT, SDPT, SDLT, MDLT, Shortgevity) and per-suite configuration.
- **[`.github/workflows/docs/required-chewie-changes.md`](/.github/workflows/docs/required-chewie-changes.md)** — the dispatch contract: every workflow Chewie dispatches (`226`, `227`, `107`, and planned `204`/`205`/`206`), when, with which inputs, what HCN reports back, and the Chewie-side changes this repo depends on.

Verified against `origin/main` at `8d98da626a` (2026-09-30) **plus** the changes on branch `27342-citr-workflows-must-be-triggerable-via-chewie` (226/227/107 dispatch workflows reporting to Chewie, triggered by the 900/901/903 crons until Chewie owns workflow dispatching; 103 on Chewie; pods scheduled from the Chewie 3.x allocation labels). If that branch hasn't merged on your checkout, §6 describes the future, not `main`. Re-verify anything below against current `.github/workflows/` before trusting it — this surface moves fast.

Note: CITR support scripts live under **`.github/workflows/support/`** (`support/chewie/`, `support/citr/`), not a repo-root `support/` directory. Paths below that say `support/...` are relative to `.github/workflows/`.

## 1. The numbering scheme

`NNN-<class>-<slug>.yaml`, `name: "NNN: [CLASS] Title"`. Classes: `user` (workflow_dispatch), `flow` (event entry point), `disp` (dispatch-driven controller), `call` (reusable `workflow_call` callee), `cron` (scheduled). Ranges per `workflow-manifest.md`: `0xx` user/dry-runs, `1xx` operational (including non-CITR `disp` controllers such as `106-disp-xts-optional-tests`), `2xx` CITR, `3xx` triggered/release, `4xx`–`5xx` reserved (empty), `6xx` test helpers, `7xx` AI helpers, `8xx` reusable callees, `9xx` crons.

Within `8xx` the manifest documents conventional sub-blocks: `800-809` MATS driver and shared blocks, `810-814` MATS-specific callees, `815-830` XTS driver/panels/callees, `831-835` CITR SDPT/SDLT/MDLT, `836-849` consumer-neutral per-suite leaves, `850-853` release, `854-864` utilities and Chewie. **The contiguous space below 850 is exhausted — the next batch of leaf workflows starts at 870.**

On the branch: 162 `.yaml` files directly under `.github/workflows/`, 108 numbered, 8 of those deprecated stubs once `302` and `900`–`904` are restored.

**Duplicate numbers still exist.** A deprecated workflow keeps its number, takes a `Deprecated:` prefix on `name:`, and is reduced to a `print-deprecated` stub — so a number is not unique on disk until you check which file is live:

| Number | Live file                                  | Deprecated file                                                                    |
|--------|--------------------------------------------|------------------------------------------------------------------------------------|
| 802    | `802-call-compile-and-spotless-check.yaml` | `802-extract-jdk-version.yaml` (superseded by `854-call-extract-jdk-version.yaml`) |
| 855    | `855-call-extract-citr-vars.yaml`          | `855-extract-citr-vars.yaml`                                                       |
| 857    | `857-call-workflow-unit-tests.yaml`        | `857-call-solo-ge044.yaml` (superseded by `856-call-solo-ge044.yaml`)              |

Lone tombstones with no live file at that number: `050` (→ `102`), `080` (→ `701`), `200` (→ `103`), `805`/`808` (the monolithic HAPI/Otter callees, replaced by the `810`/`811`/`827`/`828` panels over the `836`–`849` leaves). `302` and `900`–`904` are live again: they were deprecated earlier on the branch and restored, and `302`/`900`/`901`/`903` are to be deprecated when Chewie owns workflow dispatching. **Select workflows by filename, not by number**, and check `name:` for `Deprecated:` before trusting a match.

**The manifest drops deprecated workflows from its table** (only the old names of *live* workflows appear in its deprecated columns) — so a number missing from the manifest may still exist on disk as a stub.

**MQPT is gone, not deprecated** (`200`/`210`/`220`/`602`/`830` under the old numbering were deleted outright). **`207-user-release-chewie-allocation.yaml` no longer exists** — the release path is `225` (§2).

## 2. The Chewie call path

All SDPT, SDLT, and MDLT Kubernetes resources come from Chewie — scheduled and adhoc alike. There is no non-Chewie fallback. But **SDPT/SDLT and MDLT differ in who owns the allocation** (rationale: issue #27203):

### SDPT/SDLT — the controller acquires, the test workflow only reads

```
201/202 (adhoc) or 221/222 (scheduled)
  ├─ 862-call-get-chewie-properties     .github/chewie.yaml → default_duration/default_timeout/default_mdlt_length
  ├─ 858-call-get-chewie-jwt            identity key → JWT
  ├─ 861-call-get-test-config           support/chewie/<type>-config.json → CN/aux shape
  ├─ 860-call-validate-chewie-jwt
  ├─ acquire-kubernetes-resources → 859-call-create-chewie-request   POST + poll; emits allocation-id
  ├─ 831 / 833  (with: allocation-id — the only Chewie input)
  │    ├─ 858 + 860                     fresh JWT for the read
  │    └─ acquire-kubernetes-resources → 863-call-get-chewie-allocation   GET by id; same outputs 859 emits
  └─ release-chewie-allocation (221/222 only)  dispatches 225 on a passing run
```

`831`/`833` no longer accept `duration-minutes`/`chewie-request-timeout` and no longer call `859`.

### MDLT — 835 still acquires its own allocation

```
203 (adhoc) or 224 (Chewie-dispatched once SDPT+SDLT passed)  — 862; 224 also checks sdpt-pass/sdlt-pass tags; 203 doesn't, by design (a human picks what it runs on)
  └─ 835-call-multi-day-longevity-test   858 → 861(mdlt) → 860 → 859; outputs allocation-id
       smoke run (~3 min) → launches the production run (mdlt-length) on the cluster, then EXITS
then, out of band, keyed only by allocation-id:
  204-disp-mdlt-monitor           status (green/red/yellow/black) + prunes BN block data >59 min old
  205-disp-mdlt-publish-results   logs / optional FSTS_Insight PDFs
  206-disp-mdlt-tag-result        inputs build-tag + result(success|failure); signs mdlt-pass-/mdlt-fail-<build>
  225-disp-release-chewie-allocation   optional early teardown
```

### 103 (Solo Tests Adhoc) — opt-in remote target

`cluster-target` defaults to `kind` (an ephemeral cluster on the runner, no Chewie). `remote` is honored only for `jumpstart`, `wrb-streaming`, `e2e-block-stream-cutover` — that list lives **only** in `get-chewie-jwt`'s `if:`, and every other Chewie job (`861` with `test-type: solo-adhoc`, `862`, `860`, `859`) needs it, so all skip together on Kind runs. The three scenario jobs `need` `acquire-kubernetes-resources` and check results explicitly (a skipped need would otherwise skip them on Kind). On remote, the namespace, Teleport `kubernetes-cluster` (the allocation's `fqdn`), and `KUBE_CONTEXT=hashgraph.teleport.sh-<fqdn>` all come from `859`'s outputs; duration is a fixed 240 min. `release-chewie-allocation` dispatches `225` whenever an allocation id exists, **regardless of result**. On remote, each job's `Render Chewie Scheduling` step renders the allocation's groups into `RUNNER_TEMP` copies of the remote values files under `hedera-node/test-clients/scripts/solo/` (consensus on `cn`; haproxy, envoy, MinIO and the jumpstart mirror on `aux`) and points the scripts at them via `REMOTE_CLUSTER_NETWORK_VALUES`/`REMOTE_NETWORK_VALUES_TEMPLATE`/`REMOTE_MIRROR_VALUES_TEMPLATE`. The scenarios' `start_remote_toleration_patcher` (two copies: `remote-cluster-helpers.sh` and inline in `solo-wrb-jumpstart.sh`) pins every other non-consensus workload, block node included, to the `aux` group, and needs `AUX_LABELS`/`AUX_TOLERATIONS` in the environment. The rendered mirror values must stay identical to the patcher's patch, or the patcher rolls the mirror Deployments mid-`solo mirror node add`. A comment block above `get-chewie-jwt` documents how to make another scenario remote-capable.

`835`'s own result reflects only the **kickoff** (smoke passed, production launched). The multi-day verdict arrives via `206`, which per its header comment is meant to be dispatched by Chewie when the run completes; `204` says "short-term user-triggered; longer term Chewie dispatches it." Chewie dispatches `224` once SDPT and SDLT have both passed for the build (judged from its own suite results); `227` does not dispatch it. `204`/`205` resolve cluster/namespace from the allocation by calling `parse-chewie-allocation.sh` directly, not via `863`.

### Release — `225-disp-release-chewie-allocation.yaml`

`workflow_dispatch`, one required input `chewie-allocation-id`. Jobs: `858` (fresh JWT, from `CHEWIE_REPO_IDENTITY_KEY`) → `860` → `release-allocation` on `hl-cn-chewie-lin-sm`, which masks the JWT, rejects any id that isn't a positive integer, prints a summary via `parse-chewie-allocation.sh -f summary` (no `-g`/`-x` — groups are discovered from the response), then calls `support/chewie/release-chewie-allocation.sh` → `DELETE /api/v1/compute/allocation/:id` with `Authorization: Bearer <jwt>`.

Status handling in `release-chewie-allocation.sh`: `204` released; `410` already terminal → `::notice`, success no-op; `404` not found (also what an allocation owned by **another repo** looks like) → fail; anything else → fail.

Who calls it: `221`/`222` dispatch it via `step-security/workflow-dispatch` **only when the test result is `success`**, with `continue-on-error: true` — best effort. `103` dispatches it after every remote run, pass or fail. A fresh JWT is fetched inside `225` because the acquire-time JWT may be hours stale. **Adhoc `201`/`202` never self-release, and failed scheduled runs are not released either** — Chewie's reaper / `workflow_run.completed` webhook is the only release path for those. MDLT is released manually through `225` or at expiry.

### Authentication — the JWT is base64-encoded twice

`858-call-get-chewie-jwt.yaml` POSTs to `api/v1/auth/token`:

```bash
-H "Authorization: ${{ secrets.chewie-key }}"          # bare API key, NO "Bearer " prefix — token-exchange endpoint only
...
chewie_jwt_b64=$(printf '%s' "${chewie_jwt}" | base64 -w 0)
double_encoded_jwt=$(printf '%s' "${chewie_jwt_b64}" | base64 -w 0)
```

Outputs `jwt` (double-encoded) and `expiration`. Every consumer decodes both layers via `support/chewie/decode-b64-jwt.sh` (two sequential `base64 -d`, distinct errors per layer, written to **stderr** with `return 1`). **Decoding once yields base64, not a token.** Every call after the exchange uses `Authorization: Bearer ${CHEWIE_JWT}`. `parse-chewie-allocation.sh` and `release-chewie-allocation.sh` read the token from env `CHEWIE_JWT_B64` (kept out of the process list) and `::add-mask::` the decoded form, since masking is exact-string.

Secrets: repo secrets `CHEWIE_HOST` (includes the scheme) and `CHEWIE_REPO_IDENTITY_KEY`, passed into `831`/`833`/`835` as `chewie-host` / `chewie-id-key`, and used directly by the controllers, `103`, `225`, and `226`.

### The request body

`support/chewie/build-compute-request.sh`, driven from `859`'s inputs:

```
-d <duration-seconds> -q <cn-qty> -c <cn-cpu> -m <cn-memory-mb> -g <cn-group-name (default "cn-nodes")>
-a <aux-qty> -p <aux-cpu> -w <aux-memory-mb> -x <aux-group-name (default "aux-nodes")>
-i <run-id> -n <run-number> -t <run-attempt> -o <owner> -r <repo> -j <job> -e <request-timeout (0 → 3600)>
```

`859` takes `duration-minutes` and multiplies by 60 before passing `-d`. Any zero-valued required numeric option is rejected, so a legitimately-zero field can't be expressed.

Per-test-type shapes in `support/chewie/<type>-config.json`, read by `861` (valid `test-type`: `sdpt`, `sdlt`, `mdlt`, `solo-adhoc` — checked by a **substring** match, so a partial name like `sd` also passes):

| Type         | CN qty/CPU/mem (MB) | Aux qty/CPU/mem (MB) |
|--------------|---------------------|----------------------|
| `sdpt`       | 9 / 39 / 256000     | 1 / 39 / 256000      |
| `sdlt`       | 8 / 39 / 256000     | 1 / 39 / 256000      |
| `mdlt`       | 8 / 39 / 256000     | 1 / 39 / 256000      |
| `solo-adhoc` | 4 / 8 / 16384       | 1 / 8 / 16384        |

`solo-adhoc` (for `103`) is sized for functional runs, not perf; whether Chewie 2.11's matching accepts a request that small was not verified when it was added.

`support/chewie/` holds exactly: `apply-allocation-scheduling.sh` (§3), `build-compute-request.sh`, `decode-b64-jwt.sh`, `parse-chewie-allocation.sh`, `release-chewie-allocation.sh`, and the four `*-config.json`.

### Allocation durations actually requested

`chewie.md` says scheduled controllers "always use the `chewie.yaml` default." **They don't** — they compute it and then pass a hardcoded value to `859`/`835`:

| Controller | `duration-minutes` passed           | Source                                                                   |
|------------|-------------------------------------|--------------------------------------------------------------------------|
| `221`      | `1320` (22h)                        | hardcoded, "20 hours + 2 for post-mortem"                                |
| `222`      | `1080` (18h)                        | hardcoded, "16 hours + 2 for post-mortem"                                |
| `224`      | `8640` (6d)                         | hardcoded, 5-day run + log-review buffer                                 |
| `201`/`202`| input, else `default_duration / 60` | verified in `verify-citr-vars`                                           |
| `203`      | `fromJSON(inputs.duration-minutes)` | **raw input** (default `"8640"`) — bypasses its own computed fallback; a blanked input makes `fromJSON('')` fail |
| `103`      | `240` (4h)                          | hardcoded; covers the longest scenario job timeout (210 min)             |

The request timeout always comes from `default_timeout`. Chewie bounds duration to 3600–1209600 s and request timeout to 60–604800 s server-side.

## 3. Polling contract and response parsing

`859` accepts 200 or 202 on create, reads `.id` as the allocation id (fatal if missing/`null`), then polls `GET /api/v1/compute/allocation/:id` every 2 s. Only two statuses are branched on: `approved` breaks the loop; `pending` continues until `request-timeout` seconds elapse. **Everything else — `denied`, `cancelled`, `released`, `expired`, `expired_released`, or anything unrecognized — falls through to the same fatal `else`**, even though the comment above it still enumerates only six statuses (no `expired_released`; see `hcn-chewie-context`).

After approval, `859` no longer parses the poll response itself — it calls `support/chewie/parse-chewie-allocation.sh -g <cn> -x <aux> -f outputs`, which re-GETs the allocation (requires HTTP **200** exactly) and emits outputs. `863` is a thin wrapper around the same script, so `859` and `863` emit identical keys: `namespace`, `fqdn`, `cn-/aux-group-name`, `cn-/aux-quantity`, `cn-/aux-tolerations`, `cn-/aux-labels`, `request-expiration` (plus `allocation-id` from `859`). There is no network id, owner or role: Chewie 3.x publishes none of them.

By exact `jq` path in `parse-chewie-allocation.sh`:

- `.status`, `.namespace`, `.cluster_fqdn`, `.expires_at`
- `(.instances // [])[] | select(.group==$g) | .spec.quantity` — still nested under `.spec`
- `(.instances // [])[] | select(.group==$g) | .node_selectors` and `.tolerations` (compact JSON, passed through untouched; emitted as `<prefix>-labels` / `<prefix>-tolerations`). Chewie v3.9.0 renamed the selector map from `labels` to `node_selectors` and moved the descriptive set to `labels`, so reading `.labels` would put descriptive labels into a nodeSelector.

When the caller names its groups (`-g`/`-x`), a group that is missing or has no labels is fatal — that covers an unapproved allocation (no `instances`) and one approved before the 3.x upgrade (groups named by operator role). A response without `instances` is otherwise tolerated, so `225` can summarize an already-released allocation.

Without `-g`/`-x` (as `225` calls it), groups are discovered from `.instances[].group` and each group's own name becomes its output-key prefix instead of `cn`/`aux`.

**Pod scheduling.** Every pod spec in the CITR values templates (`support/citr/*.yaml`, 20 specs) and in `103`'s remote values files carries a `chewie-group: cn|aux` placeholder in its `nodeSelector`. `support/chewie/apply-allocation-scheduling.sh <files>` (env `CN_LABELS`/`CN_TOLERATIONS`/`AUX_LABELS`/`AUX_TOLERATIONS`, i.e. the callee outputs) replaces each placeholder with that group's labels, keeps other selector keys (the block node's `kubernetes.io/hostname`), and appends the group's tolerations. It refuses empty labels and any leftover placeholder, and installs mikefarah yq v4 if the runner lacks it. `831`/`833`/`835` call it in their `*-tests-start` jobs with `|| exit 1` (the steps run `set +e`), and pick the block-node host with `kubectl get nodes -l <cn labels>`. An unrendered placeholder matches no node, so it fails closed (Pending). Report paths, `version_run.txt` and step summaries identify a run by allocation id (`832`'s `allocation-id` input).

## 4. The `.github/chewie.yaml` duality

Two independent readers of the same file:

1. **Chewie's own server** generates `.github/chewie.yaml` at installation. Per `hcn-chewie-context`, nothing on its allocation path reads `default_duration`/`default_timeout` back.
2. **This repo's `862-call-get-chewie-properties.yaml`** reads it client-side with `yq`, and emits `request-duration`, `request-timeout`, and `mdlt-length`.

`862` fallbacks on a missing/`null` key (each also logs `::error::` but does not fail): `default_duration` → `3600`, `default_timeout` → `300`, `default_mdlt_length` → `432000`. A missing file is fatal. `default_mdlt_length` is an HCN-only key — Chewie's template doesn't render it.

Current `.github/chewie.yaml`: `branches: ["*"]`, `default_duration: 72000` (20h), `default_timeout: 3600` (1h), `default_mdlt_length: 432000` (5d, the production-run length — **not** an allocation lifetime), `workflows` commented out (no allowlist). The file's own comment says "keep `default_duration` long enough to outlive" the MDLT run, which 72000 s does not — MDLT only works because `224`/`203` pass 8640 minutes explicitly instead of the default.

## 5. Namespace cleanup

Chewie creates the namespace on approval and reclaims it on expiry (or on `225`). The cleanup work:

- **In-namespace pruning lives in `107-disp-clean-citr-namespaces.yaml`**, a faithful port of `main`'s `903` with `workflow_dispatch` only (no inputs) and a single-run concurrency group. **`903` is now a thin hourly cron that dispatches `107`** (to be deprecated when Chewie owns workflow dispatching); without one of them, long SDLT/MDLT allocations fill their disks. (`903` had been extended to Chewie namespaces on purpose in `1aa1d6c153`, #27064.) `204` separately prunes block-node data for MDLT.
- **`902` is the only thing that deletes leftover legacy `solo-*-nN` namespaces**, and only for the boxes enabled on its schedule.

- **`902-cron-auto-namespace-delete.yaml`** (daily `0 23 * * *`, unchanged from `main`) — the one cron that deleted namespaces, matching `(solo)-(sdpt|sdlt|mdlt)-n([1-9]|1[0-2])$`. Chewie's `chewie-<repo>-r<id>` namespaces can't match. On `schedule`, only `Dallas_n1`/`Dallas_n2` default to true; everything else needs manual dispatch with its box ticked. **Latent bug:** `Dallas_n10`–`n13` and `Chicago_n14` are wired from `github.event_inputs.*` (typo for `github.event.inputs`), so they're always empty — and `n13`/`n14` couldn't match the regex anyway.
- **`107` (as `main`'s `903` did, hourly)** — **does not delete namespaces.** Runs `support/citr/cronClean.sh` against every namespace matching `solo[\-].*[\-]n[0-9]|chewie` — **this now includes Chewie-allocated namespaces** — deleting stream files older than 59 minutes in each `network-node*` pod and running `mc rm --older-than 0d1h0s` against the `solo-streams`/`solo-backups` MinIO buckets.

Cluster access is via Teleport, not a kubeconfig: `teleport-actions/auth-k8s` (plus `auth`/`setup`) against `hashgraph.teleport.sh:443`, for `k8s.pft.dal.lat.ope.eng.hashgraph.io` / `k8s.pft.chi.lat.ope.eng.hashgraph.io`.

## 6. MATS, XTS, and promotion

| Concern   | Path                                                                                                                                                                                                                                             |
|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| MATS      | `600-flow-pull-request-checks` → `800-call-mats-tests` → `801`–`804`, `809`, `822`, `823`, plus panels `810` (HAPI → `836`–`843`) and `811` (Otter → `847` fast). `600` also runs `857-call-workflow-unit-tests` (gomplate template tests, `resolve-ref.sh` tests). Dry run `000`. |
| XTS       | **`900` (every 3 h) or Chewie dispatches `226-disp-citr-xts-controller`** (on `main`, `inputs.ref` = candidate SHA) → `815-call-xts-tests` (→ `802`, `806`, `807`, `817`–`821`, panels `827` HAPI → `844`–`846`, `828` Otter → `848` full / `849` chaos), plus `816` and a fire-and-forget dispatch of `106-disp-xts-optional-tests` → `824` → `825`/`826`. Dry run `001`. |
| Promotion | **`901`** (Tue–Sat 01:00: newest `xts-pass` → `build-NNNNN` from repo var `XTS_BUILD_PROMOTION_INDEX`), or later **Chewie's promotion engine** (every 15 min, a `passing_xts` commit, App-token tag, Slack report), **dispatches `227-disp-daily-controllers`** (on `main`, sole input `ref` = the build tag) → `221`, `222` (dispatched *on* the tag, no inputs), `223` (`ref` and `build-tag` = the tag). Chewie dispatches `224` separately, once SDPT and SDLT have both passed for the build (from its own suite results). |

The crons came back as triggers of the dispatch workflows, not as their own implementations (to be deprecated when Chewie owns workflow dispatching): `307` → `302` (tag `xts-candidate`) → `900` cron every 3 h, which dispatches `226` for the candidate unless it already carries an `xts-pass-*`/`build-*` tag, is off `main`, or has a `226` run queued or in progress (matched on `226`'s `run-name`, which includes the SHA) → `901` cron Tue–Sat 01:00 (newest `xts-pass` → `build-NNNNN`, delete all `xts-pass-*`, dispatch `227`). `903` dispatches `107` hourly. **Disable a cron when Chewie takes over its dispatch**; `901` and Chewie's promotion engine number builds from different counters and must never both run.

### 226 in detail

- **Input guard:** `ref` must match `^[0-9a-f]{40}$`. `855`/`854`/`815` check the candidate out at `fetch-depth: 1`, which can't resolve a short SHA, so anything else fails `fetch-xts-candidate` (reported as an *environment* failure).
- **Eligibility:** the only rule is `git tag --contains <sha>` matching `build-.{5}` (already in a build, on this commit or a descendant), plus the existing on-`main` check. A rejected candidate sets `xts-proceed=false` — the run is **not** cancelled — but `xts-tag-commit` is still emitted so it can be reported.
- **`tag-for-promotion` is required again.** It always tags `xts-pass-<epoch>` (no adhoc tags); `901` promotes the newest one and `900` skips commits that carry one, so a failed tag fails the run (it is back in `report-success`/`report-failure`). `901` deletes the `xts-pass-*` tags when it promotes.
- **`run-name` includes the candidate:** `226: [DISP] CITR XTS Controller (<sha>)` — `900` matches on it.
- **Reporting to Chewie:** `report-xts-result` calls `864` (below) with `suite-type: xts`, `if: always()` whenever a commit resolved. Disposition: `xts-proceed != true` → `not_run`; else `success`/`failure`/`cancelled` → `passed`/`failed`/`cancelled`.
- One run per candidate: `concurrency: xts-<sha>`.

### Suite-result reporting — `864-call-report-suite-result.yaml`

Every CITR suite reports to Chewie's `POST /api/v1/suites/results` through `864`. Inputs: `suite-type` (validated: `mats`/`xts`/`sdpt`/`sdlt`/`sdct`/`mdlt`, plus the AdHoc types `adhoc_sdpt`/`adhoc_sdlt`/`adhoc_sdct`), `disposition` (validated against Chewie's vocabulary: `running`/`waiting_for_cluster` non-terminal; `passed`/`failed`/`performance_issue`/`isolated_issue`/`cancelled`/`not_run` terminal), one of `commit` (full SHA) or `build-number` (`build-00404`/`00404`/`404` → sent as integer `404`), and optional `branch`.

- **Reporting is required, not best-effort.** Chewie is the source of record for suite results; a failed report (including a Chewie outage) fails the caller's run. Load-bearing git result tags have required tagging jobs: `221`/`222` `tag-*-result` (`224` checks `sdpt-pass-`/`sdlt-pass-`) and `226` `tag-for-promotion` (`xts-pass-*` drives `901`/`900`). They are back in `report-success`/`report-failure` and the Rootly service routing, so a failed tag fails the run and alerts; the Chewie suite result still carries the suite's own verdict. Only `206`'s `tag-mdlt-result` stays `continue-on-error` and secondary.
- **One job, token exchanged inline** (858's logic, bare key on `api/v1/auth/token`) rather than via `858`/`860`, so the token is always fresh at report time, even after a multi-hour suite.
- `start_time` comes from the GitHub API (`actions/runs/{run_id}/attempts/{attempt}` → `run_started_at` — `github.run_id` is the caller's run inside a reusable workflow), so **every caller must grant `actions: read`** or the call is rejected at startup. `end_time` is sent only for terminal dispositions.
- Callers map their own job results to a disposition; it differs per suite:

| Caller | Suite | Subject | Notes |
|--------|-------|---------|-------|
| `300` | `mats` | `github.sha` + `github.ref_name` | `running` when MATS starts (`report-mats-running` shares `mats-tests`' `needs`), then the result on `needs: mats-tests` + the running job — reported as soon as MATS finishes. PR MATS (`600`) is not reported. |
| `226` | `xts` | candidate SHA | `running` when XTS starts (`report-xts-running` shares `xts-execution`'s `needs` and `if`); then the result, see above |
| `221`/`222` | `sdpt`/`sdlt` | `verify-tag` build number (skipped if empty) | `running` when the test starts (shares its `needs`, so after the allocation is acquired); the result after `release-chewie-allocation`, from `831`/`833` `outputs.result` and job result |
| `201`/`202` | `adhoc_sdpt`/`adhoc_sdlt` | `verify-tag`'s resolved commit (`resolve-ref.sh`, checked out from `inputs.ref || github.ref`), plus `branch` when the ref is a branch and `build-number` when it is a `build-NNNNN` tag | AdHoc runs on any ref. `running` when the test starts (shares its `needs`); the result after the test, same mapping as `221`/`222`. Reported regardless of `disable-notifications`. Never a gate in Chewie (swirldslabs/chewie#882). `203` (AdHoc MDLT) does not report. |
| `224` | `mdlt` | build number | successful kickoff → **`running`** (835 only launches the multi-day run) |
| `204` | `mdlt` | `inputs.build-tag` | each status check: `running` / `passed` / `failed` / `cancelled`, from `monitor-mdlt`'s `state` output (a failed `verify-allocation` counts as `cancelled`) |
| `206` | `mdlt` | `inputs.build-tag` | the verdict input: `success` → `passed`, `failure` → `failed`, once `verify-build` succeeds — the authoritative MDLT result |

Each `report-*-running` job shares its test job's `needs`, so both start together; every terminal report job also `needs` the running job, so `running` always lands before the result, and the test jobs never wait on either report. MDLT reports many times per run; Chewie has to take the latest report per build (or commit), not count rows. `206` (Chewie-dispatched with the verdict) reports that verdict as `passed`/`failed` once `verify-build` succeeds — its run conclusion is not the verdict (a `failure` verdict still yields a green run), so the explicit report is the authoritative MDLT result. SDCT (`223`) reporting is a separate effort. **The endpoint exists only in Chewie 3.x (≥ v3.3.0); against 2.11 every report 404s and fails the run** — including MATS (`300`) on every push to `main`. So 3.x must be in production before this branch merges — and a release that actually schedules (carrying #817/#825, never `v3.8.0`), deployed in the order in `required-chewie-changes.md` item 9.

**Ref resolution.** The `verify-tag` job in `201`, `202`, `203`, `221`, `222` and `224` is one call to `support/citr/resolve-ref.sh -r <ref> -d <default branch> -l <SDPT|SDLT|MDLT>`, run in a checkout of the ref (env `GH_TOKEN`, `GITHUB_REPOSITORY`). It outputs `commit` (HEAD), `branch` (the ref when it is a branch, checked as a full `refs/heads/` ref; for a tag or SHA off the default branch, the newest `release/*` branch containing it via GitHub's compare API, so Chewie's reachability check passes), `build-tag-number` (only for an exact `build-NNNNN` tag) and `build-number` (the loose `build-(.{5})` match). Only `201`/`202` expose `commit`/`branch`/`build-tag-number` as job outputs. `857` runs `support/tests/test-resolve-ref.sh` against a scratch origin with a stubbed `gh`. `226`'s candidate branch lookup and `900`/`302`'s on-`main` checks are separate and use their own rules.

Result tags are the integration surface, not workflow outputs: `221`/`201` tag `sdpt-pass-<build>`/`sdpt-fail-<build>`, `222`/`202` tag `sdlt-pass-`/`sdlt-fail-`, `206` tags `mdlt-pass-`/`mdlt-fail-` (build tag regex `build-(.{5})`), GPG-signed via `step-security/ghaction-import-gpg`. `224` checks the `sdpt-pass-`/`sdlt-pass-` tags again (`verify-prerequisite-tags`, restored because Chewie doesn't dispatch yet and `221`/`222` still tag), so `221`/`222`'s tagging jobs are required again (a failed tag fails the run and alerts); `203` doesn't check, by design. Chewie will also gate MDLT on its own SDPT/SDLT results once it dispatches `224`. `223-disp-sdct-controller.yaml` has zero Chewie references — SDCT allocation still runs out of band.

## 7. Traps

- **Runner labels are per-repository.** Chewie helpers here run on `hl-cn-chewie-lin-sm`. Suite runners include `hl-cn-sdpt-lin-{sm,lg}`, `hl-cn-sdlt-lin-{sm,lg}`, `hl-cn-mdlt-lin-{sm,lg}`, `hl-cn-sdct-lin-sm`, `hl-cn-hapi-lin-{lg,xl}`, `hl-cn-hapi-bn-lin-xl`, `hl-cn-hapi-wraps-lin-lg`, `hl-cn-otter-{fast,full,chaos}-lin`, `hl-cn-default-lin-{ss,sm,md,lg}`, and more. A `runs-on` copied from Chewie's own repo (`swirldslabs-chewie-linux-medium`) won't schedule here, and neither will one copied between workflows without checking.
- **`jq -r` on a missing key yields the string `"null"`**, so a renamed response field surfaces as a corrupt value downstream, not a failed step (§3).
- **`863` does not check `status == approved` directly.** It fails only because a non-approved allocation has no `instances`, which trips the named-group guard (§3).
- **`859`/`863` declare the `chewie-token` secret as "The Chewie API key"** — it is actually the double-encoded JWT from `858`, not the identity key.
- **`decode-b64-jwt.sh` reports errors on stderr with a non-zero exit**, so callers' `[[ "${CHEWIE_JWT}" == Error* ]]` checks never fire; the step fails on the substitution under `bash -e` instead.
- **The scheduled controllers ignore `default_duration`** (§2 table), despite what `chewie.md` says.
- **The status vocabulary Chewie can return is seven values, not the six `859`'s comment lists.** See `hcn-chewie-context`.
- **Chewie's `docs/dev/citr_build_promotion.md` §4 ("Chewie does not dispatch… the tag is the handoff", `push: tags: ['build-*']`) is wrong** and being corrected — Chewie dispatches `226`/`227`. Don't redesign the controllers around tag triggers on its say-so.
- **`221`/`222` declare no `workflow_dispatch` inputs.** Dispatching them with any `inputs` is rejected ("Unexpected inputs provided"); they read the build number from `github.ref` (`build-(.{5})`), so they must be dispatched *on* the build tag.
- **The workflow-dispatch API only accepts a branch or tag as the ref a workflow runs on, never a SHA.** That's why Chewie dispatches `226` on `main` with the SHA as an input, and why `226` dispatches `106` on the candidate's branch.
- **`857-call-workflow-unit-tests` checks out `needs.fetch-xts-candidate.outputs.xts-tag-commit` without having that `needs`** — always empty, so it checks out the default ref. Harmless, but a leftover from 900.

## 8. When a workflow change here needs a check on the Chewie side

Load `hcn-chewie-context` before changing: the shape of `build-compute-request.sh`'s output (Chewie's request DTO), anything parsed in `parse-chewie-allocation.sh` (response DTO and status vocabulary), `release-chewie-allocation.sh`'s status-code handling (Chewie's DELETE contract), the JWT exchange in `858`/`860` (auth model), or anything `apply-allocation-scheduling.sh` renders (Chewie's per-group label/taint scheme).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.