Read-only copy of pvginkel/Ansible
docs/runbooks/argocd-app-move.md as of 80f07c4 (2026-10-17).
The source is authoritative: change it there.
Moving an app into a deploy monorepo #
The deploy-repo consolidation (argo-cd D11 as revised 2026-10-07) moves each app deployed from an
upstream Helm chart into a directory of PlatformAddOnsDeploy, and each app with a chart of its own
and no source repository of its own into a directory of HomelabAppsDeploy. This page moves one
app. It is the design's ten-step per-app move
(deploy-repo-consolidation.md,
§Per-app move) as slice 057's rulings D1 and D2 shape it. Slice 057 ran it for headlamp and
pgadmin, and slices 058 and 059 for the rest: every app of both classes has moved. What a moved
app then is: its registry entry in
argocd.md, its producer in
argocd.md.
In slices 057 to 059 every step was Claude's and the operator answered the stops. Slice 057's
ruling D1, which held for 058 and 059, gave Claude the whole move: the pushes, the state move, the
plan Job, the registry switch, the sync, the job deletion and the archive. It was the one exception
to argocd.md's operator keystroke, and it reached no other sync. It does
not reach a later move (an app reclassified, a dedicated app retired into a monorepo): who runs
which step of one is the operator's to rule when it comes, and until they have, the keystroke
holds. A stop halts
the move where the table below says and leaves what it says; the operator decides what follows. A
slice's run hands a stop back as its verdict question.
The stops #
| Stop | Fires when | The move is left |
|---|---|---|
| The render (step 1) | an object other than the hook's Job and RoleBinding and the ConfigMap tf-presync-revision differs between the two renders |
before the state rename: nothing has changed for Argo |
| Rule (1), the plan (step 4) | the plan at the new key fails after its presync: planning against line (one that exits before it is run again once, and stops only if that one fails too), or lists anything but github_repository_webhook.argocd[0]: destroy; for a stage whose old tfvars do not set manage_webhook = true, lists anything at all |
the rename reverted (before the registry switch) before the registry is touched |
| Rule (2), the diff (step 6) | the Application's diff after the switch shows anything but tf-presync-revision's data |
parked: the switch and the rename stay, autoSync stays off, nothing applies |
| A proof (steps 8, 10) | the producer's artifact or the model changes; the annotation proof fails | where it fails: an app that runs from the monorepo stays there |
Rule (2) parks rather than reverts: with the rename reverted, the switched Application's state
would sit at the old key while it applies at the new one. Rule (1) names the old repo's relay
webhook, which the old stage's Terraform holds when its tfvars set manage_webhook = true. A stage
whose old tfvars do not set it (keycloak's dev: its prd holds the webhook) has none to destroy, so
rule (1)'s second expectation is that its plan carries no changes. Wherever this page expects the
webhook's destroy, or on the way back its create, such a stage expects no change. grep manage_webhook /work/scratch/$OLD/config/$STAGE/terraform.tfvars says which a stage is.
Before a move #
The move needs slice 056's tooling (homelab-shared 0.6.0, the registry's path:, Architecture's
path: producers), an argocd-hook build with the plan mode (IaC/ArgoCDTools #38 on, as :latest),
and the monorepo with its AaC/<Monorepo> job and its hand-made relay webhook. Every command on
this page reads these, set once per move:
APP=headlamp STAGE=prd # the registry key, the stage
OLD=HeadlampDeploy MONO=PlatformAddOnsDeploy # the app's deploy repo, the monorepo it moves into
NS=$APP-$STAGE # hook.namespace: the Application's name and namespace
PROD=$APP-deploy JOB=$OLD # the producer, and the old job that publishes it
REC=/work/AnsibleSpecs/slices/<slice> # the move's record: renders, logs, the old job's config
KC="--kubeconfig $HOME/.kube/config-prd-write --context prd"
J="-u $JENKINS_USER:$JENKINS_TOKEN"
The clones, each brought to origin/main before its step: /work/scratch/$OLD,
/work/$MONO, /work/scratch/TerraformState (git clone https://github.com/pvginkel/TerraformState /work/scratch/TerraformState the first time),
/work/ArgoCDDeploy, /work/Architecture and /work/DockerImages. Each read's output goes into
$REC, named for what it is.
An app with two stages (keycloak's dev and prd) moves both at once, since its registry entry is
one. Step 1's prediction, steps 3, 4 and 6, the reads of step 7 and the opening read's state and
Application lines run for each stage, with STAGE and NS set to it, and step 5 sets
autoSync: false on each. A difference that only one stage's render shows stops the move all the
same, before the rename. The dev stage has a producer of its own, $APP-dev-deploy, which the old
repo's second job AaC/$OLD-dev publishes and AaC/$MONO archives beside $APP-deploy
(keycloak's keycloak-dev-deploy, from AaC/KeycloakDeploy-dev). The opening read's producer and
job lines, step 8, step 9's job lines and the rollback windows' producer steps run for each
producer, the second time with PROD=$APP-dev-deploy JOB=$OLD-dev.
Where a move stands #
A move opens with this read, and so does every return to it: after a timeout, an answered stop, a
review round. It resumes at the first step whose done when does not hold, so no production step
runs twice or is skipped. The render, step 2's wait, the plan and the diff are reads: a return runs
them again and never takes an earlier round's output. So is step 8's model compare, the A/B
sums of AaC/Architecture's builds: step 8 reads done once the re-point is pushed, before the
compare has run, so a return runs it again while the producer's old job is not deleted.
raw() { gh api "repos/pvginkel/$1/contents/$2" -H 'Accept: application/vnd.github.raw'; }
has() { gh api "repos/pvginkel/$1/contents/$2" --silent 2>/dev/null && echo "$1 $2: present" || echo "$1 $2: absent"; }
# step 2's wait: the DockerImages build running, whether the newest finished build that checked
# out DockerImages holds the pin push, whether $OLD's main has a commit the monorepo's lacks, and
# each values file the directory pins differently from $OLD
pinwait() {
{ date -u +%FT%TZ
git -C /work/DockerImages fetch -q origin
b=$(curl -s $J "$JENKINS_URL/job/DockerImages/api/json?tree=builds%5Bnumber,building,actions%5BlastBuiltRevision%5BSHA1%5D,remoteUrls%5D%5D%7B0,5%7D")
jq -r '.builds[] | select(.building) | "running: #\(.number)"' <<<"$b"
jq -r '[.builds[] | select(.building | not) | [.number, first(.actions[]
| select(.remoteUrls[0]? == "https://github.com/pvginkel/DockerImages.git") | .lastBuiltRevision.SHA1)]
| select(length == 2)][0] // [] | @tsv' <<<"$b" \
| { read -r n rev; git -C /work/DockerImages merge-base --is-ancestor $(cat $REC/pins-pushed-$APP.txt) "$rev" 2>/dev/null \
&& echo "pin push: built by #$n" || echo "pin push: not built"; }
git -C /work/$MONO fetch -q origin
git -C /work/$MONO fetch -q https://github.com/pvginkel/$OLD.git main
git -C /work/$MONO merge-base --is-ancestor FETCH_HEAD origin/main \
&& echo "$OLD main: in $MONO" || echo "$OLD main: ahead of $MONO"
for f in $(git -C /work/$MONO ls-tree -r --name-only FETCH_HEAD config | grep '/values.yaml$'); do
git -C /work/$MONO diff --quiet FETCH_HEAD:$f origin/main:$APP/$f || echo "$APP/$f: pins differ from $OLD"
done
} | tee -a $REC/pins-wait-$APP.log
}
# 1: the app's directory on the monorepo's main, and AaC/<Monorepo>'s last green build
has $MONO $APP
curl -s $J "$JENKINS_URL/job/AaC/job/$MONO/lastSuccessfulBuild/api/json?tree=number,artifacts%5BrelativePath%5D" | jq -c
# 2: deploy-pins.json entries that still name the old repo; once step 2 pushed, its wait, until
# any stage's state leaves its old key: from step 3's rename a copy to $OLD syncs an empty state
git -C /work/DockerImages fetch -q origin && git -C /work/DockerImages grep -l "\"pvginkel/$OLD\"" origin/main -- '*/deploy-pins.json'
[ -f $REC/pins-pushed-$APP.txt ] && { gh api repos/pvginkel/TerraformState/contents/argocd/$MONO/$APP --silent 2>/dev/null \
&& echo "pin wait: closed, a state is under argocd/$MONO/$APP" || pinwait; }
# 3: the state at each key, and any lock held on either
for key in argocd/$OLD/$STAGE argocd/$MONO/$APP/$STAGE; do
has TerraformState $key/terraform.tfstate
gh api repos/pvginkel/TerraformState/git/matching-refs/heads/locks/$key/ --jq '.[].ref'
done
# 5, 7: the registry entry on ArgoCDDeploy's main
raw ArgoCDDeploy releases/values.yaml | yq -c --arg app $APP '.apps[$app]'
# 5-7: the live Application, and the state key its hook's last run read
cexec iac kubectl get application -n argocd-prd $NS -o json | jq -c '{sync: .status.sync.status,
health: .status.health.status, revision: (.status.sync.revision // .status.sync.revisions),
repos: ([.spec.source // empty, (.spec.sources // [])[]] | map(.repoURL) | unique),
autoSync: (.spec.syncPolicy.automated != null)}'
cexec iac kubectl logs -n argocd-hooks job/tf-presync-$NS | grep -m1 'Getting state from' | sed 's/.*\/\///'
# 8: the producer's entry on Architecture's main
raw Architecture pipeline-producers.yaml | yq -c --arg id $PROD '.producers[] | select(.id == $id)'
# 9: the producer's old job (200 while it exists, 404 once deleted) and the old repo
curl -s -o /dev/null -w "AaC/$JOB: %{http_code}\n" $J "$JENKINS_URL/job/AaC/job/$JOB/api/json"
gh api repos/pvginkel/$OLD --jq '"archived: \(.archived)"'
| Step | Done when |
|---|---|
| 1. The directory | $MONO $APP: present |
| 2. The pins | no deploy-pins.json names pvginkel/$OLD. The wait, once step 2 pushed, is a read: run while no stage's state has left its old key (then the read prints pin wait: closed), and it holds at pin push: built by #<n> and $OLD main: in $MONO, with no pins differ line |
| 3. The state rename | the new key present, the old absent, no lock on either |
| 4. The plan | a read: run while step 5 is not done |
| 5. The registry switch | the entry names $MONO and path: $APP |
| 6. The diff | a read: run while the entry carries autoSync: false |
| 7. The sync | the entry has no autoSync; the Application reads $MONO, autoSync: true, Synced and Healthy; its hook's last run read argocd/$MONO/$APP/$STAGE/terraform.tfstate |
| 8. The producer | each producer's entry names pvginkel/$MONO, AaC/$MONO and path: $APP |
| 9. The old job and repo | AaC/$JOB: 404 for each producer, and archived: true |
| 10. The annotation proof | the pilots only: their slice's record says |
Before step 7, the hook line can wait about 20 seconds and print error: timed out waiting for the condition. That is a hook Job whose pod is gone, as on an app whose hook last ran days ago,
not a fault: step 7's sync runs the hook afresh, and the line then reads its state key.
The move #
1. The app's directory #
cd /work/$MONO && git fetch origin && git merge --ff-only origin/main
# git subtree needs GNU dirname: uutils coreutils' (Ubuntu 25.10, the KubeCoder dev container)
# prints `.` for `dirname $APP/.`, and the add fails with `invalid path './<file>'`.
mkdir -p /tmp/gnubin && ln -sf /usr/bin/gnudirname /tmp/gnubin/dirname
PATH=/tmp/gnubin:$PATH git subtree add --prefix=$APP https://github.com/pvginkel/$OLD.git main
The subtree brings $OLD's history under $APP/. Then one commit:
$APP/chart/Chart.yaml:homelab-sharedto0.6.0, the first version that hands the hookhook.path;cexec aac-tools chart-deps --repo $APP --updaterewrites$APP/chart/Chart.lock.$APP/config/$STAGE/terraform.tfvars:manage_webhook = falsewhere it wastrue. The monorepo's relay webhook is hand-made; the old repo's goes with the sync's apply (step 7).$APP/Jenkinsfile.architecture,$APP/.architecturerc,$APP/.kubecoder/and$APP/.gitignoredeleted: the monorepo's root files serve every app. A line of$APP/.gitignorethat the root's lacks joins the root's, so a test run leaves nothing untracked: an anchored line re-anchored under the app directories (/.cache/becomes/*/.cache/), an unanchored one as it is (__pycache__/, which matches at any depth)..kubecoder/project.yaml: a project$APP. A project's key is its folder and its statements' working directory, so the old repo'slintandteststatements carry over withhook.reponaming$MONOandhook.path=$APPbeside it. A test that sets the hook parameters itself (kyverno'stests/render-chart.py) takes the same two.$APP/README.mddescribes the directory, not$OLD: its title the app's key, the Application's sources under$APP/, the state keyargocd/$MONO/$APP/$STAGE/terraform.tfstate, no webhook of its own, and the producer published byAaC/$MONO. The comments that call$OLD"this repository" (terraform/webhook.tf,terraform/providers.tfand itsmanage_webhookdescription, the tfvars) say the same.
kc project lint and kc project test green from /work/$MONO (every project, $APP
among them), then commit.
The prediction, before the push: render both sides as their Applications do and compare them object by object.
OLDREV=$(git -C /work/scratch/$OLD rev-parse origin/main) # what the Application synced last
NEWREV=$(git -C /work/$MONO rev-parse HEAD) # the step-1 commit
mkdir -p $REC/render-$NS
cexec aac-tools chart-deps --repo /work/scratch/$OLD
cexec aac-tools chart-deps --repo /work/$MONO/$APP
cexec iac sh -c "cd /work/scratch/$OLD && helm template $NS chart --namespace $NS \
--set-string hook.repo=https://github.com/pvginkel/$OLD.git,hook.revision=$OLDREV,hook.stage=$STAGE,hook.namespace=$NS" \
> $REC/render-$NS/before.yaml
cexec iac sh -c "cd /work/$MONO/$APP && helm template $NS chart --namespace $NS \
--set-string hook.repo=https://github.com/pvginkel/$MONO.git,hook.revision=$NEWREV,hook.stage=$STAGE,hook.namespace=$NS,hook.path=$APP" \
> $REC/render-$NS/after.yaml
A local app's Application passes its stage values to the chart: add --values config/$STAGE/values.yaml to both helm templates. An upstream app's renders the upstream chart
beside the companion chart/, with the stage values; append it to each side, at the chart and
version its registry entry names:
for side in before:/work/scratch/$OLD after:/work/$MONO/$APP; do
cexec iac helm template $NS <chart> --repo <upstream repo> --version <version> --namespace $NS \
--values ${side#*:}/config/$STAGE/values.yaml >> $REC/render-$NS/${side%%:*}.yaml
done
Then the objects whose content differs, or which one side lacks:
objs() { yq -c 'select(. != null) | {k: "\(.kind)/\(.metadata.name)", v: .}' "$1" | sort; }
diff <(objs $REC/render-$NS/before.yaml) <(objs $REC/render-$NS/after.yaml) \
| grep '^[<>]' | cut -c3- | jq -r .k | sort -u | tee $REC/render-$NS/differs.txt
It lists ConfigMap/tf-presync-revision and Job/tf-presync-$NS, and RoleBinding/tf-presync
where the library bump changed it. The Job and
the RoleBinding are PreSync hooks, which Argo leaves out of its diff (Charts
charts/homelab-shared/templates/_tf-presync-hook.tpl). The ConfigMap is the one ordinary object,
its data.revision going from $OLDREV to the monorepo's SHA. So rule (2) expects that data
alone, however far the move jumps the library. Any other object is a stop: rule (2) would fire
after the switch, so the move stops here instead, before the state rename, and the pushed
directory deploys nothing.
Then git push origin HEAD:main: a slice's run commits on its phase branch, where git push origin main pushes the untouched local main and reports Everything up-to-date. The push
starts AaC/$MONO, which step 8 waits on.
2. The image pins #
Each DockerImages deploy-pins.json entry that names pvginkel/$OLD (the read above) takes
repo: pvginkel/$MONO and its own file under $APP/ (config/dev/values.yaml becomes
$APP/config/dev/values.yaml). Commit, then push and record the pushed commit, which the wait
reads:
git -C /work/DockerImages push origin HEAD:main && git -C /work/DockerImages rev-parse HEAD > $REC/pins-pushed-$APP.txt
The push rebuilds the app's images: DockerImages builds every image whose folder a push changed
(utils.hasChanges("${image}/.*")), deploy-pins.json included, and pins the new tag at every
entry of that image, in $APP/ and in any other repo an entry names, which rolls as it does on any
build (jenkins' mcp-filter also pins trello-mcp). From here every build of the app's images pins
into $APP/, the push's own build first. An app with no entry pushes nothing and skips the wait;
neither pilot had one.
The wait, before step 3. Two builds leave pins the move must take in first. A DockerImages
build that started before the push can still commit a pin to $OLD: the job sets
disableConcurrentBuilds() without abortPrevious, under a 180-minute timeout (DockerImages
Jenkinsfile, options), so the push queues behind a running build instead of ending it, and
that build pins from its own checkout (its Checkout stage feeds collectPins), the entries as
they were before the push. The build that checks out the push pins the rebuilt tags into $APP/
while the app runs $OLD's, which step 6's diff would show. Builds run one at a time, so once a
finished build has checked out the push, every build before it is done. Run the opening read's
pinwait until it prints pin push: built by #<n> and $OLD main: in $MONO, with no pins differ line; its log, $REC/pins-wait-$APP.log, is the record that the wait ended before the
rename.
-
pin push: not built: withrunning: #<n>, follow that build to its end,track_build.py DockerImages --buildnr <n>, which also waits for Argo CD to roll any pin it pushed; with no build running, the push's build is still queued. Then read again. -
$OLD main: ahead of $MONO: a commit reached$OLDafter step 1's subtree took it, the pin of a build that started before the push. Once the opening read's Application line shows the old ApplicationSyncedat$OLD's new head (its hook applied at the old key), bring the commit into the directory, in/work/$MONO:PATH=/tmp/gnubin:$PATH git subtree pull --prefix=$APP -m "$APP: $OLD's main since the subtree add" \ https://github.com/pvginkel/$OLD.git mainA line both sides pinned keeps the directory's tag, the later build's. Then step 1's prediction again,
/work/scratch/$OLDfetched first so thatOLDREVis that head, the push, and the read again. -
$APP/<file>: pins differ from $OLD, with$OLD main: in $MONO, before step 3: a build pinned rebuilt images into the directory, the push's own among them. The app takes them through$OLD, before the rename, as it takes any pin: each file the read names goes to$OLDas the directory has it, and the old Application syncs it at the old key. Once any stage's state has left its old key the read printspin wait: closedinstead and nothing goes to$OLD, keycloak's other stage included; step 6's diff shows a later rebuild's pins.cd /work/scratch/$OLD && git pull -q --ff-only git -C /work/$MONO show origin/main:$APP/config/$STAGE/values.yaml > config/$STAGE/values.yaml # each file the read names git commit -qam "$APP: the image pins DockerImages wrote into $MONO $APP/" && git push origin HEAD:main track_build.py --deploy $OLD main $(git rev-parse HEAD)The app rolls onto the rebuilt images. The read then shows
$OLD main: ahead of $MONO, the bullet above.
3. The state rename #
cd /work/scratch/TerraformState && git pull --ff-only
gh api repos/pvginkel/TerraformState/git/matching-refs/heads/locks/argocd/$OLD/$STAGE/ --jq '.[].ref'
mkdir -p argocd/$MONO/$APP && git mv argocd/$OLD/$STAGE argocd/$MONO/$APP/$STAGE
git commit -m "argocd/$OLD/$STAGE → argocd/$MONO/$APP/$STAGE: $APP moves into $MONO"
git push origin main
An app with a volume (a static-*-pv module) first records each PersistentVolume, for step 7's
compare, all of them in one go, one line each. A module's PV is <name>-pv, <name> being the
module's name; the loop names every module's (jenkins-prd's jenkins-pv build-cache-pv). The
line's last fields name what backs the PV: rootPath a CephFS subvolume (static-cephfs-pv, whose
handle is the PV's own name), local a ZFS dataset (static-zfs-pv, a local PV), handle an RBD
image (static-rbd-pv):
for pv in <name>-pv …; do
cexec iac kubectl get pv $pv -o json | jq -c '{pv: .metadata.name, uid: .metadata.uid,
created: .metadata.creationTimestamp, claim: .spec.claimRef.name,
rootPath: .spec.csi.volumeAttributes.rootPath, local: .spec.local.path,
handle: .spec.csi.volumeHandle}'
done > $REC/pv-$NS-before.json
The lock read prints nothing; a lock is a hook run in flight, so wait for it and read again. A
rejected push is a hook's state write that landed first: git pull --rebase, read the lock again,
push. From the push until step 5, nothing commits to $OLD (Why the order
holds).
4. The plan at the new key: rule (1) #
argocd.md's plan sheet, with
REPO=$MONO APP_PATH=$APP STAGE=$STAGE NS=$NS LOG=$REC/plan-$NS.log, at the head of the
monorepo's main. The verdict holds exactly one change:
presync: planning against argocd/<Monorepo>/<app>/<stage>/terraform.tfstate: nothing is applied
…
presync: the plan carries 1 change(s):
presync: github_repository_webhook.argocd[0]: destroy
presync: plan only: nothing was applied and no volume was reattached
The destroy is the old repo's relay webhook, which manage_webhook = false lets go. A stage whose
old tfvars do not set manage_webhook = true has no webhook in its state, and its verdict holds no
change at all, rule (1)'s second expectation:
presync: planning against argocd/<Monorepo>/<app>/<stage>/terraform.tfstate: nothing is applied
…
presync: the plan carries no changes
presync: plan only: nothing was applied and no volume was reattached
Either plan shows that the state is at the new key, where an empty state would plan a create of
every resource, and that its sops encryption does not bind the path. Anything else is rule (1)'s
stop: another change, the expected destroy missing, a plan that fails after reading the state.
Revert the rename (before the registry switch) before anything else,
then stop. A plan that exits before its presync: planning against line (in the clone, terraform init or a provider download) never read the state and is no verdict: keep its log under another
name and run the plan once more. Only if that one fails too is it rule (1)'s stop.
5. The registry switch #
ArgoCDDeploy's releases/values.yaml, the app's entry: repo: names
https://github.com/pvginkel/$MONO.git, path: $APP (the render test refuses any other
directory), and each stage takes autoSync: false. Everything else in the entry stays as it is:
an upstream app keeps its upstream: block, each stage its version, and the entry its
syncOptions. kc project lint and kc project test green
in /work/ArgoCDDeploy, commit, push. releases syncs the push on its own:
cexec iac kubectl get application -n argocd-prd releases -o jsonpath='{.status.sync.status} {.status.sync.revision}{"\n"}'
Once it reads Synced at the push, the Application reads the monorepo with no
syncPolicy.automated (the Application read above). Nothing syncs.
6. The diff: rule (2) #
Argo refreshes an Application whose spec changed. Once the Application read shows the monorepo's SHA as its revision:
cexec iac kubectl get application -n argocd-prd $NS \
-o jsonpath='{range .status.resources[?(@.status=="OutOfSync")]}{.kind}/{.name}{"\n"}{end}'
cexec iac argocd app diff $NS | tee $REC/diff-$NS.txt
The first lists ConfigMap/tf-presync-revision alone, and the diff, which exits 1 because it
shows one, changes that ConfigMap's data.revision from $OLDREV to the monorepo's SHA and nothing
else: what step 1's render predicted. Anything else parks the move (rule (2)): the switch and
the rename stay, autoSync stays off, nothing applies, and the operator decides. After the switch,
before the sync is the way back if that is the answer.
7. The sync #
The sync is autoSync turned back on: D1 merges the design's steps 6 and 7. A sync by a patched
.operation is untested for a full sync (Known behaviours). Drop each
stage's autoSync: false from the entry, test, commit, push. releases gives the Application its
syncPolicy.automated, and Argo syncs it, OutOfSync at a revision it has not synced. The hook Job,
tf-presync-$NS (tf-presync-<hook.namespace>), clones the monorepo at that SHA and applies
$APP/terraform/ at the new key.
cexec iac kubectl get application -n argocd-prd $NS \
-o jsonpath='{.status.operationState.phase} {.status.sync.status} {.status.health.status}{"\n"}'
cexec iac kubectl logs -n argocd-hooks job/tf-presync-$NS > $REC/sync-$NS.log
grep -m1 'Getting state from' $REC/sync-$NS.log; grep 'Resources:' $REC/sync-$NS.log
gh api repos/pvginkel/$OLD/hooks --jq '.[] | "\(.id) \(.config.url)"'
Succeeded Synced Healthy; the hook read …//argocd/$MONO/$APP/$STAGE/terraform.tfstate and
reports Resources: 0 added, 0 changed, 1 destroyed., or Resources: 0 added, 0 changed, 0 destroyed. for a stage whose old tfvars do not set manage_webhook = true; $OLD's hooks no
longer list https://deploy-hooks.webathome.org/api/webhook, only its Jenkins one.
An app with a volume: the hook logs presync: 0 volume(s) reattached, and step 3's read run again
into $REC/pv-$NS-after.json is the same line for line, so the PV and what backs it are the
objects they were.
8. The producer #
Run for each producer: $APP-deploy from AaC/$OLD, and a two-stage app's $APP-dev-deploy
from AaC/$OLD-dev. AaC/$MONO has a green build that archived
$APP/docs/architecture/$PROD.yaml (the read above): step 1's push started it. The producer proof
is that its artifact is, byte for byte, the one the old job published. The artifact carries the
app's image tags, and a pins differ copy to $OLD (step 2's wait) started AaC/$JOB again: run
the proof once that build is done, so that each side is its job's last green build.
curl -sf $J "$JENKINS_URL/job/AaC/job/$JOB/lastSuccessfulBuild/artifact/docs/architecture/$PROD.yaml" | sha256sum
curl -sf $J "$JENKINS_URL/job/AaC/job/$MONO/lastSuccessfulBuild/artifact/$APP/docs/architecture/$PROD.yaml" | sha256sum
Equal sums, or the producer proof stops the move. Then Architecture's pipeline-producers.yaml:
the producer's entry takes the monorepo shape (argocd.md),
repo: pvginkel/$MONO, jenkinsJob: AaC/$MONO, path: $APP. Before the push, let
AaC/Architecture go idle, nothing building and nothing queued: a green build of any producer's
job queues one through its upstream trigger, and each new build aborts the one running
(disableConcurrentBuilds(abortPrevious: true) in Architecture's Jenkinsfile), which then ends
NOT_BUILT. Its last green build is A. Commit, push, and follow the build the push starts
(track_build.py --hash <sha> AaC/Architecture). It goes green, and the model is unchanged: the
producer's collected input is the same bytes in that build B as in A.
for b in $A $B; do
curl -sf $J "$JENKINS_URL/job/AaC/job/Architecture/$b/artifact/producer-artifacts.tgz" \
| tar -xzO --wildcards "producer-artifacts/$PROD/*$PROD.yaml" | sha256sum
done
9. The old job and repo #
Each producer's old job only after that producer's step 8, its proof, its re-point and its
compare: until the re-point's green build, the collector copies the producer from AaC/$JOB, and
it copies from every registered producer's job, so a job an entry still names cannot go.
curl -sf $J "$JENKINS_URL/job/AaC/job/$JOB/config.xml" > $REC/AaC-$JOB-config.xml # a rollback's copy
curl -sf -X POST $J "$JENKINS_URL/job/AaC/job/$JOB/doDelete"
Once every producer's old job is gone, the repo:
gh api -X PATCH repos/pvginkel/$OLD -F archived=true --jq .archived
The old repo's relay webhook went with step 7.
10. The annotation proof #
Pilots only: slice 057 read it for both pilots in one wait after both moves, and it held (argo-cd
D69); a later move does not repeat it. A commit to the monorepo outside $APP/ must leave the app
alone:
cexec iac kubectl get job -n argocd-hooks tf-presync-$NS -o jsonpath='{.metadata.creationTimestamp}{"\n"}'
cexec iac kubectl get configmap -n $NS tf-presync-revision -o jsonpath='{.data.revision}{"\n"}'
cexec iac kubectl get application -n argocd-prd $NS -o jsonpath='{.status.sync.status} {.status.reconciledAt}{"\n"}'
Read before the commit, after its webhook delivery, and after the periodic refresh that follows it
(timeout.reconciliation: 30m, no jitter): the app stays Synced, with the Job's creation time and
the ConfigMap's revision unchanged. The webhook refreshes no app whose directory the commit misses;
the periodic refresh comes at a 30-minute tick once the app's last reconcile is at least 30 minutes
old, so up to an hour after the push, and only then does the app's status revision reach the new
SHA. Never a hard refresh: it re-renders at HEAD and runs the app's hook (argo-cd D69).
Why the order holds #
- The pins (2) before the rename (3), and nothing commits to
$OLDuntil the switch (5). WithselfHeal: falsethe old Application syncs only on a new commit to$OLD(argocd.md). A commit in that window, a DockerImages pin write or a hand edit, syncs it, and its hook applies against an empty state at the old key. - The wait (2) before the rename (3): a DockerImages build that started before the pin push
writes its pins from the entries it checked out, into
$OLD, and the push queues behind it rather than ending it. Before the rename its pin syncs the old Application against the state; after it, against an empty one. The push's own build pins the app's rebuilt images into$APP/: before the rename they reach the app through$OLD; after it, step 6's diff shows them and rule (2) parks the move. - The directory pushed (1) before the plan (4): the plan clones the monorepo at its SHA.
- The rename (3) and a clean plan (4) before the switch (5): the switched Application's first sync applies at the new key, which has to hold the state by then.
- The switch with
autoSync: false(5), the diff (6), thenautoSyncon (7): the diff is read before anything applies. - A green
AaC/$MONObuild before the re-point (8): a registered producer with no archived artifact fails the collector's discovery, and a failed collector run publishes nothing. - Each old job (9) after its own producer's re-point and green build: until then the collector
copies that producer from
AaC/$JOB, and a collection that cannot copy a registered producer fails. keycloak'sAaC/KeycloakDeploy-devwaits onkeycloak-dev-deploy's re-point asAaC/KeycloakDeploywaits onkeycloak-deploy's.
Rollback #
Four windows, by how far the move got. A later window's steps undo the earlier steps too, in reverse.
Before the registry switch #
Steps 3 and 4 done, 5 not; where rule (1) stops a move. Argo has seen nothing of it. Revert the rename:
cd /work/scratch/TerraformState && git pull --ff-only
mkdir -p argocd/$OLD && git mv argocd/$MONO/$APP/$STAGE argocd/$OLD/$STAGE
git commit -m "argocd/$MONO/$APP/$STAGE → argocd/$OLD/$STAGE: $APP's move into $MONO reverted"
git push origin main
Then the plan sheet at the old key, REPO=$OLD APP_PATH=, from $OLD's head: no changes. The
directory deploys nothing and stays while the operator decides; abandoning the move takes $APP/
and its project out of the monorepo in one commit, and the pins (2) back to $OLD.
After the switch, before the sync #
Step 5 done, 7 not; where rule (2) parks a move. The Application reads the monorepo with autoSync
off, and the state is at the new key. In this order:
- The registry entry back to
$OLD: itsrepo:as it was, nopath:,autoSync: falsestill on each stage. Test, commit, push. Oncereleaseshas synced, the Application reads$OLDand isSyncedat$OLDREV. - The state back to the old key, as above, with its plan.
autoSyncon: dropautoSync: false, test, commit, push. Nothing syncs: the app stands at the revision it synced last.
autoSync is off throughout, so nothing applies in between.
After the sync #
Step 7 done, 9 not: the app runs from the monorepo, $OLD is unarchived and each producer's
AaC/$JOB exists; one that step 9 already deleted comes back first, as after
archival does it. The move in reverse:
- Each producer whose step 8 is done: its entry back to
repo: pvginkel/$OLD,jenkinsJob: AaC/$JOB, nopath:. Push;AaC/Architecturegreen. - The registry entry back to
$OLDwithautoSync: falseon each stage. Push. The Application reads$OLD, and its diff (step 6's read) is the ConfigMap'sdata.revisionalone, back to$OLDREV. - The state back to the old key, as above. Its plan from
$OLD's head carries one change,github_repository_webhook.argocd[0]: create:$OLD's tfvars still setmanage_webhook = true, and the move's sync destroyed the webhook. A stage whose tfvars do not set it plans no changes. autoSyncon. Push. Argo syncs$OLDREV, and the hook's apply atargocd/$OLD/$STAGE/terraform.tfstaterecreates the webhook:1 added, 0 changed, 0 destroyed, or0 added, 0 changed, 0 destroyedfor a stage whose tfvars do not set it.- The pins (2) back to
$OLD, and$APP/and its project out of the monorepo in one commit.
After archival #
Step 9 done. Unarchive the repo, then recreate each producer's old job from its saved
config.xml, the push trigger with it; creating a job starts no build, so build it once:
gh api -X PATCH repos/pvginkel/$OLD -F archived=false --jq .archived
curl -sf -X POST $J -H 'Content-Type: application/xml' --data-binary @$REC/AaC-$JOB-config.xml \
"$JENKINS_URL/job/AaC/createItem?name=$JOB"
curl -sf -X POST $J "$JENKINS_URL/job/AaC/job/$JOB/build"
Each build green, with docs/architecture/$PROD.yaml archived, then after the
sync, whose step 1 needs them.