Package rmgr into the worker image and prepare owner-return/freshness rollout (memo 2, memo 3)
Install repo-manager e77f8b4 from its own uv.lock into an isolated venv in the builder stage (commit identity asserted via git), copy it into the runtime image and set ACTIVITY_CORE_RMGR_BIN. Add a pin test and the founder-executed rollout package (Argo CD manifest diff, PVC, token route blocker, trial runbooks). Nothing deployed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
This commit is contained in:
parent
b6222af2cf
commit
34d1dc575c
3 changed files with 564 additions and 0 deletions
538
docs/rollout-owner-return-and-freshness-worker.md
Normal file
538
docs/rollout-owner-return-and-freshness-worker.md
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
# Rollout package: owner-return pickup + bounded freshness in the worker
|
||||
|
||||
> **REQUIRES FOUNDER EXECUTION.** Prepared, not executed. The founder runs every
|
||||
> Argo CD, kubectl, secret and trigger step in an attended terminal. Coding
|
||||
> agents cannot run production deploys here. While preparing this package:
|
||||
> no Argo sync, no `kubectl apply/patch/create/exec/cp`, no secret read, no
|
||||
> automation trigger. Agents only ran read-only `kubectl get`, `helm list`,
|
||||
> `docker buildx imagetools inspect` and a local image build.
|
||||
|
||||
Prepared 2026-10-04 from activity-core `main` (`b6222af` + this package commit).
|
||||
Authorization: informed-decision batch `docs/batches/2026-10-04-state-hub-wp0094`,
|
||||
all four memos accepted 2026-10-04. This package covers:
|
||||
|
||||
- **memo 2** `owner-return-pickup-plan-b6222af`: plan approved, plus one
|
||||
operator-triggered trial for one public repository, first `submit: false`,
|
||||
then once `submit: true`.
|
||||
- **memo 3** `freshness-runner-deploy-trial-e77f8b4`: deploy repo-manager
|
||||
`e77f8b457647ff23697e45a74615edde5f42d39d` into the worker, then dry runs and
|
||||
at most three `--limit 3` apply fires.
|
||||
|
||||
It does **not** cover memo 4 (schedule enable / sweep retirement). Both
|
||||
definitions stay `enabled: false` throughout, and no schedule is created.
|
||||
|
||||
The shape follows `state-hub/docs/rollout-dependency-edge-receiving.md`.
|
||||
**Activity Core is not deployed by Helm.** `helm list -n activity-core` is
|
||||
empty. Activity Core is deployed by **Argo CD** with manual sync:
|
||||
application `argocd/activity-core`, source `k8s/gitops`, pinned `targetRevision`.
|
||||
That is why every "Helm values diff" below is a manifest diff in
|
||||
`k8s/railiance/20-runtime.yaml`. You regenerate it into `k8s/gitops/runtime.yaml`
|
||||
with `scripts/render_gitops.py`, and the deploy step is an Argo CD revision
|
||||
bump plus sync. See `docs/gitops-release.md`.
|
||||
|
||||
## 1. Live state (read-only, observed 2026-10-04)
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| Deployer | Argo CD app `activity-core`, `Synced/Healthy`, `targetRevision` **`ce9a8f8edf4ec162bae3aa087803bd57a1b33a66`**, last operation 2026-09-28T10:19:42Z, manual sync (no `automated`), options `ApplyOutOfSyncOnly, PruneLast, FailOnSharedResource` |
|
||||
| Managed set | 9 resources: Deployments `actcore-api`, `actcore-worker`, `actcore-event-router`; Services; ConfigMaps `actcore-runtime-config`, `actcore-external-activity-definitions`, `actcore-report-schemas`, `actcore-ops-service-inventory`. PVCs, ExternalSecrets, Temporal, NATS, DBs and the edge relay are **outside** Argo |
|
||||
| Worker | `deploy/actcore-worker` (rev 64), 1/1, image `activity-core@sha256:75e240ac…4d3886` (built locally 2026-09-28 12:08 CEST, no revision label, pinned since `eda44d9`), runs as uid/gid 1000 with fsGroup 1000, RollingUpdate 25%/25% |
|
||||
| API | `actcore-api`, image `@sha256:713bddad…3ccd` (built 2026-09-23, no revision label) |
|
||||
| Event router | `@sha256:cd4e924c…42f4`, not touched by this rollout |
|
||||
| Bounded operations | Temporal activity `execute_bounded_operation`, registered by `activity_core.worker` on `orchestrator-tq`. It runs in **`actcore-worker`** |
|
||||
| Ops-run queue | `OPS_RUN_QUEUE_ENABLED=true` (ConfigMap `actcore-runtime-config`), lease 900 s, max attempts 3 |
|
||||
| State Hub URL in worker | `STATE_HUB_URL=http://actcore-statehub-edge-relay:8000`. The relay forwards every GET upstream and caches only allowlisted GETs, so owner-return `/decisions/{id}` reads fail closed (unknown means stop) |
|
||||
| State Hub primary | `svc/state-hub` in ns `state-hub`, ClusterIP `10.43.68.154:8000`, revision 73 (memo 1 deployed). No NetworkPolicy exists in `state-hub` |
|
||||
| ESO | ClusterSecretStore `openbao-activity-core`. Five ExternalSecrets are synced (`actcore-forgejo-admin`, `actcore-issue-core-runtime`, …). None holds a read-only repo-manager Forgejo token |
|
||||
| Existing PVCs | `actcore-statehub-edge-outbox`, `actcore-working-memory` (`local-path`, RWO, 1Gi). No receipt or report volume |
|
||||
|
||||
The live worker and API images predate `5a90124`, so **neither operation is
|
||||
live**. Their exact source commit cannot be proven because they were built
|
||||
locally without a revision label. Promoting a CI image of `main` therefore
|
||||
ships every source change since then. `git log ce9a8f8..HEAD -- src migrations`
|
||||
lists 7 commits: `7b38f05`, `dcb5f7d`, `5a90124`, `c6df768`, `ba4ef00`,
|
||||
`b2c6c56`, `f89ebe9`. **No migration changes.**
|
||||
|
||||
## 2. Image
|
||||
|
||||
### How `rmgr` gets into the worker (decision, implemented in this commit)
|
||||
|
||||
`rmgr` is installed in the **builder stage** into its own virtualenv,
|
||||
`/opt/repo-manager/.venv`. It is built from **repo-manager's own `uv.lock`**
|
||||
at the reviewed commit, and the venv is copied into the runtime image.
|
||||
`ACTIVITY_CORE_RMGR_BIN=/opt/repo-manager/.venv/bin/rmgr` is set in the image,
|
||||
and the code-fixed argv in `context_resolvers/repo_manager.py` already honours it.
|
||||
|
||||
- The commit is pinned by `ARG REPO_MANAGER_COMMIT=e77f8b45…d39d`. The build
|
||||
fetches that SHA from `https://forgejo.coulomb.social/coulomb/repo-manager.git`
|
||||
(public, anonymous) and asserts `git rev-parse HEAD` equals it. Git's content
|
||||
addressing is the identity check.
|
||||
- `uv sync --no-dev --frozen --no-editable` installs from the lock, so the
|
||||
install is reproducible. The resulting venv does not need the source tree.
|
||||
- The build stage runs `rmgr freshness run --help`, so a broken install fails CI.
|
||||
- Git is installed only in the builder stage. The runtime image has no git,
|
||||
which was verified.
|
||||
- The dependency sets stay isolated. Activity Core's lock is unchanged, so
|
||||
there is no resolver coupling between the two projects.
|
||||
- `tests/test_gitops_release.py::test_repo_manager_cli_is_pinned_and_isolated`
|
||||
asserts the pin, the identity check, the venv copy and the env var.
|
||||
|
||||
Alternatives rejected:
|
||||
|
||||
- **Git or URL dependency in Activity Core's `pyproject`.** It couples the two
|
||||
locks and needs git in the builder anyway.
|
||||
- **Separate image or sidecar.** The operation calls a subprocess in the worker
|
||||
container, so a sidecar would need new code.
|
||||
|
||||
Local verification: `docker build --target runtime .` succeeded. In the
|
||||
container, `$ACTIVITY_CORE_RMGR_BIN freshness run --help` works and
|
||||
`build_freshness_argv({"limit":3,"max_seconds":240,"apply":False})` returns
|
||||
`['/opt/repo-manager/.venv/bin/rmgr','freshness','run','--limit','3','--max-seconds','240','--dry-run']`.
|
||||
The focused tests passed: `tests/test_gitops_release.py` and
|
||||
`tests/test_repo_manager_freshness_definition.py`, 34 tests.
|
||||
|
||||
### CI image
|
||||
|
||||
The image workflow (`.forgejo/workflows/image.yaml`) builds on pushes to `main`
|
||||
that touch `Dockerfile`, `src/**`, `tests/**` and similar paths. It runs the
|
||||
`test` stage first and publishes only `git-<full pushed head sha>` and its
|
||||
digest.
|
||||
|
||||
Existing CI images, verified read-only with `docker buildx imagetools inspect`:
|
||||
|
||||
| Tag | Digest | Has both ops | Has rmgr |
|
||||
| --- | --- | --- | --- |
|
||||
| `git-f89ebe9c821d8c984c1974b712c17a730a497ab4` | `sha256:ce7e585d46d8aa33a466f699f724102d8b54d914d8c042a9f1c26eba4db1c113` | yes | **no** |
|
||||
| `git-b6222af…` | none (docs-only push) | - | - |
|
||||
|
||||
**Required image:** the push of this package commit, made by
|
||||
`statehub fix-consistency --repo activity-core`, builds
|
||||
`git-<pushed head>`. That head may be a later `chore(consistency)` commit on
|
||||
top of it. Founder steps:
|
||||
|
||||
```bash
|
||||
cd ~/activity-core && git fetch && HEAD_SHA=$(git rev-parse origin/main)
|
||||
git merge-base --is-ancestor <package-commit> "$HEAD_SHA" && echo contains-package
|
||||
docker buildx imagetools inspect forgejo.coulomb.social/coulomb/activity-core:git-$HEAD_SHA
|
||||
# record DIGEST=sha256:... (deploy by digest only, never by tag)
|
||||
```
|
||||
|
||||
If no image exists for that head (for example, the build was skipped), run the
|
||||
`image.yaml` workflow with `workflow_dispatch` on `main` in Forgejo.
|
||||
|
||||
Retention: protect the alias for both the new digest and the rollback digests
|
||||
`75e240ac…` (worker) and `713bddad…` (API), as `gitops-release.md` requires.
|
||||
|
||||
## 3. Runtime config (manifest diff, NOT applied)
|
||||
|
||||
These changes are outside the `ACTIVITY-WP-0041-image-only-v1` routine
|
||||
authority because they touch env, mounts and a new volume. They are covered by
|
||||
memos 2 and 3 as an owner-reviewed change. `check_gitops_promotion.py` will
|
||||
correctly refuse them as "widened", so do not use the bounded promotion path.
|
||||
|
||||
### 3a. Objects outside Argo (founder applies with kubectl)
|
||||
|
||||
```yaml
|
||||
# PVC for freshness receipts + operator-supplied C-25 reports
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: actcore-ops-artefacts
|
||||
namespace: activity-core
|
||||
labels: {app.kubernetes.io/name: activity-core, app.kubernetes.io/part-of: activity-core}
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: local-path
|
||||
resources: {requests: {storage: 1Gi}}
|
||||
```
|
||||
|
||||
The Forgejo token is a **blocker**; see 3c. Once its route exists, add an
|
||||
ExternalSecret `actcore-repo-manager-forgejo-read` modelled on
|
||||
`k8s/railiance/15-externalsecret-forgejo-admin.yaml`. Use store
|
||||
`openbao-activity-core`, a **separate** target Secret, and one key
|
||||
`FORGEJO_READ_TOKEN`. It must not merge into `actcore-runtime-secret`, so the
|
||||
token is never in the worker's env.
|
||||
|
||||
### 3b. Worker Deployment diff (`k8s/railiance/20-runtime.yaml`, then `render_gitops.py`)
|
||||
|
||||
```diff
|
||||
# Deployment actcore-worker, container worker
|
||||
env:
|
||||
- name: ISSUE_SINK_TYPE
|
||||
value: state-hub
|
||||
- name: KUBECONFIG_R01
|
||||
value: /kube/config-hosteurope
|
||||
- name: KUBECONFIG_CORE
|
||||
value: /kube/config
|
||||
+ # repo-manager freshness runner (memo 3)
|
||||
+ - name: STATE_HUB_API_BASE
|
||||
+ value: http://state-hub.state-hub.svc.cluster.local:8000
|
||||
+ - name: RM_FRESHNESS_RECEIPTS_PATH
|
||||
+ value: /var/lib/activity-core/ops/freshness/receipts.jsonl
|
||||
+ - name: REPO_MANAGER_FORGEJO_TOKEN_FILE # add ONLY once 3c is resolved
|
||||
+ value: /var/run/secrets/repo-manager/FORGEJO_READ_TOKEN
|
||||
+ # ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED: deliberately absent (memo 2 plan-only first)
|
||||
- image: forgejo.coulomb.social/coulomb/activity-core@sha256:75e240ac23c1c6d7daae14691c2b4137d2d639a9af694aaf0d4c774a7e4d3886
|
||||
+ image: forgejo.coulomb.social/coulomb/activity-core@<DIGEST>
|
||||
volumeMounts:
|
||||
...
|
||||
- mountPath: /kube
|
||||
name: kubeconfigs
|
||||
readOnly: true
|
||||
+ - mountPath: /var/lib/activity-core/ops/freshness
|
||||
+ name: ops-artefacts
|
||||
+ subPath: freshness
|
||||
+ - mountPath: /etc/activity-core/owner-return
|
||||
+ name: ops-artefacts
|
||||
+ subPath: owner-return
|
||||
+ - mountPath: /var/run/secrets/repo-manager # only once 3c is resolved
|
||||
+ name: repo-manager-forgejo-read
|
||||
+ readOnly: true
|
||||
volumes:
|
||||
...
|
||||
+ - name: ops-artefacts
|
||||
+ persistentVolumeClaim:
|
||||
+ claimName: actcore-ops-artefacts
|
||||
+ - name: repo-manager-forgejo-read # only once 3c is resolved
|
||||
+ secret:
|
||||
+ secretName: actcore-repo-manager-forgejo-read
|
||||
+ defaultMode: 0440
|
||||
```
|
||||
|
||||
```diff
|
||||
# Deployment actcore-api, container api (the parser must know both operations for /admin/sync)
|
||||
- image: forgejo.coulomb.social/coulomb/activity-core@sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd
|
||||
+ image: forgejo.coulomb.social/coulomb/activity-core@<DIGEST>
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- **`STATE_HUB_API_BASE`.** Use the service DNS name rather than the edge relay.
|
||||
rmgr verifies `instance_role=primary` and label `railiance01`, and it must
|
||||
talk to the primary directly. `http://10.43.68.154:8000` is equivalent but
|
||||
breaks if the Service is recreated.
|
||||
- **Worker `STATE_HUB_URL`.** It stays on the edge relay, which is used by
|
||||
owner-return reads and existing callers. The relay passes GETs through and
|
||||
fails closed on upstream errors.
|
||||
- **Report file.** It is resolved as `activity-core://owner-return/<file>`,
|
||||
which maps to `/etc/activity-core/owner-return/<file>` because
|
||||
`ACTIVITY_CORE_ROOT=/etc/activity-core`.
|
||||
- **Receipt ownership.** The PVC subPaths are created by kubelet. The pod's
|
||||
fsGroup 1000 and local-path's 0777 dirs let uid 1000 write receipts. Check
|
||||
this in the post-checks.
|
||||
- **Definitions.** `owner-return-pickup` and `repo-manager-bounded-freshness`
|
||||
stay `enabled: false`, and the image carries them that way.
|
||||
|
||||
### 3c. Forgejo read token: route (recorded, no value handled)
|
||||
|
||||
`warden route find "forgejo read token for repo-manager freshness runner" --json`
|
||||
was run 2026-10-04. **No entry exists for the Activity Core worker or for
|
||||
repo-manager freshness.** The closest matches:
|
||||
|
||||
1. `state-hub-forge-derivation-read` (owner ops-mason, OpenBao + Forgejo,
|
||||
active, high risk). This is an organisation-wide repository-read token at
|
||||
`platform/workloads/state-hub/forge-derivation` field `FORGE_READ_TOKEN`.
|
||||
Its Kubernetes auth role binds **ServiceAccount `state-hub/state-hub`
|
||||
only**, so it is not routable to `activity-core`.
|
||||
2. `policy-nexus-forgejo-source-read` (railiance-platform), and
|
||||
`core-hub repo-manager publisher` (`platform/workloads/core-hub/forgejo-source-read`).
|
||||
Both are bound to other namespaces.
|
||||
|
||||
The required action is a credential change request in `railiance-platform`,
|
||||
following `CCR-2026-0014`. It must add a read-only org-wide repository-read
|
||||
token at a new path, for example
|
||||
`platform/workloads/activity-core/repo-manager-forgejo-read`, and grant the
|
||||
`openbao-activity-core` store role read on it. Then register a warden catalog
|
||||
route. The minting and the `paste_once_provision` delivery are founder steps.
|
||||
|
||||
**Interim option (founder choice):** deploy without the token lines, so Forge
|
||||
reads are anonymous. Dry runs will then show private repositories as forge
|
||||
fetch failures. Do **not** run apply fires in that mode: the private-repo
|
||||
refusals would fall under the packet's "unexplained refusal" stop condition.
|
||||
|
||||
## 4. Deploy (founder, attended)
|
||||
|
||||
### 0. Preconditions
|
||||
|
||||
```bash
|
||||
kubectl -n argocd get application activity-core -o jsonpath='{.spec.source.targetRevision} {.status.sync.status} {.status.health.status}{"\n"}'
|
||||
# expect ce9a8f8edf4ec162bae3aa087803bd57a1b33a66 Synced Healthy -> record PREV_REV
|
||||
kubectl -n activity-core get deploy actcore-worker actcore-api -o jsonpath='{range .items[*]}{.metadata.name}={.spec.template.spec.containers[0].image}{"\n"}{end}'
|
||||
# record PREV_WORKER_IMAGE (75e240ac…), PREV_API_IMAGE (713bddad…)
|
||||
kubectl -n activity-core get pods --field-selector=status.phase!=Running # stop on unrelated pending pods
|
||||
curl -s http://10.43.68.154:8000/state/health # primary answers
|
||||
```
|
||||
|
||||
### 1. Out-of-Argo objects
|
||||
|
||||
```bash
|
||||
kubectl apply -f actcore-ops-artefacts-pvc.yaml # 3a
|
||||
# (when 3c is resolved) kubectl apply -f actcore-repo-manager-forgejo-read-externalsecret.yaml
|
||||
kubectl -n activity-core get pvc actcore-ops-artefacts # Pending until first consumer (WaitForFirstConsumer) is OK
|
||||
# kubectl -n activity-core get externalsecret actcore-repo-manager-forgejo-read # SecretSynced/True
|
||||
```
|
||||
|
||||
### 2. Manifest commit (agent or founder, no deploy effect)
|
||||
|
||||
Apply diff 3b with `<DIGEST>`, then run:
|
||||
|
||||
```bash
|
||||
uv run python scripts/render_gitops.py # regenerate k8s/gitops/runtime.yaml
|
||||
uv run python scripts/render_gitops.py --check --verify-frontend --verify-platform
|
||||
git commit -am "Promote worker+api to <DIGEST>; rmgr receipts volume and env (memo 3)"
|
||||
statehub fix-consistency --repo activity-core # sanctioned push
|
||||
NEW_REV=$(git rev-parse origin/main)
|
||||
```
|
||||
|
||||
### 3. Server dry run (expected diff)
|
||||
|
||||
```bash
|
||||
argocd app diff activity-core --revision "$NEW_REV"
|
||||
# or without the argocd CLI:
|
||||
git -C ~/activity-core show "$NEW_REV":k8s/gitops/runtime.yaml > /tmp/ac-new.yaml
|
||||
kubectl diff -f /tmp/ac-new.yaml
|
||||
kubectl apply -f /tmp/ac-new.yaml --dry-run=server -o name
|
||||
```
|
||||
|
||||
Expected diff vs. live, and nothing else:
|
||||
|
||||
- `actcore-worker`: the image digest, 2–3 env entries, 2–3 volumeMounts and
|
||||
1–2 volumes.
|
||||
- `actcore-api`: the image digest.
|
||||
|
||||
The `ConfigMap`s and `actcore-event-router` show no change. Any other change
|
||||
stops the rollout. Note that `kubectl diff` may also show server-managed field
|
||||
noise; ignore that only if it is identical against `PREV_REV`.
|
||||
|
||||
### 4. Upgrade (the authorized action)
|
||||
|
||||
```bash
|
||||
kubectl -n argocd patch application activity-core --type merge \
|
||||
-p "{\"spec\":{\"source\":{\"targetRevision\":\"$NEW_REV\"}}}"
|
||||
argocd app sync activity-core --prune=false --timeout 300 \
|
||||
|| kubectl -n argocd patch application activity-core --type merge -p '{"operation":{"sync":{"prune":false,"revision":"'"$NEW_REV"'"}}}'
|
||||
kubectl -n activity-core rollout status deploy/actcore-worker --timeout=180s
|
||||
kubectl -n activity-core rollout status deploy/actcore-api --timeout=180s
|
||||
```
|
||||
|
||||
There is no `--atomic` equivalent. If either rollout does not become Ready
|
||||
within 180 s, roll back immediately; see step 7.
|
||||
|
||||
### 5. Post-checks
|
||||
|
||||
```bash
|
||||
kubectl -n argocd get application activity-core -o jsonpath='{.status.sync.revision} {.status.sync.status} {.status.health.status}{"\n"}' # NEW_REV Synced Healthy
|
||||
kubectl -n activity-core get pods -l app.kubernetes.io/name=actcore-worker -o jsonpath='{.items[0].status.containerStatuses[0].imageID}{"\n"}' # == DIGEST
|
||||
kubectl -n activity-core exec deploy/actcore-worker -- sh -c '$ACTIVITY_CORE_RMGR_BIN freshness run --help >/dev/null && echo rmgr-ok; env | grep -E "^(STATE_HUB_API_BASE|RM_FRESHNESS_RECEIPTS_PATH|REPO_MANAGER_FORGEJO_TOKEN_FILE|ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED)=" | sed "s/=.*TOKEN.*/=<set>/"; touch /var/lib/activity-core/ops/freshness/.w && rm /var/lib/activity-core/ops/freshness/.w && echo receipts-writable; ls -ld /etc/activity-core/owner-return'
|
||||
# expect: rmgr-ok; ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED absent; receipts-writable
|
||||
kubectl -n activity-core exec deploy/actcore-worker -- python -c "import urllib.request,json;print(json.load(urllib.request.urlopen('http://state-hub.state-hub.svc.cluster.local:8000/state/health')))"
|
||||
kubectl -n activity-core port-forward svc/actcore-api 8010:8010 &
|
||||
curl -s http://127.0.0.1:8010/health
|
||||
curl -s -X POST 'http://127.0.0.1:8010/admin/sync?definitions=true&schedules=true' # errors[] empty
|
||||
curl -s http://127.0.0.1:8010/ops/automations | python3 -c "import json,sys;[print(a.get('id'),a.get('name'),a.get('enabled')) for a in (json.load(sys.stdin).get('automations') or []) if a.get('id') in ('3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a','d7addc37-4cfe-550c-98e4-f54e4bd4e8ec')]"
|
||||
# both present, enabled False; no Temporal schedule active for either
|
||||
```
|
||||
|
||||
The definition ids are deterministic:
|
||||
|
||||
- `owner-return-pickup` is `3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a`.
|
||||
- `repo-manager-bounded-freshness` is `d7addc37-4cfe-550c-98e4-f54e4bd4e8ec`.
|
||||
|
||||
### 6. Evidence
|
||||
|
||||
Write `docs/evidence/2026-10-XX-owner-return-freshness-worker-rollout.json`
|
||||
in the shape of state-hub's `2026-10-04-edge-receiving-rollout.json`. Include:
|
||||
|
||||
- authorization: the memo ids and approval ids from the review store
|
||||
- `previous_revision`, `previous_images` and `revision`
|
||||
- image tag and digest
|
||||
- dry-run diff summary and post-checks
|
||||
- `executed_by`
|
||||
|
||||
### 7. Rollback
|
||||
|
||||
```bash
|
||||
kubectl -n argocd patch application activity-core --type merge \
|
||||
-p '{"spec":{"source":{"targetRevision":"ce9a8f8edf4ec162bae3aa087803bd57a1b33a66"}}}'
|
||||
argocd app sync activity-core --prune=false
|
||||
```
|
||||
|
||||
This restores digests `75e240ac…` and `713bddad…` and removes the env and
|
||||
mounts. Keep the PVC and its receipts; never delete them. Projections are
|
||||
idempotent per commit, so there is no data rollback. Open owner-return ops
|
||||
runs are closed through the normal ops-run close path, never deleted.
|
||||
|
||||
## 5. Trial runbooks
|
||||
|
||||
### How trial params reach the operation
|
||||
|
||||
`POST /ops/automations/{id}/trigger` accepts only `confirm_side_effect`. The
|
||||
parameters come from the synced definition row. Every trial parameter set (the
|
||||
report sha256, `submit`, `limit`, `apply`) is therefore a **reviewed definition
|
||||
change**. Make it as a ConfigMap-projected copy of the definition with the same
|
||||
`id`, in `actcore-external-activity-definitions`:
|
||||
`k8s/railiance/20-runtime.yaml` regenerated into `k8s/gitops`.
|
||||
|
||||
`/admin/sync` scans `/app/activity-definitions` (the image) **before**
|
||||
`ACTIVITY_DEFINITION_DIRS`. It upserts in that order inside one transaction
|
||||
(`sync_activity_definitions.sync`), so the projected copy deterministically
|
||||
overrides the baked one.
|
||||
|
||||
Each trial step is:
|
||||
|
||||
1. Commit the projection.
|
||||
2. Push with `fix-consistency`.
|
||||
3. Bump and sync the Argo revision (a ConfigMap-only diff).
|
||||
4. Wait about 60 s for the volume refresh.
|
||||
5. Run `/admin/sync?definitions=true&schedules=true`.
|
||||
6. Trigger.
|
||||
|
||||
Keep `enabled: false` in every copy. Manual trigger does not require
|
||||
`enabled: true`. Remove the copies after the trials, which restores the baked
|
||||
definitions.
|
||||
|
||||
### 5A. Memo 2: owner-return pickup, plan-only (`submit: false`)
|
||||
|
||||
1. **Choose one public repository.** Anonymous Forge reads must succeed, for
|
||||
example `git ls-remote https://forgejo.coulomb.social/coulomb/<repo>.git`
|
||||
without credentials. It should have at least one C-25 owner-return finding.
|
||||
`state-hub` is public and is the natural first target.
|
||||
2. **Produce the report on a clean checkout.** The checker must be state-hub
|
||||
`150cb0b` or later, and the run is read-only (no `--fix`):
|
||||
```bash
|
||||
cd ~/state-hub && git merge-base --is-ancestor 150cb0b HEAD
|
||||
cd ~/<repo> && git status --porcelain workplans/ | wc -l # must be 0
|
||||
git fetch && test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" # HEAD must be Forge default-branch HEAD
|
||||
cd ~/state-hub && python3 scripts/consistency_check.py --repo <repo> --json > /tmp/<repo>-c25.json
|
||||
python3 -c "import json;r=json.load(open('/tmp/<repo>-c25.json'));print(r.get('source_revision'),r.get('result'))" # full 40-hex, not error
|
||||
SHA256=$(sha256sum /tmp/<repo>-c25.json | cut -d' ' -f1)
|
||||
```
|
||||
Stop if `source_revision` is null (`source_revision_reason`), or if the text
|
||||
output has a C-25 "reply collection … is incomplete" finding.
|
||||
3. **Copy the file into the mount** (founder; this is a kubectl write):
|
||||
```bash
|
||||
kubectl -n activity-core cp /tmp/<repo>-c25.json "$(kubectl -n activity-core get pod -l app.kubernetes.io/name=actcore-worker -o name | cut -d/ -f2)":/etc/activity-core/owner-return/<repo>-c25.json
|
||||
kubectl -n activity-core exec deploy/actcore-worker -- sha256sum /etc/activity-core/owner-return/<repo>-c25.json # == $SHA256
|
||||
```
|
||||
4. **Project the definition copy** (`owner-return-pickup.md`, `enabled: false`)
|
||||
with this context source, following the steps above:
|
||||
```yaml
|
||||
context_sources:
|
||||
- type: state-hub-owner-return
|
||||
query: pickup
|
||||
operation: owner_return_pickup
|
||||
required: true
|
||||
bind_to: context.owner_return
|
||||
params:
|
||||
target_repo: <repo>
|
||||
report_file: activity-core://owner-return/<repo>-c25.json
|
||||
report_sha256: <SHA256>
|
||||
submit: false
|
||||
expected_source_revision: <source_revision>
|
||||
predecessor_repos: {"STATE-WP-": state-hub, "ACTIVITY-WP-": activity-core, "RMGR-WP-": repo-manager}
|
||||
```
|
||||
5. **Trigger** (founder, with SSO or an operator token held by the founder):
|
||||
```bash
|
||||
curl -sS -X POST http://127.0.0.1:8010/ops/automations/3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a/trigger \
|
||||
-H 'Content-Type: application/json' -H "X-Operator-Token: $OP_TOKEN" \
|
||||
-d '{"confirm_side_effect": true}'
|
||||
curl -s http://127.0.0.1:8010/ops/automations/3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a/runs | python3 -m json.tool
|
||||
```
|
||||
6. **Evidence.** The result is bound to `context.owner_return` and holds IDs and
|
||||
counts only:
|
||||
- `mode: plan-only`, report revision and `report_sha256`
|
||||
- `current_tasks` revision and count, and HEAD verified
|
||||
- planner dispositions and stop reasons
|
||||
- candidate IDs, with `submitted_count: 0`
|
||||
- `select count(*) from ops_runs where source_type='state_hub_owner_return'`
|
||||
unchanged
|
||||
|
||||
Store it as `docs/evidence/ACTIVITY-WP-0043-plan-only-<date>.md`, together
|
||||
with a hub progress event. **The founder reviews this evidence before 5B.**
|
||||
|
||||
### 5B. Memo 2: `submit: true` and duplicate re-run
|
||||
|
||||
1. Preconditions:
|
||||
- 5A was reviewed.
|
||||
- The repo HEAD is unchanged. If it changed, regenerate the report (step 2)
|
||||
and re-pin it.
|
||||
2. Set the gate. This is a worker env change through the same manifest path
|
||||
(3b plus Argo sync, which restarts the worker):
|
||||
`ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED=true`. The pod restart keeps the
|
||||
PVC report.
|
||||
3. Project the definition copy with `submit: true` and otherwise identical
|
||||
params. Sync and trigger as in 5A.5.
|
||||
4. Evidence: receipts with `triggering_event_id`, `idempotency_key`,
|
||||
`status=inserted`, `ops_run_id` and `state`. Also the new
|
||||
`state_hub_owner_return` rows.
|
||||
5. **Duplicate re-run.** Trigger once more with an unchanged definition and
|
||||
report. Expect every prior `inserted` to become `duplicate`, and the ops-run
|
||||
count for `state_hub_owner_return` to be unchanged.
|
||||
6. Unset the flag again (step 2 in reverse) unless the founder decides
|
||||
otherwise. Close the trial rows through the normal ops-run close path. Write
|
||||
`docs/evidence/ACTIVITY-WP-0043-submit-trial-<date>.md` and a hub progress
|
||||
event.
|
||||
|
||||
Stop and roll back (unset the flag) on any of these:
|
||||
|
||||
- a write outside the expected rows
|
||||
- a cleared wait, status or human gate
|
||||
- a message body in the evidence
|
||||
- an insert on the re-run
|
||||
|
||||
### 5C. Memo 3: freshness dry run, then up to three `--limit 3` apply fires
|
||||
|
||||
1. Project a `repo-manager-bounded-freshness.md` copy (`enabled: false`) with
|
||||
params `{limit: 3, max_seconds: 240, apply: false}`. Sync, then trigger
|
||||
`d7addc37-4cfe-550c-98e4-f54e4bd4e8ec` with `confirm_side_effect: true`.
|
||||
The resulting argv is
|
||||
`rmgr freshness run --limit 3 --max-seconds 240 --dry-run`.
|
||||
|
||||
Check:
|
||||
- the hub verified as primary `railiance01`
|
||||
- the planned repositories
|
||||
- no receipts written (the receipt-file line count is unchanged)
|
||||
2. Change the projection to `apply: true` (still `limit: 3`). Then fire at most
|
||||
**three** times, one at a time. After each fire, review:
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8010/ops/automations/d7addc37-4cfe-550c-98e4-f54e4bd4e8ec/runs | python3 -m json.tool # counts per outcome, exit_code
|
||||
kubectl -n activity-core exec deploy/actcore-worker -- sh -c 'wc -l < $RM_FRESHNESS_RECEIPTS_PATH; tail -n 3 $RM_FRESHNESS_RECEIPTS_PATH'
|
||||
kubectl -n activity-core exec deploy/actcore-worker -- /opt/repo-manager/.venv/bin/rmgr freshness receipts --latest
|
||||
```
|
||||
3. **Stop conditions** (from the packet). Stop firing and remove the projection
|
||||
on any of these:
|
||||
- any receipt from an unverified primary
|
||||
- any checkout read
|
||||
- a fire exceeding `max_seconds` twice
|
||||
- any refusal not explained by known holes:
|
||||
- RMGR-WP-0019-T03 duplicate projections
|
||||
- RMGR-WP-0014/0015 refusals
|
||||
- without the token (3c), private-repository fetch failures
|
||||
4. **Evidence per fire:**
|
||||
- fire id and Temporal workflow id
|
||||
- start, end and duration
|
||||
- outcome counts (`applied`, `noop`, `queued`, `failed`, `refused`,
|
||||
`skipped`) and exit code
|
||||
- receipt lines added, each with repo, Forge commit, outcome and hub
|
||||
`instance_role`/label
|
||||
- a refusal-reason classification
|
||||
|
||||
Store it under repo-manager `RMGR-WP-0020-T04` evidence and an Activity Core
|
||||
`ACTIVITY-WP-0044` note, with a hub progress event.
|
||||
5. **After the trials,** remove both projected copies, sync and run
|
||||
`/admin/sync`. The baked definitions (`enabled: false`) return. Schedules
|
||||
stay off until memo 4's own runbook.
|
||||
|
||||
## Blockers and open points
|
||||
|
||||
1. **Forgejo read token route missing** (3c). This needs a railiance-platform
|
||||
CCR and a warden catalog entry. Until then, run only dry runs, or apply
|
||||
fires restricted to public-only outcomes, at the founder's choice.
|
||||
2. **CI image.** It does not exist until this package is pushed; record the
|
||||
digest from the pushed head.
|
||||
3. **Image bump scope.** The live images have no revision label. The bump ships
|
||||
all source since roughly 2026-09-23 (API) and 2026-09-28 (worker). There are
|
||||
no migrations.
|
||||
4. **Per-trial definition projection.** Each param change needs a commit and an
|
||||
Argo sync, because the trigger takes no params. This is by design (the
|
||||
reviewed params pin the report digest) but costs one sync per step.
|
||||
Loading…
Add table
Add a link
Reference in a new issue