Prepare scoped GitOps adoption and tested immutable image releases
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e241-8285-7a63-8c0c-51c9cb824dc3
This commit is contained in:
parent
5d5ee84d70
commit
c12a8fbcfb
12 changed files with 2585 additions and 731 deletions
|
|
@ -2,6 +2,5 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.git/
|
.git/
|
||||||
tests/
|
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ on:
|
||||||
- "pyproject.toml"
|
- "pyproject.toml"
|
||||||
- "uv.lock"
|
- "uv.lock"
|
||||||
- "alembic.ini"
|
- "alembic.ini"
|
||||||
|
- "tests/**"
|
||||||
|
- "schemas/**"
|
||||||
|
- "k8s/**"
|
||||||
|
- ".dockerignore"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|
@ -33,18 +37,22 @@ jobs:
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
REF="${GITHUB_SHA:-main}"
|
REF="${GITHUB_SHA:?commit required}"
|
||||||
SHORT="${REF:0:7}"
|
test "${#REF}" -eq 40
|
||||||
mkdir -p buildctx "${HOME}/bin"
|
mkdir -p buildctx "${HOME}/bin"
|
||||||
wget -qO /tmp/repo.tar.gz \
|
wget -qO /tmp/repo.tar.gz \
|
||||||
"https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz"
|
"https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz"
|
||||||
tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1
|
tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1
|
||||||
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
|
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
|
||||||
| tar xz --strip-components=1 -C "${HOME}/bin" docker/docker
|
| tar xz --strip-components=1 -C "${HOME}/bin" docker/docker
|
||||||
export PATH="${HOME}/bin:${PATH}"
|
export PATH="${HOME}/bin:${PATH}"
|
||||||
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
|
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
|
||||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
docker build -t "${IMAGE}:latest" -t "${IMAGE}:main-${SHORT}" buildctx
|
# Check the exact source archive before publishing. Never update latest.
|
||||||
docker push "${IMAGE}:latest"
|
docker build --target test buildctx
|
||||||
docker push "${IMAGE}:main-${SHORT}"
|
docker build --label "org.opencontainers.image.revision=${REF}" -t "${IMAGE}:git-${REF}" buildctx
|
||||||
echo "pushed ${IMAGE}:latest and ${IMAGE}:main-${SHORT}"
|
docker push "${IMAGE}:git-${REF}"
|
||||||
|
docker inspect --format '{{index .RepoDigests 0}}' "${IMAGE}:git-${REF}" > image-digest.txt
|
||||||
|
grep -Eq '^forgejo.coulomb.social/coulomb/activity-core@sha256:[a-f0-9]{64}$' image-digest.txt
|
||||||
|
cat image-digest.txt
|
||||||
|
# Deployment promotion is separate and consumes the digest, never the tag.
|
||||||
|
|
|
||||||
16
Dockerfile
16
Dockerfile
|
|
@ -1,13 +1,23 @@
|
||||||
# Stage 1 — install Python deps
|
# Stage 1 — install Python deps
|
||||||
FROM python:3.12-slim AS builder
|
FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS builder
|
||||||
RUN pip install uv --no-cache-dir
|
RUN pip install uv==0.5.9 --no-cache-dir
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY pyproject.toml uv.lock ./
|
COPY pyproject.toml uv.lock ./
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
RUN uv sync --no-dev --frozen
|
RUN uv sync --no-dev --frozen
|
||||||
|
|
||||||
|
# Isolated CI checks; development dependencies do not enter the runtime image.
|
||||||
|
FROM builder AS test
|
||||||
|
COPY tests/ ./tests/
|
||||||
|
COPY Dockerfile ./Dockerfile
|
||||||
|
COPY .forgejo/workflows/image.yaml ./.forgejo/workflows/image.yaml
|
||||||
|
COPY schemas/ ./schemas/
|
||||||
|
COPY k8s/ ./k8s/
|
||||||
|
COPY scripts/render_gitops.py scripts/check_gitops_promotion.py ./scripts/
|
||||||
|
RUN uv sync --frozen --extra dev && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py
|
||||||
|
|
||||||
# Stage 2 — runtime image
|
# Stage 2 — runtime image
|
||||||
FROM python:3.12-slim AS runtime
|
FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/.venv /app/.venv
|
COPY --from=builder /app/.venv /app/.venv
|
||||||
COPY --from=builder /app/src /app/src
|
COPY --from=builder /app/src /app/src
|
||||||
|
|
|
||||||
21
docs/evidence/2026-09-27-gitops-preflight.json
Normal file
21
docs/evidence/2026-09-27-gitops-preflight.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"managed_resources": 9,
|
||||||
|
"server_dry_run": "passed",
|
||||||
|
"client_diff": "empty",
|
||||||
|
"spec_changes": 0,
|
||||||
|
"source": "live snapshot reconciled to canonical manifest; generated GitOps projection",
|
||||||
|
"excluded": [
|
||||||
|
"Jobs",
|
||||||
|
"Secrets",
|
||||||
|
"ESO custody",
|
||||||
|
"databases",
|
||||||
|
"Temporal",
|
||||||
|
"NATS",
|
||||||
|
"llm-connect",
|
||||||
|
"edge relay",
|
||||||
|
"storage",
|
||||||
|
"ingress"
|
||||||
|
],
|
||||||
|
"activation_authorization": "User requested implementation of ACTIVITY-WP-0041 on 2026-09-27",
|
||||||
|
"automation_enabled": false
|
||||||
|
}
|
||||||
66
docs/gitops-release.md
Normal file
66
docs/gitops-release.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Activity-core GitOps release lane
|
||||||
|
|
||||||
|
ACTIVITY-WP-0041 owns this lane; RPF-WP-0048 owns platform adoption. The founder
|
||||||
|
requested implementation on 2026-09-27. Adoption is authorized; the existing
|
||||||
|
24-hour healthy observation period and release-identity proof remain mandatory.
|
||||||
|
|
||||||
|
## Managed set and ownership
|
||||||
|
|
||||||
|
`k8s/gitops/kustomization.yaml` renders exactly nine resources: the API, worker
|
||||||
|
and event-router Deployments; API and worker-metrics Services; runtime config,
|
||||||
|
external definitions, report schemas and service-inventory ConfigMaps.
|
||||||
|
`scripts/render_gitops.py --check` verifies the generated projection against
|
||||||
|
`k8s/railiance/20-runtime.yaml`. Change that source and regenerate; after adoption,
|
||||||
|
never apply the mixed legacy directory directly. Migration/sync Jobs are deliberately
|
||||||
|
excluded. Sync definitions through the existing authenticated admin endpoint after
|
||||||
|
ConfigMap projection refresh; this is an explicit release verification step.
|
||||||
|
|
||||||
|
The baseline includes live backup wrapper mounts and the API's Temporal UI setting.
|
||||||
|
Dependencies stay with their existing owners: ESO/OpenBao secrets, verified backup
|
||||||
|
ConfigMap, storage, host-path contents, Temporal/NATS/databases, llm-connect, edge
|
||||||
|
relay, ingress/SSO and monitoring. ArgoCD must not prune or adopt them implicitly.
|
||||||
|
Namespace classification remains production-tier (platform target, no authoritative
|
||||||
|
rApp binding); MASON-WP-0006 is the mapping owner. Adoption does not invent a binding
|
||||||
|
or lower readiness requirements.
|
||||||
|
|
||||||
|
## Build and publication
|
||||||
|
|
||||||
|
The image workflow retrieves the full commit archive, builds the isolated test
|
||||||
|
stage, and publishes only `git-<full-sha>` plus its registry digest. Python's base
|
||||||
|
image digest, uv version and dependency lock are pinned. Deployments consume
|
||||||
|
`forgejo.coulomb.social/coulomb/activity-core@sha256:…`, independently for each
|
||||||
|
component. A commit tag is a lookup convenience, not an immutability guarantee.
|
||||||
|
Existing images are retained during metadata-only adoption; do not replace a
|
||||||
|
running component with newer code merely to complete adoption.
|
||||||
|
|
||||||
|
The pipeline uses its existing runner-held registry credentials. No credentials
|
||||||
|
are copied into Git or into the coding-agent session. Successful publication and
|
||||||
|
anonymous/cluster pull must be evidenced before the first digest promotion.
|
||||||
|
|
||||||
|
## Bounded routine authority — implementation boundary
|
||||||
|
|
||||||
|
Proposed executable scope `ACTIVITY-WP-0041-image-only-v1` is intentionally narrow:
|
||||||
|
only the three Deployment image fields and transition from Never to IfNotPresent
|
||||||
|
may change. No resource inventory, commands, mounts, environment, access, schema,
|
||||||
|
replicas, resources, migrations, definition behavior or budget changes are admitted.
|
||||||
|
Those changes require their owner review under the existing governance policy.
|
||||||
|
|
||||||
|
`scripts/check_gitops_promotion.py` refuses a non-digest image, widened diff,
|
||||||
|
missing independent review/checks, unadmitted identity, stale health evidence,
|
||||||
|
missing rollback revision, or less than 24 healthy hours. It is a **validator**,
|
||||||
|
not an authority issuer: evidence fields are not signatures. The release executor
|
||||||
|
must authenticate the CI/reviewer/health receipts and bind the exact candidate
|
||||||
|
revision before invoking it. A producer-supplied JSON file cannot grant access.
|
||||||
|
|
||||||
|
No unattended merge/deployment identity has been admitted by this change. Do not
|
||||||
|
mark this policy active or turn on automatic sync based only on these fixtures.
|
||||||
|
After identity proof and the observation period, enable only the activity-core
|
||||||
|
child's bounded promotion path; root-wide automation and destructive pruning stay
|
||||||
|
off. The existing root remains manually reconciled for unrelated applications.
|
||||||
|
|
||||||
|
A release must name the prior pinned revision before promotion, sync through
|
||||||
|
ArgoCD with pruning disabled, verify health and report-sink/schedule invariants,
|
||||||
|
and revert its source revision through ArgoCD on failure. Prove both successful
|
||||||
|
promotion and failed-health rollback before claiming unattended operation.
|
||||||
|
The 24-hour observation begins with the recorded successful adoption; a stateful
|
||||||
|
failure or unintended spec change invalidates that observation.
|
||||||
4
k8s/gitops/kustomization.yaml
Normal file
4
k8s/gitops/kustomization.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- runtime.yaml
|
||||||
1517
k8s/gitops/runtime.yaml
Normal file
1517
k8s/gitops/runtime.yaml
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
63
scripts/check_gitops_promotion.py
Normal file
63
scripts/check_gitops_promotion.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
"""Fail-closed admission check for a candidate image-only GitOps release.
|
||||||
|
|
||||||
|
This is a validator, not an authority issuer or a cluster/Git credential broker.
|
||||||
|
Evidence must be supplied by the separately admitted release identity.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import yaml
|
||||||
|
IMAGE=re.compile(r'forgejo\.coulomb\.social/coulomb/activity-core@sha256:[0-9a-f]{64}')
|
||||||
|
SHA=re.compile(r'[0-9a-f]{40}')
|
||||||
|
DEPLOYS={'actcore-api','actcore-worker','actcore-event-router'}
|
||||||
|
def require(condition,message):
|
||||||
|
if not condition: raise ValueError(message)
|
||||||
|
def indexed(docs):
|
||||||
|
out={}
|
||||||
|
for d in docs:
|
||||||
|
require(isinstance(d,dict),'invalid resource')
|
||||||
|
key=(d['kind'],d['metadata']['name'])
|
||||||
|
require(key not in out and d['metadata'].get('namespace')=='activity-core','duplicate or foreign resource')
|
||||||
|
out[key]=copy.deepcopy(d)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def validate(before,after,evidence,now=None):
|
||||||
|
now=now or datetime.now(timezone.utc)
|
||||||
|
require(evidence.get('schema_version')==1,'unknown evidence schema')
|
||||||
|
require(evidence.get('identity_admitted') is True,'release identity not admitted')
|
||||||
|
require(evidence.get('authority')=='ACTIVITY-WP-0041-image-only-v1','unknown authority')
|
||||||
|
require(evidence.get('review_result')=='pass' and evidence.get('checks')=='pass','checks/review not passed')
|
||||||
|
require(evidence.get('reviewer') and evidence.get('reviewer')!=evidence.get('producer'),'independent review required')
|
||||||
|
require(bool(SHA.fullmatch(evidence.get('candidate_commit',''))),'full candidate revision required')
|
||||||
|
require(bool(SHA.fullmatch(evidence.get('rollback_commit',''))),'rollback revision required')
|
||||||
|
require(evidence['candidate_commit']!=evidence['rollback_commit'],'rollback must name prior revision')
|
||||||
|
observed=datetime.fromisoformat(evidence['healthy_since'].replace('Z','+00:00'))
|
||||||
|
measured=datetime.fromisoformat(evidence['observed_at'].replace('Z','+00:00'))
|
||||||
|
require(observed.tzinfo is not None and measured.tzinfo is not None,'timestamps need timezone')
|
||||||
|
require(now-timedelta(minutes=5)<=measured<=now,'health evidence stale or future')
|
||||||
|
require(measured-observed>=timedelta(hours=24),'24-hour healthy observation period incomplete')
|
||||||
|
require(evidence.get('argo_synced') is True and evidence.get('argo_healthy') is True,'Argo not healthy/synced')
|
||||||
|
left,right=indexed(before),indexed(after)
|
||||||
|
require(left.keys()==right.keys(),'resource inventory change')
|
||||||
|
changed=[]
|
||||||
|
for key,a in left.items():
|
||||||
|
b=right[key]
|
||||||
|
if a==b: continue
|
||||||
|
require(key[0]=='Deployment' and key[1] in DEPLOYS,'only runtime images may change')
|
||||||
|
ac=a['spec']['template']['spec']['containers'];bc=b['spec']['template']['spec']['containers']
|
||||||
|
require(len(ac)==len(bc)==1,'container inventory change')
|
||||||
|
require(bool(IMAGE.fullmatch(bc[0].get('image',''))),'registry digest required')
|
||||||
|
require(bc[0].get('imagePullPolicy')=='IfNotPresent','pull policy must support registry recovery')
|
||||||
|
ac[0]['image']=bc[0]['image'];ac[0]['imagePullPolicy']=bc[0]['imagePullPolicy']
|
||||||
|
require(a==b,'non-image deployment change')
|
||||||
|
changed.append(key[1])
|
||||||
|
require(bool(changed),'no release change')
|
||||||
|
return {'admitted':True,'deployments':changed,'candidate_commit':evidence['candidate_commit'],'rollback_commit':evidence['rollback_commit']}
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
p=argparse.ArgumentParser();p.add_argument('before',type=Path);p.add_argument('after',type=Path);p.add_argument('evidence',type=Path);a=p.parse_args()
|
||||||
|
try: print(json.dumps(validate(list(yaml.safe_load_all(a.before.read_text())),list(yaml.safe_load_all(a.after.read_text())),json.loads(a.evidence.read_text()))))
|
||||||
|
except (ValueError,KeyError,TypeError) as e: raise SystemExit(f'refused: {e}')
|
||||||
30
scripts/render_gitops.py
Normal file
30
scripts/render_gitops.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""Render only the reviewed application resource set, excluding jobs and custody."""
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
|
MANAGED={
|
||||||
|
'ConfigMap': ['actcore-runtime-config','actcore-external-activity-definitions','actcore-report-schemas','actcore-ops-service-inventory'],
|
||||||
|
'Service': ['actcore-api','actcore-worker-metrics'],
|
||||||
|
'Deployment': ['actcore-api','actcore-worker','actcore-event-router'],
|
||||||
|
}
|
||||||
|
class Dumper(yaml.SafeDumper): pass
|
||||||
|
def strings(d,v): return d.represent_scalar('tag:yaml.org,2002:str',v,style='|' if '\n' in v else None)
|
||||||
|
Dumper.add_representer(str,strings)
|
||||||
|
def render():
|
||||||
|
docs=list(yaml.safe_load_all((ROOT/'k8s/railiance/20-runtime.yaml').read_text()))
|
||||||
|
selected=[]
|
||||||
|
for kind,names in MANAGED.items():
|
||||||
|
for name in names:
|
||||||
|
matches=[d for d in docs if d and d['kind']==kind and d['metadata']['name']==name]
|
||||||
|
if len(matches)!=1: raise ValueError(f'expected exactly one {kind}/{name}')
|
||||||
|
d=matches[0]
|
||||||
|
if d['metadata'].get('namespace')!='activity-core': raise ValueError('namespace outside grant')
|
||||||
|
selected.append(d)
|
||||||
|
return '# Generated by scripts/render_gitops.py; do not edit directly.\n'+'---\n'.join(yaml.dump(d,Dumper=Dumper,sort_keys=False) for d in selected)
|
||||||
|
if __name__=='__main__':
|
||||||
|
parser=argparse.ArgumentParser(); parser.add_argument('--check',action='store_true'); args=parser.parse_args()
|
||||||
|
path=ROOT/'k8s/gitops/runtime.yaml'; text=render()
|
||||||
|
if args.check:
|
||||||
|
if path.read_text()!=text: raise SystemExit('GitOps projection stale; run scripts/render_gitops.py')
|
||||||
|
else: path.write_text(text)
|
||||||
64
tests/test_gitops_release.py
Normal file
64
tests/test_gitops_release.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""Adoption and routine-promotion boundaries are checked without cluster access."""
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import copy
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
|
def load(name):
|
||||||
|
spec=importlib.util.spec_from_file_location(name,ROOT/'scripts'/f'{name}.py')
|
||||||
|
mod=importlib.util.module_from_spec(spec);spec.loader.exec_module(mod);return mod
|
||||||
|
|
||||||
|
def test_render_exact_reviewed_set_and_no_jobs_or_secrets():
|
||||||
|
render=load('render_gitops')
|
||||||
|
assert render.render()==(ROOT/'k8s/gitops/runtime.yaml').read_text()
|
||||||
|
docs=list(yaml.safe_load_all(render.render()))
|
||||||
|
assert len(docs)==9
|
||||||
|
assert {d['kind'] for d in docs}=={'Deployment','ConfigMap','Service'}
|
||||||
|
assert all(d['metadata']['namespace']=='activity-core' for d in docs)
|
||||||
|
worker=next(d for d in docs if d['metadata']['name']=='actcore-worker')
|
||||||
|
mounts=worker['spec']['template']['spec']['containers'][0]['volumeMounts']
|
||||||
|
assert any(m.get('subPath')=='entrypoint' and m['name']=='backup-verified' for m in mounts)
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_workflow_publishes_commit_tag_and_digest():
|
||||||
|
text=(ROOT/'.forgejo/workflows/image.yaml').read_text()
|
||||||
|
assert ':latest' not in text and ':git-${REF}' in text
|
||||||
|
assert '${REF}.tar.gz' in text and '--target test' in text
|
||||||
|
assert 'RepoDigests' in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_inputs_are_pinned():
|
||||||
|
text=(ROOT/'Dockerfile').read_text()
|
||||||
|
assert text.count('python:3.12-slim@sha256:')==2
|
||||||
|
assert 'uv==0.5.9' in text and '--frozen' in text
|
||||||
|
|
||||||
|
|
||||||
|
def promotion():
|
||||||
|
from datetime import datetime,timezone
|
||||||
|
before=list(yaml.safe_load_all((ROOT/'k8s/gitops/runtime.yaml').read_text()))
|
||||||
|
after=copy.deepcopy(before)
|
||||||
|
worker=next(d for d in after if d['metadata']['name']=='actcore-worker')
|
||||||
|
container=worker['spec']['template']['spec']['containers'][0]
|
||||||
|
container.update(image='forgejo.coulomb.social/coulomb/activity-core@sha256:'+'a'*64,imagePullPolicy='IfNotPresent')
|
||||||
|
evidence=dict(schema_version=1,identity_admitted=True,authority='ACTIVITY-WP-0041-image-only-v1',checks='pass',review_result='pass',reviewer='independent-ci',producer='build-ci',candidate_commit='a'*40,rollback_commit='b'*40,healthy_since='2026-09-26T00:00:00Z',observed_at='2026-09-27T01:00:00Z',argo_synced=True,argo_healthy=True)
|
||||||
|
return before,after,evidence,datetime(2026,9,27,1,tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
def test_admits_only_digest_release_after_soak():
|
||||||
|
assert load('check_gitops_promotion').validate(*promotion())['deployments']==['actcore-worker']
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('key,value',[('identity_admitted',False),('checks','fail'),('reviewer','build-ci'),('healthy_since','2026-09-27T00:00:00Z'),('observed_at','2026-09-26T01:00:00Z'),('argo_healthy',False),('rollback_commit','not-a-sha')])
|
||||||
|
def test_refuses_missing_release_guards(key,value):
|
||||||
|
before,after,evidence,now=promotion();evidence[key]=value
|
||||||
|
with pytest.raises(ValueError):load('check_gitops_promotion').validate(before,after,evidence,now)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('change',['command','resources','tag','inventory'])
|
||||||
|
def test_refuses_authority_expansion(change):
|
||||||
|
before,after,evidence,now=promotion()
|
||||||
|
worker=next(d for d in after if d['metadata']['name']=='actcore-worker')
|
||||||
|
c=worker['spec']['template']['spec']['containers'][0]
|
||||||
|
if change=='command':c['command']=['sh']
|
||||||
|
elif change=='resources':c['resources']={'requests':{'cpu':'2'}}
|
||||||
|
elif change=='tag':c['image']='forgejo.coulomb.social/coulomb/activity-core:latest'
|
||||||
|
else:after.pop()
|
||||||
|
with pytest.raises(ValueError):load('check_gitops_promotion').validate(before,after,evidence,now)
|
||||||
|
|
@ -4,7 +4,7 @@ type: workplan
|
||||||
title: "Remove recurring deployment exceptions through governed GitOps adoption"
|
title: "Remove recurring deployment exceptions through governed GitOps adoption"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: activity-core
|
repo: activity-core
|
||||||
status: ready
|
status: active
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: activity-core
|
topic_slug: activity-core
|
||||||
created: "2026-09-27"
|
created: "2026-09-27"
|
||||||
|
|
@ -22,7 +22,7 @@ e5dd02cb-b5b2-45c8-a600-748ea2ffd31b and does not change estate policy.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: ACTIVITY-WP-0041-T01
|
id: ACTIVITY-WP-0041-T01
|
||||||
status: todo
|
status: progress
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "fadce2e0-67c8-5dab-98cc-3a08d466a39f"
|
state_hub_task_id: "fadce2e0-67c8-5dab-98cc-3a08d466a39f"
|
||||||
```
|
```
|
||||||
|
|
@ -44,7 +44,7 @@ server-dry-run the proposed managed set; no unintended spec change or pruning.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: ACTIVITY-WP-0041-T02
|
id: ACTIVITY-WP-0041-T02
|
||||||
status: wait
|
status: progress
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "af09865d-8cc9-5571-9c34-20ae679a1e4e"
|
state_hub_task_id: "af09865d-8cc9-5571-9c34-20ae679a1e4e"
|
||||||
```
|
```
|
||||||
|
|
@ -94,3 +94,18 @@ kubectl apply or a fresh founder exception. Failure recovers through the declare
|
||||||
rollback path. Human involvement is limited to the agreed monthly policy review.
|
rollback path. Human involvement is limited to the agreed monthly policy review.
|
||||||
GLAS-WP-0012/HFACT-WP-0001 readiness remains a separate prerequisite for automated
|
GLAS-WP-0012/HFACT-WP-0001 readiness remains a separate prerequisite for automated
|
||||||
pattern editing; completing this plan does not imply that executor is admitted.
|
pattern editing; completing this plan does not imply that executor is admitted.
|
||||||
|
|
||||||
|
## Implementation authorization and scope — 2026-09-27
|
||||||
|
|
||||||
|
The founder explicitly requested implementation of these actionable tasks. This
|
||||||
|
supersedes the earlier wait for adoption authorization; it does not waive the
|
||||||
|
24-hour healthy observation period or invent an unattended release credential.
|
||||||
|
RPF-WP-0048 owns the scoped AppProject/child adoption. Nine runtime resources are
|
||||||
|
reconciled to live state with an empty client diff and successful server dry-run;
|
||||||
|
Jobs/custody/infrastructure are excluded. See docs/gitops-release.md.
|
||||||
|
|
||||||
|
Image CI now tests before publishing full-commit tags/digests with pinned build
|
||||||
|
inputs. Local container tests passed. Registry publication/readback must still be
|
||||||
|
observed. The image-only promotion validator has negative fixtures for widened
|
||||||
|
authority, stale/missing evidence, non-digest references and incomplete soak.
|
||||||
|
It is not a credential issuer or proof of an admitted unattended executor.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue