|
| 1 | +# Registry IssueOps — operator runbook |
| 2 | + |
| 3 | +This repo runs a GitHub IssueOps pipeline for the on-chain Stellar Registry. It mirrors the pattern used by [SCF Public Goods Maintenance](https://github.com/SCF-Public-Goods-Maintenance/scf-public-goods-maintenance.github.io): structured issue forms drive a label-based state machine, a pilot team votes via reactions, and an accepted intake fires a GitHub-environment-gated workflow that signs the on-chain transaction with a CI key. |
| 4 | + |
| 5 | +## What lives where |
| 6 | + |
| 7 | +| File | What it does | |
| 8 | +|---|---| |
| 9 | +| `.github/ISSUE_TEMPLATE/registry-{publish,register,deploy,admin}.yml` | Four issue forms — one per registry method family. The form sets the `registry-intake` parent label and a `registry-intake:<kind>` subtype. | |
| 10 | +| `.github/validator/config.yml` + `validate-*.js` | Field-level validation (Stellar strkeys, semver, wasm hashes, contract names, https URLs). | |
| 11 | +| `.github/registry-quorum.yml` | Per-kind quorum policy: pilot team slug, `min_voters`, `require_unanimous`. Edit via PR. | |
| 12 | +| `.github/workflows/validate-registry-intake.yml` | Runs on issue open/edit/reopen. Parses + validates the form; on success labels `:in-review`, on failure labels `issueops:validation-error` and posts a friendly comment. | |
| 13 | +| `.github/workflows/registry-quorum-check.yml` | Runs on `.quorum-check` comments. Tallies pilot 👍/👎, applies the policy, dispatches the on-chain submit workflow on accept (or closes the issue on reject). | |
| 14 | +| `.github/workflows/registry-onchain-submit.yml` | `workflow_dispatch` only. Re-fetches and re-validates the issue body, parses to CLI args, runs `stellar-registry-cli` from a protected GitHub Environment with the CI signer secret. | |
| 15 | +| `.github/scripts/parse_intake_to_args.py` | Single source of truth for the kind → CLI subcommand → flags mapping. Edit via PR. | |
| 16 | +| `.github/scripts/render_validation_failure.py` | Pretty-prints validator errors into an issue comment. | |
| 17 | + |
| 18 | +## Architecture at a glance |
| 19 | + |
| 20 | +``` |
| 21 | +issue-form-submit ─▶ validate-registry-intake.yml ─▶ :in-review |
| 22 | + │ │ |
| 23 | + │ └─ on fail ▶ issueops:validation-error + comment |
| 24 | + ▼ |
| 25 | +pilots react 👍/👎 |
| 26 | +.quorum-check comment ─▶ registry-quorum-check.yml |
| 27 | + │ │ |
| 28 | + │ ┌──── reject path ────┐ |
| 29 | + │ ▼ │ |
| 30 | + │ :rejected + close+lock │ |
| 31 | + │ │ |
| 32 | + └────── accept path ─────────────────┘ |
| 33 | + │ |
| 34 | + ▼ |
| 35 | + :accepted + dispatch |
| 36 | + │ |
| 37 | + ▼ |
| 38 | + registry-onchain-submit.yml (workflow_dispatch) |
| 39 | + │ |
| 40 | + ─────────┴───────── GitHub Environment gate (required reviewers) |
| 41 | + │ |
| 42 | + ▼ |
| 43 | + stellar-registry-cli <method> --source ci ... |
| 44 | + │ |
| 45 | + ▼ |
| 46 | + :submitted + tx hash comment ─OR─ :submission-failed |
| 47 | +``` |
| 48 | + |
| 49 | +The CI key is set as the registry contract's **manager** (see `contracts/registry/src/lib.rs:80-82`). When a manager is set, `publish`, `register_contract`, `deploy`, `update_*`, `rename_contract`, and `flag_contract` all accept manager auth in lieu of the original author/admin/owner. That is the contract-side hook that lets one CI key cover all of v1. |
| 50 | + |
| 51 | +## Bootstrapping (one-time per network) |
| 52 | + |
| 53 | +Do these in order. All steps that touch the registry contract are signed locally by the registry **admin**, not by CI. |
| 54 | + |
| 55 | +### 1. Create the pilots team |
| 56 | + |
| 57 | +In the GitHub org that owns this repo, create a team named `registry-pilots`. Add at least 3 members (mirroring `defaults.min_voters` in `.github/registry-quorum.yml`). Members of this team are the only accounts whose 👍/👎 reactions count toward quorum, and only members may run `.quorum-check`. |
| 58 | + |
| 59 | +### 2. Provision the org-read PAT |
| 60 | + |
| 61 | +The `registry-quorum-check.yml` workflow needs to enumerate `registry-pilots` members. The default `GITHUB_TOKEN` cannot read org teams, so create a fine-grained PAT: |
| 62 | + |
| 63 | +- Resource owner: the org |
| 64 | +- Repository access: this repo only |
| 65 | +- Organization permissions: **Members → Read** |
| 66 | + |
| 67 | +Store as repo secret `READ_ORG_MEMBERS_PAT`. Mirrors pg-atlas's setup. |
| 68 | + |
| 69 | +### 3. Generate the CI key |
| 70 | + |
| 71 | +Offline (developer workstation), per network: |
| 72 | + |
| 73 | +```sh |
| 74 | +stellar keys generate ci-publisher # testnet auto-funds; for mainnet, fund out-of-band |
| 75 | +stellar keys public-key ci-publisher # → CI_PUBKEY |
| 76 | +stellar keys show ci-publisher # → CI_SECRET (handle with care) |
| 77 | +``` |
| 78 | + |
| 79 | +Treat the mainnet secret as you would any production credential — keep the offline copy in a password manager / hardware wallet only, and ensure no one can recover it from `STELLAR_SECRET_KEY` once it lands in GitHub. |
| 80 | + |
| 81 | +### 4. Set the CI key as the registry manager |
| 82 | + |
| 83 | +The registry **admin** (whoever holds the admin keypair from registry deployment) runs once per network: |
| 84 | + |
| 85 | +```sh |
| 86 | +stellar contract invoke \ |
| 87 | + --id "$STELLAR_REGISTRY_CONTRACT_ID" \ |
| 88 | + --source admin \ |
| 89 | + --network <testnet|mainnet> \ |
| 90 | + -- set_manager --new_manager "$CI_PUBKEY" |
| 91 | +``` |
| 92 | + |
| 93 | +Verify: |
| 94 | + |
| 95 | +```sh |
| 96 | +stellar contract invoke --id "$STELLAR_REGISTRY_CONTRACT_ID" --network <…> -- manager |
| 97 | +# → "$CI_PUBKEY" |
| 98 | +``` |
| 99 | + |
| 100 | +### 5. Create GitHub Environments |
| 101 | + |
| 102 | +In repo settings → Environments, create two: |
| 103 | + |
| 104 | +- `registry-testnet` |
| 105 | +- `registry-mainnet` |
| 106 | + |
| 107 | +For each, attach **required reviewers** (recommend the same set as `registry-pilots` for testnet; a stricter ops subset for mainnet). The env reviewer prompt is the second gate after the pilot quorum vote. |
| 108 | + |
| 109 | +For each environment set: |
| 110 | + |
| 111 | +| Kind | Name | Value | |
| 112 | +|---|---|---| |
| 113 | +| Secret | `STELLAR_SECRET_KEY` | The CI key's secret seed (`SC…`). | |
| 114 | +| Secret | `STELLAR_NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` (testnet) or `Public Global Stellar Network ; September 2015` (mainnet). | |
| 115 | +| Variable | `STELLAR_RPC_URL` | The RPC endpoint for that network. | |
| 116 | +| Variable | `STELLAR_REGISTRY_CONTRACT_ID` | The registry contract's `C…` address on that network. | |
| 117 | + |
| 118 | +## Day-to-day usage |
| 119 | + |
| 120 | +### Submitter workflow |
| 121 | + |
| 122 | +1. Click **New issue** in this repo. Pick one of the four "Registry: …" forms. |
| 123 | +2. Fill in the fields. The form's `validations: required: true` blocks empty submission for must-have fields; the workflow re-validates kind-specific cross-field rules (e.g. `update_contract_owner` requires `new_owner`). |
| 124 | +3. On submit, the validator workflow runs. If it labels you `issueops:validation-error`, fix the fields and edit the issue — the workflow re-runs on edit. |
| 125 | +4. Once labeled `registry-intake:in-review`, wait for pilot votes. |
| 126 | + |
| 127 | +### Pilot workflow |
| 128 | + |
| 129 | +1. Read the issue, including the **Justification** section. |
| 130 | +2. React with 👍 to approve or 👎 to reject. (One reaction of each kind per pilot — the API enforces this.) |
| 131 | +3. When you believe the quorum threshold has been reached, comment exactly `.quorum-check`. Only `registry-pilots` members can trigger this; the action checks via the org PAT. |
| 132 | +4. The workflow tallies according to `.github/registry-quorum.yml`: |
| 133 | + - **Default formula:** `accepted iff ups ≥ (min_voters + 2 × downs)`. So 3 ups beats 0 downs; 5 ups beats 1 down. |
| 134 | + - **`require_unanimous: true`** (admin kind): any 👎 blocks acceptance regardless of 👍 count. |
| 135 | +5. On accept, the on-chain submit workflow is dispatched. It will pause at the env gate; an environment reviewer must click **Approve and deploy** before the CI key signs. |
| 136 | +6. On reject, the issue is closed and locked. |
| 137 | + |
| 138 | +### Where to find the result |
| 139 | + |
| 140 | +A successful submission posts a comment on the issue with the tx hash, links to the workflow run, and applies the `registry-intake:submitted` label. A failed submission applies `registry-intake:submission-failed` and links to the run logs; a maintainer can re-trigger via the workflow_dispatch UI after fixing the underlying problem. |
| 141 | + |
| 142 | +## Key rotation |
| 143 | + |
| 144 | +``` |
| 145 | +Generate new CI key offline |
| 146 | + ↓ |
| 147 | +admin: set_manager(<NEW_PUBKEY>) |
| 148 | + ↓ |
| 149 | +Update each Environment's STELLAR_SECRET_KEY secret to the new SC… |
| 150 | + ↓ |
| 151 | +(Optional, hardens the gap) admin: remove_manager + set_manager(<NEW_PUBKEY>) |
| 152 | + ↓ |
| 153 | +Old key now has no on-chain authority; can be retired |
| 154 | +``` |
| 155 | + |
| 156 | +There is intentionally no "rotate from CI" path — rotation is an admin-only ceremony. |
| 157 | + |
| 158 | +## Out of scope for v1 (offline-only, admin-signed) |
| 159 | + |
| 160 | +The following operations require **registry admin** auth, not the manager / CI key. They are not exposed as IssueOps forms. To perform them, sign locally with the registry admin keypair. |
| 161 | + |
| 162 | +| Operation | Why offline | |
| 163 | +|---|---| |
| 164 | +| `set_admin` | Changing who can change the manager. Highest blast radius. | |
| 165 | +| `set_manager` | Including initial bootstrap and key rotation. Admin-signed by design. | |
| 166 | +| `remove_manager` | Same. | |
| 167 | +| `upgrade` (registry self-upgrade) | Replaces the registry contract itself. Out of scope for IssueOps. | |
| 168 | + |
| 169 | +## Future migration: Tansu factory pattern |
| 170 | + |
| 171 | +When per-org contracts land: |
| 172 | + |
| 173 | +1. Deploy the org contract `O` with multi-sig logic via Soroban `__check_auth`. |
| 174 | +2. Admin runs `set_manager(O)`. |
| 175 | +3. Update `registry-onchain-submit.yml`: instead of submitting a tx fully signed by the CI key, submit a tx whose auth entry references `O`. `O.__check_auth` reads quorum signatures from its own bag of signers. |
| 176 | +4. The CI key is retained as one of N quorum signers, or retired entirely if all signers are humans / hardware wallets. |
| 177 | + |
| 178 | +The IssueOps pipeline (forms, validation, pilot vote, env gate) stays unchanged. It now governs **one** of N signatures rather than the sole authority. This is the explicit reason to invest in IssueOps now: the durable shape of human-mediated approvals doesn't change when the contract-side authority model becomes more sophisticated. |
0 commit comments