SCM Projects Infra/Deploy RUNBOOK Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to drive this runbook, BUT this is an operations runbook (account creation + sops + CR re-apply + helm), NOT TDD code. There is no test-first cycle. Steps use checkbox (
- [ ]) syntax; each step is a concrete operator action with the exact command. Steps marked [USER-GATED] MUST be performed by the human (prod mutation, outward-facing SCM writes, or interactive secret-setting from a tty); the agent prepares and verifies around them but does not run them.
Goal: Stand up the SCM-Projects feature in prod: a dedicated tatara-bot GitHub + GitLab identity with a per-Project fine-grained PAT, the board + approval label, the scm block patched onto each Project CR via the declarative manifest (never kubectl patch on a helm/declaratively-managed spec), bumped operator/cli/wrapper image+chart pins applied from main, and the expanded webhook event set registered on every enrolled repo.
Architecture: Identity + secrets land first (bot accounts, FG-PAT, sops token/webhookSecret). The board (GitHub Projects v2 tatara / GitLab issue board) + tatara/awaiting-approval label + Proposed column are created and their numeric ids captured. The Project CR carries a new spec.scm block (provider/owner/botLogin/board/mergePolicy/prReactionScope/approvalLabel) edited into the declarative sample manifest and re-applied (the Project is NOT chart-templated, so this is kubectl apply -f of the single source of truth, not an ad-hoc patch). Operator/cli/wrapper image+chart pins are bumped in the infra helmfile, diffed, applied from main, then the operator is rollout-restarted because it reads image-pin envs only at startup (internal/config/config.go). Webhooks are registered per-repo because a fine-grained PAT has no org-webhook automation.
Tech Stack: GitHub (REST v3 + Projects v2 GraphQL), GitLab (REST), gh / glab CLIs, kubectl, helmfile + mise, sops-secret-helper, Harbor OCI charts/images.
Spec: docs/superpowers/specs/2026-06-09-scm-projects-pr-reactions-design.md. Contract lock: docs/superpowers/specs/2026-06-09-scm-projects-contract-lock.md (sections 2 and 5 govern the CR scm shape and the controller flow this infra feeds). All cluster config lives in ~/Documents/infra/helmfile/helmfiles/tatara (CLAUDE.md rule 14). Build/deploy from main only (rule 10).
File Structure¶
| File | Create/Modify | Responsibility |
|---|---|---|
~/Documents/tatara/tatara-operator/deploy-samples/tatara-project.yaml | Modify | Single declarative source of the tatara Project CR; gains the spec.scm block. Re-applied with kubectl apply -f (Project is not chart-templated; no kubectl patch). |
~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/default.secrets.yaml | Modify (sops, USER-run set) | Holds scmToken (bot FG-PAT) + scmWebhookSecret; chart renders them into the tatara-scm Secret keys token + webhookSecret. |
~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/default.yaml | Modify | Bumps ingesterImage if needed (no scm-feature change); confirms scmSecretName: "tatara-scm" present. |
~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/common.yaml | Modify | Bumps operator image.tag pin to the new operator appVersion. |
~/Documents/infra/helmfile/helmfiles/tatara/helmfile.yaml.gotmpl | Modify | Bumps operator chart version: pin to the new chart version. |
/tmp/tatara-board-ids.env | Create (ephemeral) | Scratch capture of the board number/id + project node id for the GraphQL board ops; never committed. |
Board identifiers to be filled in by the operator and recorded in MEMORY.md once known: - GITHUB_PROJECT_NUMBER = <TBD-after-Task 3> (the Projects v2 board number) - GITLAB_BOARD_ID = <TBD-after-Task 3, only if a GitLab Project exists> - GITHUB_PROJECT_NODE_ID = <TBD-after-Task 3> (GraphQL node id; used by the operator's addProjectV2ItemById board ops, not stored in the CR)
Task 1: Dedicated tatara-bot identity + per-Project fine-grained PAT¶
Files: none (account + token provisioning). Capture the bot logins; they become spec.scm.botLogin in Task 4.
-
Step 1: [USER-GATED] Create the
tatara-botGitHub user. A bot identity cannot be created by the agent (account signup, email verification). The human signs up a dedicated GitHub account (suggested logintatara-bot), enables 2FA, and confirms the login string. Record it asGH_BOT_LOGIN=tatara-bot(this exact string is the authoritative "is this mine" signalspec.scm.botLogin; contract lock section 2). The agent does NOT create accounts. -
Step 2: [USER-GATED] Invite
tatara-botto theszymonrychuorg and grant repo + org-Projects scope. Human action in the org settings: addtatara-botas a member, and on each enrolled repo grant the bot Write on Contents, Pull requests, and Issues. Grant the bot read+write on org-level Projects (so it can move cards on thetataraProjects v2 board). The agent cannot administer org membership. Verify (agent-runnable, read-only) once the human confirms:Expected:gh api orgs/szymonrychu/members/tatara-bot --silent && echo "bot is org member" gh api repos/szymonrychu/tatara-operator/collaborators/tatara-bot/permission \ --jq '.permission' # expect: write or adminbot is org member; permissionwrite(oradmin). -
Step 3: [USER-GATED] Mint the per-Project fine-grained PAT (GitHub). Logged in AS
tatara-bot, the human creates a fine-grained PAT scoped to the resource ownerszymonrychu, selecting only the enrolled repos, with Repository permissions: Contents=Read and write, Pull requests=Read and write, Issues=Read and write, Metadata=Read (mandatory), Webhooks=Read and write (so Task 6 can register hooks with this same token); and Organization permissions: Projects=Read and write. Noadmin:org. The PAT value is thetokenkey for the Project Secret. The agent never sees the token. Record nothing in git; the human keeps it for Step 5 of Task 2. -
Step 4: [USER-GATED] Create the
tatara-botGitLab user +apitoken (only if a GitLab Project is enrolled). Human signs up a dedicated GitLab usertatara-bot, adds it to the group/project with Developer (or Maintainer if MR-merge is required) role, then mints a Project (or group) access token / PAT with theapiscope (covers issues, MR notes/approvals, award emoji, board label edits per contract lock section 1). This token is thetokenkey whenprovider=gitlab. Skip this task entirely if no GitLab Project is enrolled in this rollout.
Task 2: Per-Project Secret keys token + webhookSecret (sops, USER-set)¶
Files: - Modify: ~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/default.secrets.yaml (sops; set is USER-run from a tty) - Confirm: ~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/default.yaml has scmSecretName: "tatara-scm"
Per the operator sops-managed-secrets design, the chart renders the tatara-scm Secret from sops values scmToken -> data key token and scmWebhookSecret -> data key webhookSecret (the keys Project.Spec.ScmSecretRef resolves; consumed at internal/agent/pod.go, internal/ingest/job.go, internal/webhook/server.go). Setting these is the only secret path; do NOT kubectl create secret by hand for an sops-managed cluster.
-
Step 1: Confirm the secrets file exists and list its current keys (agent-runnable, no values).
Expected: lists existing keys (e.g.cd ~/Documents/infra/helmfile [[ -f .env ]] && echo "dotenv present" || echo "no dotenv (run op_env_dotfile first)" ./scripts/sops-secret-helper.sh keys helmfiles/tatara/values/tatara-operator/default.secrets.yamloperatorOidcClientSecret,anthropicOauthToken, ...).keysshows names only, never values; the agent may run it. -
Step 2: [USER-GATED] Set the bot FG-PAT as
scmToken. The value is read interactively from/dev/tty;setfails without a tty and the agent must NOT run it. Tell the user to run, pasting the GitHub FG-PAT (or GitLabapitoken) from Task 1 when prompted: -
Step 3: [USER-GATED] Set the webhook HMAC secret as
scmWebhookSecret. Same tty-onlyset; agent must NOT run it. Generate a value first (the user runs both lines; the secret is piped into the prompt, never echoed into git):Note for the user: the samecd ~/Documents/infra/helmfile openssl rand -hex 24 # copy this output; paste it at the prompt below AND keep it for Task 6 webhook registration ./scripts/sops-secret-helper.sh set helmfiles/tatara/values/tatara-operator/default.secrets.yaml scmWebhookSecretscmWebhookSecretvalue must be supplied asconfig[secret]to every webhook in Task 6, so the operator's HMAC verification (internal/webhook/server.go) matches. Keep it in your clipboard / a tmp file you delete after Task 6. -
Step 4: Verify the keys landed (agent-runnable). After the user confirms Steps 2-3:
Expected: bothcd ~/Documents/infra/helmfile ./scripts/sops-secret-helper.sh keys helmfiles/tatara/values/tatara-operator/default.secrets.yaml | grep -E 'scmToken|scmWebhookSecret'scmTokenandscmWebhookSecretlisted. -
Step 5: Confirm
scmSecretNameis wired (agent-runnable, read-only).Expected:grep -n 'scmSecretName' ~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/default.yamlscmSecretName: "tatara-scm"present (matchesProject.spec.scmSecretRef: tatara-scm). If absent, add it; it is non-secret and lives in plaintext values.
Task 3: Board + approval label + Proposed column¶
Files: ephemeral /tmp/tatara-board-ids.env (captured ids; never committed). The captured GITHUB_PROJECT_NUMBER becomes spec.scm.board.githubProjectNumber in Task 4; GITLAB_BOARD_ID becomes spec.scm.board.gitlabBoardId.
-
Step 1: [USER-GATED] Create the GitHub Projects v2 board named
tatara. Outward-facing prod create against the org. Run as a human (or the agent with explicit go-ahead, since it mutates the org). Get the org node id, create the project, capture its number + node id:Expected: a project number (small int) and a node id (OWNER_ID=$(gh api graphql -f query='query($login:String!){organization(login:$login){id}}' -F login=szymonrychu --jq '.data.organization.id') PROJ=$(gh api graphql -f query=' mutation($ownerId:ID!,$title:String!){createProjectV2(input:{ownerId:$ownerId,title:$title}){projectV2{id number}}}' \ -F ownerId="$OWNER_ID" -F title='tatara') echo "$PROJ" echo "GITHUB_PROJECT_NUMBER=$(echo "$PROJ" | jq -r '.data.createProjectV2.projectV2.number')" | tee -a /tmp/tatara-board-ids.env echo "GITHUB_PROJECT_NODE_ID=$(echo "$PROJ" | jq -r '.data.createProjectV2.projectV2.id')" | tee -a /tmp/tatara-board-ids.envPVT_...). RecordGITHUB_PROJECT_NUMBER- it is required in Task 4 and inMEMORY.md. -
Step 2: [USER-GATED] Ensure a
Statussingle-select field with aProposedoption. The operator'sSetBoardColumnwrites theStatussingle-select (contract lock section 1,BoardRef.StatusFielddefault"Status"). New Projects v2 boards include aStatusfield withTodo/In Progress/Done; add theProposedoption used by the proposal flow:Expected: aPNODE=$(grep GITHUB_PROJECT_NODE_ID /tmp/tatara-board-ids.env | cut -d= -f2) FIELD=$(gh api graphql -f query=' query($p:ID!){node(id:$p){... on ProjectV2{field(name:"Status"){... on ProjectV2SingleSelectField{id options{id name}}}}}}' -F p="$PNODE") echo "$FIELD" # confirm a Status field exists; note its id # If "Proposed" is not among options, add it (preserve existing options - GraphQL replaces the full set): # gh api graphql -f query='mutation($f:ID!,$opts:[ProjectV2SingleSelectFieldOptionInput!]!){updateProjectV2Field(input:{fieldId:$f,singleSelectOptions:$opts}){projectV2Field{... on ProjectV2SingleSelectField{options{name}}}}}' \ # -F f=<FIELD_ID> -F 'opts[][name]=Proposed' -F 'opts[][color]=GRAY' -F 'opts[][description]=tatara-proposed, awaiting approval' (repeat for Todo/In Progress/Done so they are not dropped)Statusfield id and an options list includingProposed(after the optional update). If your tatara workflow uses board columns beyondProposed(e.g.In Progress,Done), ensure they exist here too; the operator only writes the option name it is told to set. -
Step 3: [USER-GATED] Create the approval label
tatara/awaiting-approvalon every enrolled repo. Labels are per-repo on GitHub. This is thespec.scm.approvalLabeldefault and the approval state-machine source of truth (spec section D). Outward-facing prod write; run as human or with explicit go-ahead:Expected: a label per repo (or "already exists"). Also ensure the existing trigger labelfor r in tatara-memory tatara-cli tatara-operator tatara-chat tatara-memory-repo-ingester tatara-claude-code-wrapper tatara-argo-workflows; do gh api -X POST "repos/szymonrychu/$r/labels" \ -f name='tatara/awaiting-approval' -f color='fbca04' \ -f description='tatara-proposed work item; remove to approve' \ && echo "label: $r" || echo "label exists or failed: $r" donetataraexists on each repo (enrollment runbook already created it; re-run this loop with-f name='tatara'if any are missing). -
Step 4: [USER-GATED] (GitLab only) Create the issue board + list labels + approval label. Skip if no GitLab Project is enrolled. The board is label-driven (contract lock section 1:
SetBoardColumnswaps aboard::<col>scoped label). Create the scoped list labels and the approval label, then the board, and capture its id:Expected: the four labels created and a board id. RecordGL_PROJECT="<group/project>" # the enrolled GitLab project path for lbl in 'board::Proposed' 'board::In Progress' 'board::Done' 'tatara/awaiting-approval'; do glab api -X POST "projects/$(printf '%s' "$GL_PROJECT" | jq -sRr @uri)/labels" \ -f "name=$lbl" -f color='#fbca04' && echo "gl label: $lbl" done BOARD=$(glab api -X POST "projects/$(printf '%s' "$GL_PROJECT" | jq -sRr @uri)/boards" -f name='tatara') echo "GITLAB_BOARD_ID=$(echo "$BOARD" | jq -r '.id')" | tee -a /tmp/tatara-board-ids.envGITLAB_BOARD_IDfor Task 4.
Task 4: Patch each Project CR with the scm block (declarative re-apply, NOT kubectl patch)¶
Files: - Modify: ~/Documents/tatara/tatara-operator/deploy-samples/tatara-project.yaml
The Project CR is NOT templated by the operator chart (the chart's templates/ has no project.yaml); deploy-samples/tatara-project.yaml is its single declarative source. CLAUDE.md "never kubectl patch helm-managed spec fields" applies to declaratively-managed specs generally: edit the manifest (the source of truth) and kubectl apply -f it, so the live spec never drifts from git. Do NOT kubectl patch project .... The scm field shapes are byte-for-byte from contract lock section 2 (camelCase json tags: provider, owner, botLogin, board.{githubProjectNumber,gitlabBoardId,statusField}, mergePolicy, prReactionScope, approvalLabel).
-
Step 1: Confirm the operator CRD already serves
spec.scm(agent-runnable, read-only). The CRD must carry thescmschema (shipped by the operator build bumped in Task 5). If the operator/CRD bump in Task 5 has not been applied yet, do Task 5 first - applying a Project with an unknownspec.scmfield is rejected/pruned.Expected: thekubectl get crd projects.tatara.dev -o jsonpath='{.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.scm.properties}{"\n"}' | tr ',' '\n' | grep -E 'provider|owner|botLogin|board|mergePolicy|prReactionScope|approvalLabel' && echo "scm schema present"scmsub-properties listed andscm schema present. If empty, run Task 5 (chart/CRD bump) before continuing. -
Step 2: Edit the Project manifest to add
spec.scm. Open~/Documents/tatara/tatara-operator/deploy-samples/tatara-project.yamland add thescm:block under thetataraProject'sspec:, fillingboard.githubProjectNumberfromGITHUB_PROJECT_NUMBERcaptured in Task 3 (andgitlabBoardIdonly for a GitLab Project). For the GitHub dogfood Project the spec block is exactly:For a GitLab Project, the block is insteadscm: provider: github owner: szymonrychu botLogin: tatara-bot board: githubProjectNumber: <GITHUB_PROJECT_NUMBER> statusField: Status mergePolicy: afterApproval prReactionScope: labeledOrMentioned approvalLabel: tatara/awaiting-approvalprovider: gitlab,owner: <group>,botLogin: tatara-bot,board: {gitlabBoardId: <GITLAB_BOARD_ID>}, samemergePolicy/prReactionScope/approvalLabel. Field names and enum values (github|gitlab,afterApproval|autoMergeOnGreenCI,labeledOrMentioned|all) must match contract lock section 2 byte-for-byte.statusFielddefaults toStatusserver-side (kubebuilder default) but is set explicitly here for clarity. Repeat the whole block per Project if more than one Project is enrolled. -
Step 3: [USER-GATED] Apply the updated Project manifest (prod CR mutation). Declarative apply of the source-of-truth manifest - never
Expected:kubectl patch. Prod-mutating; run as human or with explicit go-ahead:project.tatara.dev/tatara configured. -
Step 4: Verify the live spec carries the
Expected: the JSON object withscmblock (agent-runnable, read-only).provider,owner,botLogin,board.githubProjectNumber,mergePolicy:afterApproval,prReactionScope:labeledOrMentioned,approvalLabel:tatara/awaiting-approval. No field pruned (confirms the CRD schema served it). -
Step 5: Commit the manifest change (no secrets; agent-runnable).
Task 5: Bump operator/cli/wrapper image+chart pins; helmfile diff -> apply from main; rollout-restart operator¶
Files: - Modify: ~/Documents/infra/helmfile/helmfiles/tatara/helmfile.yaml.gotmpl (operator chart version: pin) - Modify: ~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/common.yaml (operator image.tag pin) - Modify: ~/Documents/infra/helmfile/helmfiles/tatara/values/tatara-operator/default.yaml (wrapper image is consumed via the Project CR spec.agent.image, bumped in Task 4's manifest; bump ingesterImage here only if that image was rebuilt - the scm feature does not rebuild it)
The operator reads image-pin envs ONLY at startup (internal/config/config.go os.Getenv("INGESTER_IMAGE"), MEMORY_IMAGE, etc., surfaced via the chart ConfigMap). A helm apply that changes ConfigMap values does not by itself restart the manager Pod, so a rollout restart is required after apply. Build from main only (rule 10): the operator/cli/wrapper images and the operator chart must already be published to Harbor from their respective main branches before this task (image/chart publishing is out of this runbook's scope; this task pins + applies the published artifacts).
-
Step 1: Confirm the published artifact versions (agent-runnable, read-only). Determine the new operator chart
Expected: the operator chart version/appVersion that include the scm CRD + controller flow.version/appVersion, the operatorimage.tag, the wrapper image tag, and the cli image tag that carry the scm-projects feature (from each repo'sChart.yaml/release tag onmain). Record them asOP_VER,WRAP_TAG,CLI_TAGfor the edits below. (The cli image is bundled into the wrapper image at wrapper build time; the cluster pins only the wrapper image via the Project CRspec.agent.image, soCLI_TAGis informational here.) -
Step 2: Bump the operator chart
version:pin in the helmfile.Expected: thecd ~/Documents/infra/helmfile # replace the current operator release version pin (e.g. 0.2.14) with $OP_VER: sed -i '' 's/^\( version: \)0\.2\.14$/\1<OP_VER>/' helmfiles/tatara/helmfile.yaml.gotmpl grep -n 'version:' helmfiles/tatara/helmfile.yaml.gotmpltatara-operatorreleaseversion:now reads<OP_VER>. (Edit by hand if multiple releases share that version literal so only the operator release changes.) -
Step 3: Bump the operator
image.tagpin.Expected:cd ~/Documents/infra/helmfile sed -i '' 's/^\( tag: "\)0\.2\.14"$/\1<OP_VER>"/' helmfiles/tatara/values/tatara-operator/common.yaml grep -n 'tag:' helmfiles/tatara/values/tatara-operator/common.yamltag: "<OP_VER>". (image.tagdefaults toappVersionwhen empty, but the bucket pins it explicitly; keep them in lockstep.) -
Step 4: Bump the wrapper image pin. The wrapper image is consumed by the operator-spawned agent Pod via the Project CR
spec.agent.image(NOT an operator value). If the wrapper image tag changed, update it indeploy-samples/tatara-project.yamland re-apply per Task 4 Step 3. Confirm the intended tag:Expected:grep -n 'tatara-claude-code-wrapper' ~/Documents/tatara/tatara-operator/deploy-samples/tatara-project.yamlagent.image: harbor.szymonrichert.pl/containers/tatara-claude-code-wrapper:<WRAP_TAG>. If stale, edit it and fold it into Task 4's apply (one prod apply, not two). -
Step 5: helmfile diff (agent-runnable, read-only).
Expected: the diff shows the operator Deployment image bumped tocd ~/Documents/infra/helmfile [[ -f .env ]] && echo "dotenv present" || echo "no dotenv (run op_env_dotfile first)" mise exec -- helmfile -e default -l application=tatara-operator diff<OP_VER>, the chart version change, thetatara-scmSecret rendered from the Task 2 sops values (token/webhookSecret), and no spurious churn in unrelated objects. -
Step 6: [USER-GATED] helmfile apply from main (prod deploy). Prod-mutating; run as human or with explicit go-ahead. Must be from
mainof the infra repo (rule 10):Expected: the release upgrades cleanly;cd ~/Documents/infra/helmfile git rev-parse --abbrev-ref HEAD # confirm: main mise exec -- helmfile -e default -l application=tatara-operator applytatara-scmSecret present with keystoken+webhookSecret. -
Step 7: [USER-GATED] Rollout-restart the operator (it reads image-pin envs at startup). Even though the apply changed the Deployment image (triggering a rollout), force a restart to guarantee the manager re-reads every image-pin env from the bumped ConfigMap:
Expected:kubectl -n tatara rollout restart deploy/tatara-operator kubectl -n tatara rollout status deploy/tatara-operator --timeout=300sdeployment "tatara-operator" successfully rolled out. -
Step 8: Verify the operator is healthy and serving the scm CRD + image-pin envs (agent-runnable, read-only).
Expected: operator podkubectl -n tatara get pods -l app.kubernetes.io/name=tatara-operator kubectl -n tatara get cm tatara-operator -o jsonpath='{.data.INGESTER_IMAGE}{"\n"}' kubectl get crd projects.tatara.dev -o jsonpath='{.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.scm}{"\n"}' | head -c 200; echoRunning/ready; the ConfigMap image-pin env reads the expected tag; the CRD servesspec.scm. (This is the precondition for Task 4 if Task 4 was deferred until after this task.) -
Step 9: Commit the infra pin bumps (no secrets; agent-runnable).
Task 6: Register the expanded webhook event set per enrolled repo¶
Files: none. Uses the bot FG-PAT (which carries Webhooks=Read and write from Task 1 Step 3) and the scmWebhookSecret value from Task 2 Step 3. A fine-grained PAT has NO org-webhook automation, so every repo is registered individually.
The operator's expanded dispatch (spec section C) handles issues, issue_comment, pull_request, pull_request_review, plus the existing push. GitLab equivalents: Issues, Note (comments), Merge request, and pipeline events. All POST to https://tatara.szymonrichert.pl/operator/webhooks/tatara with HMAC = scmWebhookSecret.
-
Step 1: Stage the webhook secret for the registration loop (USER provides it). The agent never holds the secret. The human exports it for this shell (it was generated in Task 2 Step 3):
Expected:# [USER-GATED] human runs: export WHS='<the scmWebhookSecret value from Task 2 Step 3>' [ -n "$WHS" ] && echo "WHS set" || echo "WHS missing - human must export it" URL="https://tatara.szymonrichert.pl/operator/webhooks/tatara"WHS set. -
Step 2: [USER-GATED] Register/patch the GitHub webhook on every enrolled repo with the new event set. Outward-facing prod write using the bot PAT (
ghauthenticated astatara-bot, orGH_TOKEN=<bot FG-PAT>). Idempotent: create, and if a hook to that URL exists, patch its events instead:Expected: each repo reportsEVENTS='-f events[]=push -f events[]=issues -f events[]=issue_comment -f events[]=pull_request -f events[]=pull_request_review' for r in tatara-memory tatara-cli tatara-operator tatara-chat tatara-memory-repo-ingester tatara-claude-code-wrapper tatara-argo-workflows; do HID=$(gh api "repos/szymonrychu/$r/hooks" --jq ".[] | select(.config.url==\"$URL\") | .id" 2>/dev/null | head -1) if [ -n "$HID" ]; then gh api -X PATCH "repos/szymonrychu/$r/hooks/$HID" -F active=true $EVENTS \ -f config[url]="$URL" -f config[content_type]=json -f config[secret]="$WHS" -f config[insecure_ssl]=0 \ && echo "patched: $r" || echo "patch FAILED: $r" else gh api -X POST "repos/szymonrychu/$r/hooks" -f name=web -F active=true $EVENTS \ -f config[url]="$URL" -f config[content_type]=json -f config[secret]="$WHS" -f config[insecure_ssl]=0 \ && echo "created: $r" || echo "create FAILED: $r" fi donecreatedorpatched. The hook now deliverspush,issues,issue_comment,pull_request,pull_request_review. -
Step 3: [USER-GATED] (GitLab only) Register the project webhook with the equivalent events. Skip if no GitLab Project enrolled. Uses the bot
apitoken (glabauthenticated astatara-bot):Expected:GL_PROJECT="<group/project>" PID=$(printf '%s' "$GL_PROJECT" | jq -sRr @uri) glab api -X POST "projects/$PID/hooks" \ -f url="$URL" -f token="$WHS" \ -F push_events=true -F issues_events=true -F note_events=true \ -F merge_requests_events=true -F pipeline_events=true -F enable_ssl_verification=true \ && echo "gl hook created" || echo "gl hook FAILED"gl hook created. (note_eventscovers MR/issue comments;merge_requests_events+pipeline_eventscover the PR/MR-review + CI signals.) -
Step 4: Verify deliveries land (agent-runnable, read-only once a test event fires). Confirm the operator receives and HMAC-verifies the new events. Either ping a hook or watch the metric:
Expected: a delivery with# GitHub: ping one hook and check last delivery HID=$(gh api repos/szymonrychu/tatara-operator/hooks --jq ".[] | select(.config.url==\"$URL\") | .id" | head -1) gh api -X POST "repos/szymonrychu/tatara-operator/hooks/$HID/pings" && echo "pinged" gh api "repos/szymonrychu/tatara-operator/hooks/$HID/deliveries" --jq '.[0] | {event:.event, status:.status_code}' # Operator side: the action-labelled webhook counter should increment kubectl -n tatara exec deploy/tatara-operator -- wget -qO- localhost:9090/metrics 2>/dev/null | grep operator_webhook_events_total | headstatus_code: 200(or the operator's accepted code);operator_webhook_events_total{...,action="..."}present (theactionlabel was added per spec section H / contract lock section 6). -
Step 5: [USER-GATED] Clear the staged webhook secret from the shell. Human runs:
Task 7: Record board identifiers + close out¶
Files: - Modify: ~/Documents/tatara/tatara-operator/MEMORY.md (record the board number/id, bot login, and that the Project carries spec.scm)
-
Step 1: Record the identifiers in MEMORY.md (agent-runnable). One dated line each: the GitHub Projects v2 board number (
GITHUB_PROJECT_NUMBER), the GitLab board id if any (GITLAB_BOARD_ID), the bot login (tatara-bot), and thatspec.scmnow lives indeploy-samples/tatara-project.yaml(declarative, not chart-templated). Then commit: -
Step 2: End-to-end smoke (agent-runnable, read-only verification per superpowers:verification-before-completion). Open a
tatara-labelled issue on one repo and confirm a Task is created with the new fields; have the agentpropose_issueand confirm the mirror Task holdsPhase=AwaitingApprovaluntil thetatara/awaiting-approvallabel is removed (the approval gate, contract lock section 5):Expected: a Task withkubectl -n tatara get tasks -o custom-columns=NAME:.metadata.name,KIND:.spec.kind,PHASE:.status.phase,AUTHOR:.spec.source.authorLoginKINDpopulated (implement|review|selfImprove) and, for a proposal,PHASE=AwaitingApproval. Removing the approval label flipsApprovalApproved=Trueand releases the gate.
Notes¶
- User-gated steps (prod mutation / outward-facing SCM writes / tty secret-setting; agent must NOT run): Task 1 Steps 1-4 (account + PAT creation); Task 2 Steps 2-3 (
sops ... set); Task 3 Steps 1-4 (board/label/column creation against prod org); Task 4 Step 3 (kubectl applyof the Project CR); Task 5 Steps 6-7 (helmfile apply+ rollout restart); Task 6 Steps 1-3 and 5 (webhook registration + secret handling). Everything else (reads,keys,remove,diff, edits to git-tracked manifests, commits) is agent-runnable. - Never
kubectl patchthe Project spec (CLAUDE.md): the Project is declaratively managed viadeploy-samples/tatara-project.yaml; thescmblock is edited there andkubectl apply -f-ed, so live state never drifts from git. - Order dependency: Task 5 (operator/CRD bump applied) must precede Task 4's apply, because applying a Project with an unknown
spec.scmfield is pruned/rejected. If executing strictly top-to-bottom, do Task 5 before Task 4 Step 3 (Task 4 Step 1 guards this). - One prod apply for the Project CR: if the wrapper image tag also changed (Task 5 Step 4), fold that edit into Task 4 Step 2 and apply once.
- Board ids are captured to
/tmp/tatara-board-ids.envduring Task 3 and recorded in MEMORY.md in Task 7; the tmp file is deleted in Task 6 Step 5.