Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/ADRs/0057-repos-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,13 @@ and implementation details are in the

## Implementation status

`repos install` (batch install with WIF serialization) is being implemented in PR #3033.
Remaining subcommands (`repos init`, `repos status`, `repos sync`,
`repos upgrade`, `repos upgrade-mint`, `repos remove`) are tracked in the
Implemented subcommands:

- `repos init` — PR #3033
- `repos install` — PR #3033
- `repos status` — PR #4079
- `repos add`, `repos remove`, `repos uninstall` — PR #4081

Remaining subcommands (`repos sync`, `repos diff`, `repos upgrade`,
`repos upgrade-mint`) are tracked in the
[repos management plan](../plans/repos-management.md).
99 changes: 91 additions & 8 deletions docs/cli/repos.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ Manage per-repo installations across multiple orgs via a declarative `repos.yaml
| Command | Description |
|---------|-------------|
| `fullsend repos init <org\|owner/repo>` | Generate a repos.yaml manifest by discovering existing installations |
| `fullsend repos install` | Install fullsend on uninstalled manifest repos |
| `fullsend repos install [repos...]` | Install fullsend on uninstalled manifest repos |
| `fullsend repos add <repos...>` | Add repo entries to a repos.yaml manifest |
| `fullsend repos remove <repos...>` | Remove repo entries from a repos.yaml manifest |
| `fullsend repos uninstall <repos...>` | Tear down fullsend from specific repos |
| `fullsend repos status` | Compare manifest against actual repo state |

## `repos init`
Expand Down Expand Up @@ -73,17 +76,18 @@ Runs in three phases:
```bash
fullsend repos install -f repos.yaml
fullsend repos install --dry-run
fullsend repos install --repo acme/api --repo acme/web
fullsend repos install --direct --concurrency 8
fullsend repos install acme/api acme/web
fullsend repos install "acme/*" --direct --concurrency 8
```

When repos are specified as positional arguments, only those repos are installed. Glob patterns (e.g. `acme/*`) are matched against manifest entries. When no repos are specified, all manifest repos are installed.

### Flags

| Flag | Default | Description |
|------|---------|-------------|
| `-f`, `--manifest` | `repos.yaml` | Path or URL to repos.yaml manifest |
| `--dry-run` | `false` | Preview what would be installed without making changes |
| `--repo` | (all) | Install specific repos only (repeatable) |
| `--skip-mint-check` | `false` | Skip mint URL discovery and org registration (EnsureOrgInMint). Use when orgs are already registered in the mint. |
| `--concurrency` | `4` | Max parallel operations (1-32) |
| `--roles` | `triage,coder,review,fix,retro,prioritize` | Agent roles to install |
Expand All @@ -106,7 +110,7 @@ fullsend repos install -f repos.yaml --dry-run
Install specific repos (orgs already registered):

```bash
fullsend repos install --repo acme/api --repo acme/web --skip-mint-check
fullsend repos install acme/api acme/web --skip-mint-check
```

> **Note:** Without `--skip-mint-check`, `repos install` will register any new
Expand All @@ -121,17 +125,17 @@ Read-only comparison of the `repos.yaml` manifest against actual forge state. Re
```bash
fullsend repos status
fullsend repos status -f path/to/repos.yaml
fullsend repos status --repo owner/repo1 --repo owner/repo2
fullsend repos status --json
fullsend repos status --repo acme/api --repo acme/web
fullsend repos status --repo "acme/*" --json
```

### Flags

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--manifest` | `-f` | `repos.yaml` | Path or HTTPS URL to manifest file |
| `--repo` | | | Filter to specific repos (repeatable, supports globs) |
| `--json` | | `false` | Emit JSON output instead of table |
| `--repo` | | | Filter to specific repos (repeatable) |
| `--concurrency` | | `8` | Max parallel API calls |

### Output
Expand All @@ -153,6 +157,85 @@ The command returns a non-zero exit code when any repo has drift, is not install

Requires a GitHub token via `GH_TOKEN`, `GITHUB_TOKEN`, or `gh auth token`.

## `repos add`

Add one or more repo entries to the `repos.yaml` manifest file, editing it in place. Use `--install` to also install fullsend on the added repos after updating the manifest.

```bash
fullsend repos add acme/new-api acme/new-web
fullsend repos add acme/new-api --install --direct
fullsend repos add acme/new-api --dry-run
```

### Flags

| Flag | Default | Description |
|------|---------|-------------|
| `-f`, `--manifest` | `repos.yaml` | Path to repos.yaml manifest |
| `--dry-run` | `false` | Preview what would be added without making changes |
| `--install` | `false` | Also install fullsend on the added repos |
| `--concurrency` | `4` | Max parallel operations (1-32, used with `--install`) |
| `--direct` | `false` | Push scaffold directly to default branch (used with `--install`) |
| `--roles` | default roles | Agent roles to install (used with `--install`) |

Duplicate entries are silently skipped. Glob patterns (e.g. `acme/*`) are allowed as manifest entries.

> **Note:** With `--install`, the manifest is updated before installation begins.
> If installation fails for some repos, those entries remain in the manifest as
> desired state. Run `fullsend repos status` to identify repos that need
> re-installation, then `fullsend repos install <repo>` to retry.

## `repos remove`

Remove one or more repo entries from the `repos.yaml` manifest file, editing it in place. When multiple repos are targeted (via globs or explicit bulk lists), the command prompts for confirmation unless `--yes` is set.

Use `--uninstall` to tear down fullsend from the repos before removing them from the manifest (deletes workflow, variables, secrets, and WIF).

```bash
fullsend repos remove acme/old-api
fullsend repos remove "acme/*" --yes
fullsend repos remove acme/old-api --uninstall
fullsend repos remove acme/old-api --uninstall --skip-wif-cleanup
```

### Flags

| Flag | Default | Description |
|------|---------|-------------|
| `-f`, `--manifest` | `repos.yaml` | Path to repos.yaml manifest |
| `--dry-run` | `false` | Preview what would be removed without making changes |
| `--uninstall` | `false` | Tear down fullsend from repos before removing from manifest |
| `--yes` | `false` | Skip confirmation prompt when multiple repos are targeted |
| `--skip-wif-cleanup` | `false` | Skip GCP WIF provider deletion (only with `--uninstall`) |
| `--concurrency` | `4` | Max parallel operations (1-32, used with `--uninstall`) |

## `repos uninstall`

Tear down fullsend from the specified repos by deleting workflow files, variables, secrets, and WIF infrastructure. Does **not** modify `repos.yaml` — use `repos remove` for that.

When multiple repos are targeted (via globs or explicit bulk lists), the command prompts for confirmation unless `--yes` is set.

Runs in two phases:
1. **Parallel per-repo cleanup** — delete workflow, variables, secrets (concurrent)
2. **Sequential WIF deregistration** — deregister from mint and delete WIF provider

```bash
fullsend repos uninstall acme/old-api
fullsend repos uninstall "acme/*" --yes
fullsend repos uninstall acme/old-api --skip-wif-cleanup
fullsend repos uninstall acme/old-api --dry-run
```

### Flags

| Flag | Default | Description |
|------|---------|-------------|
| `-f`, `--manifest` | `repos.yaml` | Path to repos.yaml manifest |
| `--dry-run` | `false` | Preview what would be uninstalled without making changes |
| `--yes` | `false` | Skip confirmation prompt when multiple repos are targeted |
| `--skip-wif-cleanup` | `false` | Skip GCP WIF provider deletion |
| `--concurrency` | `4` | Max parallel operations (1-32) |

## See also

- [Getting Started](../guides/getting-started/) — Standard per-repo installation
Expand Down
23 changes: 21 additions & 2 deletions docs/guides/dev/cli-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,33 @@ fullsend
│ │ ├── --inference-project <id> # Default GCP project for inference
│ │ ├── --force # Overwrite output file if it exists
│ │ └── --concurrency <int> # Max parallel API calls (default: 8)
│ ├── install # Install fullsend on uninstalled manifest repos
│ ├── install [repos...] # Install fullsend on uninstalled manifest repos
│ │ ├── -f, --manifest <path> # Path or URL to repos.yaml (default: repos.yaml)
│ │ ├── --dry-run # Preview without making changes
│ │ ├── --repo <owner/repo> # Install specific repos only (repeatable)
│ │ ├── --skip-mint-check # Skip org registration in mint
│ │ ├── --concurrency <int> # Max parallel operations (1-32, default: 4)
│ │ ├── --roles <list> # Agent roles (default: triage,coder,review,fix,retro,prioritize)
│ │ └── --direct # Push scaffold to default branch (skip PR)
│ ├── add <repos...> # Add repo entries to manifest
│ │ ├── -f, --manifest <path> # Path to repos.yaml (default: repos.yaml)
│ │ ├── --dry-run # Preview without making changes
│ │ ├── --install # Also install fullsend on the added repos
│ │ ├── --concurrency <int> # Max parallel operations (1-32, default: 4)
│ │ ├── --direct # Push scaffold to default branch (skip PR)
│ │ └── --roles <list> # Agent roles to install (used with --install)
│ ├── remove <repos...> # Remove repo entries from manifest
│ │ ├── -f, --manifest <path> # Path to repos.yaml (default: repos.yaml)
│ │ ├── --dry-run # Preview without making changes
│ │ ├── --uninstall # Tear down fullsend before removing
│ │ ├── --yes # Skip confirmation for glob patterns
│ │ ├── --skip-wif-cleanup # Skip GCP WIF provider deletion
│ │ └── --concurrency <int> # Max parallel operations (1-32, default: 4)
│ ├── uninstall <repos...> # Tear down fullsend from repos
│ │ ├── -f, --manifest <path> # Path to repos.yaml (default: repos.yaml)
│ │ ├── --dry-run # Preview without making changes
│ │ ├── --yes # Skip confirmation for glob patterns
│ │ ├── --skip-wif-cleanup # Skip GCP WIF provider deletion
│ │ └── --concurrency <int> # Max parallel operations (1-32, default: 4)
│ └── status # Compare manifest against actual repo state
├── agent # Manage agent registrations in config
│ ├── add <url-or-path> # Register an agent (URL auto-pinned)
Expand Down
5 changes: 4 additions & 1 deletion docs/guides/getting-started/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ For organizations that separate GCP and GitHub responsibilities across teams, fu
| GCP Admin (Mint) | `fullsend mint status` | Inspect mint state and PEM health |

| Fleet Admin | `fullsend repos init <org\|owner/repo>` | Generate a `repos.yaml` manifest by discovering existing installations |
| Platform Admin | `fullsend repos install -f repos.yaml` | Bulk-install fullsend on repos from a declarative manifest (parallel discovery → sequential WIF → parallel scaffold) |
| Platform Admin | `fullsend repos install [repos...]` | Bulk-install fullsend on repos from a declarative manifest (parallel discovery → sequential WIF → parallel scaffold) |
| Fleet Admin | `fullsend repos add <repos...>` | Add repo entries to `repos.yaml` manifest (with optional `--install`) |
| Fleet Admin | `fullsend repos remove <repos...>` | Remove repo entries from `repos.yaml` manifest (with optional `--uninstall`) |
| Platform Admin | `fullsend repos uninstall <repos...>` | Tear down fullsend from repos (workflow, variables, secrets, WIF) without modifying manifest |
| Fleet Admin | `fullsend repos status` | Compare `repos.yaml` manifest against actual per-repo state (drift detection) |

| Developer | `fullsend agent add <url-or-path>` | Register an agent in config (URL auto-pinned to commit SHA) |
Expand Down
72 changes: 53 additions & 19 deletions docs/plans/repos-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ per-repo installations across multiple orgs.
The work is structured as 8 PRs across two phases. Phase 1 (PRs 1–4)
builds the foundation: extracting reusable install logic, the manifest
parser, a new forge method, and read-only status. Phase 2 (PRs 5–8)
adds write operations: bulk install, sync/diff, upgrade, and remove.
adds write operations: bulk install, sync/diff, upgrade, add/remove/uninstall.

---

Expand Down Expand Up @@ -181,7 +181,7 @@ Installs fullsend on repos not yet installed. Three-phase execution:
Concurrent `repos install` and `fullsend github setup` targeting the
same repo are unsafe — no distributed lock is held.

Supports `--dry-run`, `--repo` (filter), `--concurrency`.
Supports `--dry-run`, positional args (filter, supports globs), `--concurrency`.

#### `fullsend repos diff`

Expand Down Expand Up @@ -244,19 +244,40 @@ is behind the target fullsend ref. The `/health` endpoint must be
extended to include a `version` field (currently only returns
`{"status":"ok"}`).

#### `fullsend repos add`

Adds repo entries to the `repos.yaml` manifest. Supports glob patterns
(e.g. `acme/*`) and validates repo name format (`owner/repo`). Skips
duplicates. With `--install`, also installs fullsend on the added repos.

Supports `--dry-run`, `--install`, `--concurrency`, `--direct`.

#### `fullsend repos remove`

Removes fullsend from specific repos. Requires explicit repo names —
no glob expansion, to prevent accidental bulk deletion.
Removes repo entries from the `repos.yaml` manifest. Glob patterns are
matched against manifest entries and prompt for confirmation unless
`--yes` is set.

With `--uninstall`, tears down fullsend from the matched repos before
removing them from the manifest (deletes workflow, variables, secrets,
and WIF infrastructure).

Supports `--dry-run`, `--uninstall`, `--yes`, `--skip-wif-cleanup`,
`--concurrency`.

#### `fullsend repos uninstall`

Tears down fullsend from specific repos without modifying the manifest.
Glob patterns are matched against manifest entries.

For each repo: deletes workflow file, variables, secrets, deregisters
from mint's `PER_REPO_WIF_REPOS` (sequential), deletes WIF provider.

Does **not** remove repos from the manifest (operator edits manually).
Does **not** remove `.fullsend/` — it contains user-authored config
that may be version-controlled independently.
Does **not** remove repos from the manifest — use `repos remove` for
that. Does **not** remove `.fullsend/` — it contains user-authored
config that may be version-controlled independently.

Supports `--dry-run`, `--skip-wif-cleanup`, `--concurrency`.
Supports `--dry-run`, `--yes`, `--skip-wif-cleanup`, `--concurrency`.

### Version management

Expand Down Expand Up @@ -314,7 +335,7 @@ should be proposed in its own ADR when pursued.
PRs 1, 2, 3 ─────────> PR 5 (repos install)
PRs 2, 3 ────> PR 4 (repos status) ──┬──> PR 6 (sync/diff)
└──> PR 7 (upgrade)
PRs 1, 3 ─────────> PR 8 (remove)
PRs 1, 3 ─────────> PR 8 (add/remove/uninstall)
```

PRs 1, 2, 3 are independent and can be developed in parallel.
Expand All @@ -326,7 +347,8 @@ PR 7 depends on PR 4 (reuses `extractWorkflowRef()` for reading
current refs from workflow files).
PR 8 depends on PRs 1 and 3 (reuses install types +
`DeleteRepoVariable`/`DeleteRepoSecret`) and can be developed in
parallel with PRs 4–7.
parallel with PRs 4–7. Implements three commands: `repos add`,
`repos remove`, and `repos uninstall`.

The `repos init` command is covered by a
[separate implementation plan](repos-init.md) and can be developed
Expand Down Expand Up @@ -845,7 +867,7 @@ Flags:

- `--manifest` / `-f` (string, default `repos.yaml`): path or URL.
- `--dry-run` (bool).
- `--repo` (string, repeatable): install specific repos only.
- Positional args: install specific repos only (supports globs).
- `--skip-app-setup` (bool).
- `--skip-mint-check` (bool).
- `--concurrency` (int, default 4): max parallel scaffold writes.
Expand Down Expand Up @@ -1156,9 +1178,9 @@ tests. Mint compatibility tested with a fake HTTP server.

---

### PR 8: `fullsend repos remove` (uninstall)
### PR 8: repos management (add, remove, uninstall)

**Scope:** New CLI command. Deletes infrastructure.
**Scope:** Three new CLI commands for managing per-repo installations.

**Depends on:** PR 1 (reuses types), PR 3 (`DeleteRepoVariable`,
`DeleteRepoSecret`).
Expand All @@ -1167,21 +1189,33 @@ Can be developed in parallel with PRs 4–7.

#### `internal/cli/repos.go` (modify)

Add `newReposRemoveCmd()`.
Add `newReposAddCmd()`, `newReposRemoveCmd()`, `newReposUninstallCmd()`.

Flags:
`repos add` — adds repo entries to the manifest:
- Positional args: repos to add (supports globs).
- `--manifest` / `-f`.
- `--dry-run`.
- `--install`: also install fullsend on added repos.
- `--concurrency`, `--direct` (used with `--install`).

`repos remove` — removes repo entries from the manifest:
- Positional args: repos to remove (supports globs).
- `--manifest` / `-f`.
- `--dry-run`.
- `--uninstall`: tear down fullsend before removing from manifest.
- `--yes`: skip confirmation for glob patterns.
- `--skip-wif-cleanup`, `--concurrency` (used with `--uninstall`).

`repos uninstall` — tears down fullsend without modifying manifest:
- Positional args: repos to uninstall (supports globs).
- `--manifest` / `-f`: used to resolve mint config for WIF cleanup.
- `--repo` (repeatable, **required**): repos to remove.
- `--dry-run`.
- `--yes`: skip confirmation for glob patterns.
- `--skip-wif-cleanup`: skip GCP WIF provider deletion and mint
deregistration.
- `--concurrency` (int, default 4): max parallel Phase 1 cleanup
operations.

No glob expansion. `--repo` requires exact `owner/repo` values to
prevent accidental bulk removal.

#### `internal/repos/remove.go` (new)

```go
Expand Down
12 changes: 12 additions & 0 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,18 @@ func (a *gcfProvisionerAdapter) DeletePerRepoWIF(ctx context.Context, repo strin
return a.provisioner.RemoveRepoFromMint(ctx, repo)
}

func (a *gcfProvisionerAdapter) DeleteWIFProvider(ctx context.Context, repo string) error {
if a.provisioner == nil {
return fmt.Errorf("WIF provisioner not configured")
}
parts := strings.SplitN(repo, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("invalid repo format %q: expected owner/repo", repo)
}
providerID := mintcore.BuildRepoProviderID(strings.ToLower(parts[0]), strings.ToLower(parts[1]))
return a.provisioner.DeleteWIFProvider(ctx, providerID)
}

// applyPerRepoScaffold commits scaffold files to the repo's default branch
// and configures the repository variables and secrets needed for fullsend.
func applyPerRepoScaffold(ctx context.Context, client forge.Client, printer *ui.Printer,
Expand Down
Loading
Loading