From 202ee34ef3ab8da4797b11d21b7b60403b1261bc Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Wed, 3 Jun 2026 13:40:05 +0200 Subject: [PATCH] chore: remove bootstrap scripts from main The tools/seed-gitea.sh seeder + README reached main via an earlier chore commit, but the agreed decision is to manage Gitea entirely with the tea CLI and not keep bespoke scripts in the repo. Remove them; the backlog they seeded already exists in Gitea. Co-Authored-By: Claude Opus 4.8 --- tools/README.md | 54 ------ tools/seed-gitea.sh | 430 -------------------------------------------- 2 files changed, 484 deletions(-) delete mode 100644 tools/README.md delete mode 100644 tools/seed-gitea.sh diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index e296e08..0000000 --- a/tools/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# tools/ - -Repo bootstrap and maintenance scripts. Not part of the application runtime. - -## `seed-gitea.sh` — bootstrap the Gitea backlog - -One-time (idempotent) script that creates the project's backlog in Gitea from the -contents of [`BACKLOG.md`](../BACKLOG.md): the label taxonomy, the iteration -milestones, and all 26 slice issues (`S-00`…`S-25`). Gitea is the system of record -(see `CLAUDE.md` §7); this script just gets the empty repo to that starting state. - -It overlaps part of **S-00**'s acceptance ("milestones, labels … exist in Gitea"). -The authoritative operational write-up will move to `docs/runbooks/` and -`docs/gitea-workflow.md` when S-00 is implemented. - -### Prerequisites - -- `curl` and `jq` on `PATH`. -- A Gitea **personal access token** with scopes **`write:issue`** and - **`write:repository`** (Gitea → Settings → Applications → Generate New Token). - -### Usage - -```sh -GITEA_TOKEN= bash tools/seed-gitea.sh -``` - -The token is read from the environment only — it is never written to disk or -committed. The target repo (`eho/register-referentie` on -`git.labs.respellion.tech`) is hard-coded near the top of the script; edit the -`BASE`/`OWNER`/`REPO` variables to point elsewhere. - -### What it creates - -| Step | Items | -|------|-------| -| Labels | `type:{slice,bug,adr-proposal,chore}` + 12 `area:*` labels (16 total) | -| Milestones | `Iteration 0 — Foundations` … `Iteration 6 — Production Posture` (7 total) | -| Issues | `S-00`…`S-25` (26 total), each with body, milestone, and area labels | - -### Idempotency - -Every item is matched by name (labels), title (milestones), or issue title prefix -(`S-NN · …`) before creation, and skipped if it already exists. A partial or failed -run can be re-run safely — the second run reports every item as `skip`. - -### Verify (no token needed — reads are anonymous) - -```sh -B=https://git.labs.respellion.tech/api/v1/repos/eho/register-referentie -curl -s "$B/labels?limit=100" | jq length # expect 16 -curl -s "$B/milestones?state=all&limit=100" | jq length # expect 7 -curl -s "$B/issues?state=all&type=issues&limit=100" | jq length # expect 26 -``` diff --git a/tools/seed-gitea.sh b/tools/seed-gitea.sh deleted file mode 100644 index aec0e7b..0000000 --- a/tools/seed-gitea.sh +++ /dev/null @@ -1,430 +0,0 @@ -#!/usr/bin/env bash -# -# seed-gitea.sh — populate the register-referentie Gitea repo with the backlog: -# 1. label taxonomy 2. iteration milestones 3. all 26 slice issues (S-00..S-25) -# -# Idempotent: existing labels/milestones/issues (matched by name/title) are skipped, -# so a partial or failed run can simply be re-run. -# -# Usage: -# GITEA_TOKEN= bash tools/seed-gitea.sh -# -# The token needs scopes: write:issue and write:repository (for labels + milestones). -# -set -euo pipefail - -: "${GITEA_TOKEN:?Set GITEA_TOKEN to a Gitea personal access token (scopes: write:issue, write:repository)}" - -BASE="https://git.labs.respellion.tech/api/v1" -OWNER="eho" -REPO="register-referentie" -REPO_API="$BASE/repos/$OWNER/$REPO" - -AUTH=(-H "Authorization: token $GITEA_TOKEN") -JSON=(-H "Content-Type: application/json" -H "Accept: application/json") - -say() { printf '%s\n' "$*"; } - -# --------------------------------------------------------------------------- -# Preflight: confirm the token can see the repo. -# --------------------------------------------------------------------------- -code=$(curl -s -o /dev/null -w '%{http_code}' "${AUTH[@]}" "$REPO_API") -if [ "$code" != "200" ]; then - say "ERROR: cannot access $OWNER/$REPO (HTTP $code). Check the token and its scopes." - exit 1 -fi -say "Authenticated against $REPO_API" - -# --------------------------------------------------------------------------- -# 1. Labels -# --------------------------------------------------------------------------- -say "" -say "== Labels ==" -existing_labels=$(curl -s "${AUTH[@]}" "$REPO_API/labels?limit=100") - -create_label() { - local name="$1" color="$2" - if echo "$existing_labels" | jq -e --arg n "$name" 'any(.[]; .name == $n)' >/dev/null; then - say " skip $name" - return - fi - curl -s "${AUTH[@]}" "${JSON[@]}" -X POST "$REPO_API/labels" \ - -d "$(jq -n --arg n "$name" --arg c "$color" '{name:$n, color:$c}')" >/dev/null - say " create $name" -} - -create_label "type:slice" "#0e8a16" -create_label "type:bug" "#d73a4a" -create_label "type:adr-proposal" "#5319e7" -create_label "type:chore" "#fbca04" -for area in acl domain portal-self-service portal-openbaar portal-behandel \ - portal-beheer infra workflow event-subscriber projection bff docs; do - create_label "area:$area" "#1d76db" -done - -# --------------------------------------------------------------------------- -# 2. Milestones -# --------------------------------------------------------------------------- -say "" -say "== Milestones ==" -existing_ms=$(curl -s "${AUTH[@]}" "$REPO_API/milestones?state=all&limit=100") - -create_milestone() { - local title="$1" - if echo "$existing_ms" | jq -e --arg t "$title" 'any(.[]; .title == $t)' >/dev/null; then - say " skip $title" - return - fi - curl -s "${AUTH[@]}" "${JSON[@]}" -X POST "$REPO_API/milestones" \ - -d "$(jq -n --arg t "$title" '{title:$t}')" >/dev/null - say " create $title" -} - -create_milestone "Iteration 0 — Foundations" -create_milestone "Iteration 1 — Walking Skeleton" -create_milestone "Iteration 2 — Flow Completeness" -create_milestone "Iteration 3 — Beheer & Observability" -create_milestone "Iteration 4 — Objecten" -create_milestone "Iteration 5 — Data Governance" -create_milestone "Iteration 6 — Production Posture" - -# Re-fetch labels + milestones so we have ids for issue creation. -labels_json=$(curl -s "${AUTH[@]}" "$REPO_API/labels?limit=100") -milestones_json=$(curl -s "${AUTH[@]}" "$REPO_API/milestones?state=all&limit=100") -existing_issues=$(curl -s "${AUTH[@]}" "$REPO_API/issues?state=all&type=issues&limit=100") - -# Definition of Done checklist appended to every slice body (CLAUDE.md §3). -DOD=$(cat <<'EOF' - -## Definition of Done - -- [ ] A linked Gitea issue exists (this one). -- [ ] Failing test written and committed first. -- [ ] Implementation makes the test pass. -- [ ] Refactor commit follows if structure improved. -- [ ] Conventional Commit messages referencing this issue (`refs #NN`). -- [ ] All Gitea Actions CI jobs green: lint, unit, integration, mutation (ratchet), e2e, container build + push, compose-up smoke test. -- [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. -- [ ] Docs touched if behaviour, contracts, or operations changed. -- [ ] ADR added in `docs/architecture/` if a non-obvious decision was made. -- [ ] Demo note appended to `docs/demo-script.md` if the slice is user-visible. -- [ ] This issue closed by the merging PR (`closes #NN`). -EOF -) - -# --------------------------------------------------------------------------- -# 3. Issues -# --------------------------------------------------------------------------- -say "" -say "== Issues ==" - -create_issue() { - local title="$1" milestone="$2" labels_csv="$3" body="$4" - - if echo "$existing_issues" | jq -e --arg t "$title" 'any(.[]; .title == $t)' >/dev/null; then - say " skip $title" - return - fi - - local ms_id - ms_id=$(echo "$milestones_json" | jq -r --arg m "$milestone" '.[] | select(.title == $m) | .id') - if [ -z "$ms_id" ]; then - say " ERROR milestone not found: $milestone (issue: $title)" - exit 1 - fi - - local want label_ids - want=$(printf '%s' "$labels_csv" | jq -R 'split(",")') - label_ids=$(echo "$labels_json" | jq -c --argjson want "$want" \ - '[ .[] | select(.name as $n | $want | index($n)) | .id ]') - - local full_body="${body}${DOD}" - - local payload - payload=$(jq -n --arg t "$title" --arg b "$full_body" \ - --argjson ms "$ms_id" --argjson ls "$label_ids" \ - '{title:$t, body:$b, milestone:$ms, labels:$ls}') - - curl -s "${AUTH[@]}" "${JSON[@]}" -X POST "$REPO_API/issues" -d "$payload" >/dev/null - say " create $title" -} - -# ---- Iteration 0 ---------------------------------------------------------- -create_issue "S-00 · Repository skeleton, Gitea Actions CI, contributor workflow" \ - "Iteration 0 — Foundations" "type:slice,area:infra,area:docs" "$(cat <<'EOF' -**Outcome:** A fresh `git clone` from the Respellion Gitea remote, followed by `docker compose up`, produces a green "hello world" health endpoint from a placeholder BFF. Gitea Actions runs lint, build, unit tests, and the compose-up smoke test, all green. Issue templates, PR template, milestones, labels, and the first project board exist in Gitea. - -**Acceptance:** - -- New developer follows `README.md` and reaches a green local environment in under 10 minutes. -- Gitea Actions pipeline green on `main`. -- `git-cliff` produces an empty `CHANGELOG.md`. -- `docs/PRD.md`, `CLAUDE.md`, `BACKLOG.md`, `docs/architecture/adr-0001-loose-coupling.md`, `docs/gitea-workflow.md` all in repo. -- `.gitea/workflows/ci.yaml`, `.gitea/ISSUE_TEMPLATE/{slice,bug,adr-proposal}.md`, `.gitea/PULL_REQUEST_TEMPLATE.md` all in repo. -- Gitea milestone `Iteration 1 — Walking Skeleton` exists, populated with issues S-01 through S-09. - -**Touches:** repo layout, Gitea Actions workflows, Dockerfile for placeholder BFF, `docker-compose.yml` skeleton, MkDocs scaffold, Gitea issue/PR templates. - -**Out of scope:** any business logic, frontend, OpenZaak. -EOF -)" - -# ---- Iteration 1 ---------------------------------------------------------- -create_issue "S-01 · OpenZaak + Open Notificaties + Postgres come up in compose" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:infra" "$(cat <<'EOF' -**Outcome:** Local `docker compose up` brings up OpenZaak, Open Notificaties, their dependencies, and a seeded ZTC catalogus called `BIG`. A health check confirms all reachable. - -**Acceptance:** - -- `curl` to OpenZaak `/zaken/api/v1/` returns 401 (auth working). -- A test client with a generated JWT can list zaaktypen in the `BIG` catalogus. -- The seeded catalogus contains one lean `BIG-registratie` zaaktype with only the schema-mandatory fields plus `bsn` as an eigenschap. - -**Touches:** `infra/openzaak/`, `infra/opennotificaties/`, `infra/seed/`, ADR for catalogus design. - -**Out of scope:** any portal, BFF, Flowable, ACL code. -EOF -)" - -create_issue "S-02 · Keycloak with mock DigiD, eHerkenning, eIDAS, medewerker realms" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:infra" "$(cat <<'EOF' -**Outcome:** Keycloak runs locally with four realms pre-seeded. Each realm has 1–2 test users with known credentials documented in `docs/synthetic-data.md`. - -**Acceptance:** - -- Browser-based OIDC login flow works for each realm against a placeholder client. -- Mock DigiD realm returns a BSN claim; eHerkenning returns a KvK; eIDAS returns a foreign identifier; medewerker returns role claims. - -**Touches:** `infra/keycloak/`, seed scripts. - -**Out of scope:** real federation, MFA. -EOF -)" - -create_issue "S-03 · Flowable up with a minimal BPMN: \"Registratie ontvangen\"" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:infra,area:workflow" "$(cat <<'EOF' -**Outcome:** Flowable runs locally with Postgres. A single BPMN model (`registratie.bpmn`) deployed with one start event, one external task `OpenZaakAanmaken`, one end event. - -**Acceptance:** - -- BPMN model deployed via Flowable's REST API on container start. -- An HTTP call can start a process instance and observe it waiting on the external task. - -**Touches:** `infra/flowable/`, `workflows/registratie.bpmn`. - -**Out of scope:** DMN, boundary timers, second model. -EOF -)" - -create_issue "S-04 · ACL skeleton with one operation: open a zaak" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:acl" "$(cat <<'EOF' -**Outcome:** A .NET library + service that exposes one method: `OpenZaak(domainPayload) → zaakUrl`. It default-fills `bronorganisatie`, `verantwoordelijkeOrganisatie`, `startdatum`, `vertrouwelijkheidaanduiding`, and posts to OpenZaak. **Strict TDD throughout.** - -**Acceptance:** - -- BDD scenario: "Given a domain registration payload, when I call the ACL, then a zaak exists in OpenZaak with the default-filled fields." -- Mutation score baseline captured and enforced by the Gitea Actions pipeline. -- Integration test using Testcontainers against real OpenZaak passes. - -**Touches:** `services/acl/`, tests, ADR for default-fill strategy. - -**Out of scope:** all other ZGW operations, status transitions, documents. -EOF -)" - -create_issue "S-05 · BIG Domain Service skeleton with the Registration aggregate" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:domain" "$(cat <<'EOF' -**Outcome:** A .NET service exposing a single endpoint `POST /registrations`. The Registration aggregate has a state machine with at minimum `INGEDIEND`. The service orchestrates: start a Flowable process → external task callback executes the ACL `OpenZaak` → zaak URL stored on the aggregate. - -**Acceptance:** - -- BDD scenario: "Given a zorgprofessional submits a registration, when the domain service receives it, then a Flowable process is started and a zaak is opened in OpenZaak." -- Integration test exercises the full path (no real frontend yet). -- The Workflow Client is the only code that calls Flowable. - -**Touches:** `services/domain/`, `services/acl/` (consumed), tests, ADR for external-task job-worker pattern. - -**Out of scope:** any other use case, documents, decisions. -EOF -)" - -create_issue "S-06 · Event Subscriber + Read Projection (minimal)" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:event-subscriber,area:projection" "$(cat <<'EOF' -**Outcome:** An NRC webhook consumer that, on `zaak.gecreeerd`, writes a row to a `register_projection` table with `id`, `bsn`, `naam_placeholder`, `status`. Idempotent. Rebuildable. - -**Acceptance:** - -- BDD scenario: "Given a zaak is created in OpenZaak, when the NRC event is delivered, then the projection contains a row with status INGEDIEND." -- Replaying the same event twice does not create duplicates. -- A `projection rebuild` admin command repopulates from OpenZaak. - -**Touches:** `services/event-subscriber/`, `services/projection-api/`, tests. - -**Out of scope:** decision events, multiple projections, public-safe field filtering (will tighten in S-09). -EOF -)" - -create_issue "S-07 · BFF with one endpoint per portal + OIDC validation" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:bff" "$(cat <<'EOF' -**Outcome:** A .NET BFF exposing four endpoint groups (one per portal). Validates tokens issued by Keycloak. Implements the minimum needed for the walking skeleton: `POST /self-service/registrations`, `GET /openbaar/register?q=...`. - -**Acceptance:** - -- BDD scenarios cover the two endpoints with valid and invalid tokens. -- OpenAPI spec generated and committed. - -**Touches:** `services/bff/`, OpenAPI spec, tests. - -**Out of scope:** behandelaar and beheer endpoints (later slices). -EOF -)" - -create_issue "S-08 · Self-Service portal (Angular, NL DS) — submit a registration" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:portal-self-service" "$(cat <<'EOF' -**Outcome:** The self-service Angular app, in the Nx monorepo, lets a zorgprofessional log in via mock DigiD and submit a registration. NL Design System styling. Generated API client. - -**Acceptance:** - -- E2E test (Playwright): full happy path, login → submit → success page. -- Component tests (Testing Library) for the form. -- Accessibility audit (axe-core) passes WCAG 2.1 AA on the submit page. - -**Touches:** `apps/self-service/`, `libs/ui/`, `libs/auth/`, `libs/api-client/`, tests. - -**Out of scope:** document upload, status tracking page. -EOF -)" - -create_issue "S-09 · Openbaar Register portal — public lookup" \ - "Iteration 1 — Walking Skeleton" "type:slice,area:portal-openbaar,area:projection" "$(cat <<'EOF' -**Outcome:** The openbaar Angular app shows a search box. Anonymous. Queries the BFF's `/openbaar/register` which reads only the projection's **public-safe** fields. Confirms the walking skeleton end-to-end. - -**Acceptance:** - -- E2E test: zorgprofessional registers via self-service (S-08), behandelaar approves via a temporary admin endpoint (no behandel-portal yet), openbaar register shows the entry. -- Public-safe field whitelist enforced and tested. - -**Touches:** `apps/openbaar/`, projection-api hardening, tests. - -**Out of scope:** advanced search filters, sorting. - -_End of walking skeleton. Demo: submit → process → projection → public visibility. All CI gates green on Gitea Actions. Cut release `vYYYY.MM.0` and publish via Gitea Releases._ -EOF -)" - -# ---- Iteration 2 ---------------------------------------------------------- -create_issue "S-10 · Document upload + boundary timer for document timeout (Flow 2)" \ - "Iteration 2 — Flow Completeness" "type:slice,area:workflow,area:portal-self-service" "$(cat <<'EOF' -**Outcome:** BPMN extended with a "wacht op documenten" user task with a 30-day boundary timer. Self-service portal supports diploma upload. On timeout the case is cancelled. - -**Acceptance:** BDD scenarios for both branches; integration tests for the timer firing. -EOF -)" - -create_issue "S-11 · Withdrawal (Flow 3)" \ - "Iteration 2 — Flow Completeness" "type:slice,area:portal-self-service,area:domain,area:workflow" "$(cat <<'EOF' -**Outcome:** Self-service portal has a "trek aanvraag in" action. Domain service issues a withdraw command; BPMN message event correlates; case cancels with audit trail. -EOF -)" - -create_issue "S-12 · Behandel-portal — werkbak + beoordeling" \ - "Iteration 2 — Flow Completeness" "type:slice,area:portal-behandel,area:workflow" "$(cat <<'EOF' -**Outcome:** Behandel portal with login (medewerker realm), werkbak listing INGEDIEND/IN_BEHANDELING cases, claim and complete user tasks via Flowable, decision endpoint via Domain Service. - -**Acceptance:** BDD scenarios for claim, complete, request additional document, decide. -EOF -)" - -create_issue "S-13 · DMN decision: diploma eligibility (Flow 4)" \ - "Iteration 2 — Flow Completeness" "type:slice,area:workflow,area:domain" "$(cat <<'EOF' -**Outcome:** A DMN decision table evaluated by the Domain Service via Workflow Client. Foreign diplomas route to an extra "CBGV-advies" user task in BPMN. - -**Acceptance:** BDD scenarios for domestic and foreign diploma paths; DMN evaluated separately is unit-tested. -EOF -)" - -create_issue "S-14 · Beoordeling escalation (Flow 5)" \ - "Iteration 2 — Flow Completeness" "type:slice,area:workflow" "$(cat <<'EOF' -**Outcome:** Boundary timer on beoordeling user task — 14 days. On timeout, reassigns to a teamlead role. -EOF -)" - -# ---- Iteration 3 ---------------------------------------------------------- -create_issue "S-15 · Beheer-portal — catalogus & default-fill rules" \ - "Iteration 3 — Beheer & Observability" "type:slice,area:portal-beheer,area:acl" "$(cat <<'EOF' -**Outcome:** Beheer portal lets an admin view ZTC catalogi (read-only first), and manage the ACL's default-fill configuration via a CRUD UI. MFA on the medewerker realm enforced. -EOF -)" - -create_issue "S-16 · OpenTelemetry traces + Grafana dashboard" \ - "Iteration 3 — Beheer & Observability" "type:slice,area:infra" "$(cat <<'EOF' -**Outcome:** Traces span portal → BFF → Domain → ACL → OpenZaak and portal → BFF → Domain → Flowable. Grafana dashboards pre-built for golden signals. -EOF -)" - -create_issue "S-17 · Quartz.NET scheduler — herregistratie reminder sweep" \ - "Iteration 3 — Beheer & Observability" "type:slice,area:domain" "$(cat <<'EOF' -**Outcome:** Nightly job that finds entries within 90 days of expiry and emits a domain event. (No outbound notification in v1 — logged.) -EOF -)" - -# ---- Iteration 4 ---------------------------------------------------------- -create_issue "S-18 · Objecten + Objecttypen up in compose; Register objecttype defined" \ - "Iteration 4 — Objecten" "type:slice,area:infra" "$(cat <<'EOF' -**Outcome:** Objecten and Objecttypen running. A `RegisterRecord` objecttype defined with the public-safe schema. -EOF -)" - -create_issue "S-19 · ACL extension: write register-record to Objecten on approval" \ - "Iteration 4 — Objecten" "type:slice,area:acl,area:projection" "$(cat <<'EOF' -**Outcome:** Approval path writes the canonical register record to Objecten, not OpenZaak eigenschappen. Projection now sourced from Objecten events. - -**ADR required:** "Why Objecten holds the register, OpenZaak holds the process." -EOF -)" - -# ---- Iteration 5 ---------------------------------------------------------- -create_issue "S-20 · OpenMetadata module + seed bundle deployed alongside" \ - "Iteration 5 — Data Governance" "type:slice,area:infra" "$(cat <<'EOF' -**Outcome:** OpenMetadata stack runs as a separate compose file (`infra/governance/`). Seed bundle loaded: glossary, classification taxonomy, roles, default DQ tests. -EOF -)" - -create_issue "S-21 · Read-replica ingestion + API ingestion" \ - "Iteration 5 — Data Governance" "type:slice,area:infra" "$(cat <<'EOF' -**Outcome:** Postgres read replicas of domain, Flowable, projection. OpenMetadata ingestion connectors discover schemas. API connector ingests OpenZaak and Objecten via OpenAPI. -EOF -)" - -create_issue "S-22 · Lineage SDK (.NET) + lineage assertions across the personal-data path" \ - "Iteration 5 — Data Governance" "type:slice,area:acl,area:event-subscriber,area:domain" "$(cat <<'EOF' -**Outcome:** A thin .NET package wrapping OpenMetadata's lineage API, published to the **Gitea Packages** registry. ACL, Event Subscriber, and Domain Service call it as personal data flows. Each lineage edge carries purpose and legal basis. - -**ADR required:** "Lineage as a property of code, not docs." -EOF -)" - -create_issue "S-23 · GDPR reporting cookbook" \ - "Iteration 5 — Data Governance" "type:slice,area:docs" "$(cat <<'EOF' -**Outcome:** `docs/gdpr-reporting.md` showing how to answer specific AVG questions using OpenMetadata (data subject request, processing register, lineage trace). -EOF -)" - -# ---- Iteration 6 ---------------------------------------------------------- -create_issue "S-24 · Helm chart (sketch) + Kubernetes manifests for the platform" \ - "Iteration 6 — Production Posture" "type:slice,area:infra" "$(cat <<'EOF' -**Outcome:** A non-deployed-but-reviewable Helm chart and accompanying ADR on production posture. Documents HA, secrets, backup, observability, identity wiring. -EOF -)" - -create_issue "S-25 · Runbook completeness review" \ - "Iteration 6 — Production Posture" "type:slice,area:docs" "$(cat <<'EOF' -**Outcome:** All runbooks complete: startup, seed, common failures, upgrade upstream modules, restore from backup, rotate secrets, Gitea Actions gotchas. -EOF -)" - -say "" -say "Done. Verify counts (no token needed):" -say " curl -s '$REPO_API/labels?limit=100' | jq length # expect 16" -say " curl -s '$REPO_API/milestones?state=all&limit=100' | jq length # expect 7" -say " curl -s '$REPO_API/issues?state=all&type=issues&limit=100' | jq length # expect 26"