Skip to content
Closed
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
64 changes: 49 additions & 15 deletions docs/cli/repos.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,6 @@ fullsend repos migrate <org> --project <gcp-project>

Converge repos to the desired state defined in a manifest. This is the primary command for managing per-repo installations — it handles adding repos to the manifest, provisioning new repos, syncing variable drift, and upgrading scaffold refs.

When the manifest file does not exist and positional repo arguments are
provided, `repos install` bootstraps a new manifest (`version: 1`),
adds the specified repos, and writes the file. The `--forge` flag is
required in this case. This enables a greenfield setup without running
`repos migrate` or manually creating the YAML first.

Runs in three phases:

1. **Manifest add** — repos specified as positional arguments that are not already in the manifest are added (requires `--forge`). Per-repo overrides (`--inference-region`, `--fullsend-ref`, `--mint-url`, `--allowed-remote-resources`) are written to the manifest entry.
Expand Down Expand Up @@ -127,6 +121,17 @@ When repos are specified as positional arguments, only those repos are processed
| `--fullsend-ref` | | Per-repo fullsend workflow ref override |
| `--mint-url` | | Per-repo mint URL override |
| `--allowed-remote-resources` | | Per-repo allowed remote resources override |
| `--gitlab-bot-token` | | GitLab bot PAT for free-tier instances that don't support project access tokens |

### GitLab bot token

For GitLab repos, `repos install` automatically creates a project access token and stores it as the `FULLSEND_FORGE_TOKEN` CI/CD variable. This requires GitLab Premium or Ultimate.

On free-tier or Community Edition instances where project access tokens are not available, pass `--gitlab-bot-token` with a personal access token (PAT) that has `api` scope:

```bash
fullsend repos install group/project --forge gitlab --gitlab-bot-token glpat-xxxxxxxxxxxx
```

### Common workflows

Expand Down Expand Up @@ -154,9 +159,11 @@ Install specific repos:
fullsend repos install acme/api acme/web
```

### Limitations
Add a GitLab repo and install it:

- **GitLab scaffold generation is not yet implemented.** `repos install` only supports GitHub repos. GitLab repos in the manifest will fail with an error during the provision phase. GitLab support for `repos status` and `repos uninstall` (teardown) works normally.
```bash
fullsend repos install group/project --forge gitlab --direct
```

## `repos status`

Expand Down Expand Up @@ -248,20 +255,47 @@ fullsend repos set-default forge.github.mint_url "" # removes the key

### Valid keys

| Key | Description |
|-----|-------------|
| `defaults.allowed_remote_resources` | Default allowed remote resources for all repos |
| `forge.github.url` | GitHub instance URL (defaults to `https://github.com`) |
| `forge.github.mint_url` | Token mint Cloud Run endpoint URL |
| `forge.github.fullsend_ref` | Default fullsend workflow ref for GitHub repos |
| `forge.gitlab.url` | GitLab instance URL |
| Key | Type | Description |
|-----|------|-------------|
| `defaults.allowed_remote_resources` | comma-separated URLs | HTTPS URLs agents may fetch at runtime |
| `forge.github.url` | URL | GitHub instance URL (default: `https://github.com`) |
| `forge.github.mint_url` | URL | Cloud Run endpoint URL for the token mint |
| `forge.github.fullsend_ref` | ref string | Git ref to pin in scaffold workflow YAML |
| `forge.gitlab.url` | URL | GitLab instance URL |
| `forge.gitlab.runner_tags` | comma-separated tags | CI runner tags for routing agent jobs |

### Flags

| Flag | Default | Description |
|------|---------|-------------|
| `-f`, `--manifest` | `repos.yaml` | Path to repos.yaml |

### Examples

Set the GitLab runner tags:

```bash
fullsend repos set-default forge.gitlab.runner_tags fullsend-agent
```

Set multiple runner tags:

```bash
fullsend repos set-default forge.gitlab.runner_tags "fullsend-agent,gpu-runner"
```

Remove runner tags:

```bash
fullsend repos set-default forge.gitlab.runner_tags ""
```

Set the GitLab instance URL:

```bash
fullsend repos set-default forge.gitlab.url https://gitlab.example.com
```

## See also

- [Getting Started](../guides/getting-started/) — Standard per-repo installation
Expand Down
14 changes: 0 additions & 14 deletions docs/guides/getting-started/repo-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,6 @@ WIF infrastructure per repo, installs per-repo (scaffold, variables,
secrets), unenrolls migrated repos from per-org config, and generates
a `repos.yaml` manifest.

### Creating a manifest from scratch

If you do not have an existing per-org installation to migrate from,
`repos install` can bootstrap a new manifest for you. Pass repo names
as positional arguments with `--forge`:

```bash
fullsend repos install acme/api acme/web --forge github --direct
```

This creates `repos.yaml` (or the path given by `-f`) with
`version: 1`, adds the specified repos, and runs the install. The
`--forge` flag is required when no manifest exists.

### Multi-forge manifests

Every repo entry in the manifest must declare its forge (`github` or
Expand Down
1 change: 1 addition & 0 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,7 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
dryRunFiles, dryRunErr := repos.BuildScaffoldFiles(repos.InstallConfig{
Owner: owner,
Repo: repo,
Forge: repos.ForgeGitHub,
Roles: roles,
MintURL: mintDisplay,
InferenceProject: inferenceProject,
Expand Down
94 changes: 76 additions & 18 deletions internal/cli/repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
Expand All @@ -13,6 +12,7 @@ import (
"github.com/fullsend-ai/fullsend/internal/config"
"github.com/fullsend-ai/fullsend/internal/dispatch/gcf"
"github.com/fullsend-ai/fullsend/internal/forge"
gl "github.com/fullsend-ai/fullsend/internal/forge/gitlab"
"github.com/fullsend-ai/fullsend/internal/layers"
"github.com/fullsend-ai/fullsend/internal/mintcore"
"github.com/fullsend-ai/fullsend/internal/repos"
Expand Down Expand Up @@ -402,6 +402,9 @@ type reposInstallConfig struct {
inferenceProjectNumber string
inferenceRegion string

// GitLab-specific
gitlabBotToken string

// Per-repo overrides
fullsendRef string
mintURL string
Expand Down Expand Up @@ -451,6 +454,7 @@ GCP infrastructure (WIF, mint) must be provisioned separately via
cmd.Flags().StringVar(&opts.fullsendRef, "fullsend-ref", "", "per-repo fullsend workflow ref override")
cmd.Flags().StringVar(&opts.mintURL, "mint-url", "", "per-repo mint URL override")
cmd.Flags().StringSliceVar(&opts.allowedRemoteResources, "allowed-remote-resources", nil, "per-repo allowed remote resources override")
cmd.Flags().StringVar(&opts.gitlabBotToken, "gitlab-bot-token", "", "GitLab bot PAT for free-tier instances that don't support project access tokens")

return cmd
}
Expand Down Expand Up @@ -483,24 +487,12 @@ func runReposInstall(ctx context.Context, opts *reposInstallConfig) error {
printer.StepStart("Loading manifest")
manifest, err := repos.LoadManifest(ctx, opts.manifest)
if err != nil {
// Bootstrap an empty manifest when the file does not exist and
// positional repos are provided. The --forge requirement is
// enforced later when repos are added to the manifest.
if len(opts.repoFilter) > 0 &&
!strings.HasPrefix(opts.manifest, "https://") &&
!strings.HasPrefix(opts.manifest, "http://") &&
errors.Is(err, os.ErrNotExist) {
manifest = &repos.Manifest{Version: 1}
printer.StepDone("No manifest found; bootstrapping new manifest")
} else {
return fmt.Errorf("loading manifest: %w", err)
}
} else {
if err := manifest.Validate(); err != nil {
return fmt.Errorf("manifest validation failed: %w", err)
}
printer.StepDone(fmt.Sprintf("Loaded manifest with %d repo entries", len(manifest.Repos)))
return fmt.Errorf("loading manifest: %w", err)
}
if err := manifest.Validate(); err != nil {
return fmt.Errorf("manifest validation failed: %w", err)
}
printer.StepDone(fmt.Sprintf("Loaded manifest with %d repo entries", len(manifest.Repos)))

var clients repos.ForgeClientFactory
if opts.testClient != nil {
Expand Down Expand Up @@ -626,6 +618,9 @@ func runReposInstall(ctx context.Context, opts *reposInstallConfig) error {
return fmt.Errorf("getting repo info: %w", repoErr)
}
commitMsg := "chore: initialize fullsend per-repo installation"
if rc.Forge == repos.ForgeGitLab {
commitMsg += " [skip ci]"
}
prTitle := "chore: initialize fullsend per-repo installation"
prBody := defaultScaffoldPRBody
_, commitErr := layers.CommitScaffoldFiles(ctx, fc.Client, printer, owner, repo,
Expand Down Expand Up @@ -669,6 +664,43 @@ func runReposInstall(ctx context.Context, opts *reposInstallConfig) error {
return err
}

// GitLab post-install: set up bot token and pipeline schedules for
// newly installed GitLab repos.
if !opts.dryRun && len(result.Installed) > 0 {
for _, r := range result.Installed {
rc, ok := manifest.ResolveConfigWithGlobs(r.Owner, r.Repo)
if !ok || rc.Forge != repos.ForgeGitLab {
continue
}
repoFullName := r.Owner + "/" + r.Repo
printer.Blank()
printer.StepStart(fmt.Sprintf("[%s] GitLab post-install setup", repoFullName))

fc, fcErr := clients.ConfigFor(repos.ForgeGitLab)
if fcErr != nil {
printer.StepWarn(fmt.Sprintf("[%s] Could not get GitLab client: %v", repoFullName, fcErr))
continue
}
glClient, _ := fc.Client.(*gl.LiveClient)

targetRepo, repoErr := fc.Client.GetRepo(ctx, r.Owner, r.Repo)
if repoErr != nil {
printer.StepWarn(fmt.Sprintf("[%s] Could not get repo info: %v", repoFullName, repoErr))
continue
}

_, botErr := setupGitLabBotToken(ctx, fc.Client, glClient, printer, r.Owner, r.Repo, opts.gitlabBotToken)
if botErr != nil {
printer.StepWarn(fmt.Sprintf("[%s] Bot token setup failed: %v", repoFullName, botErr))
}

_, schedErr := setupGitLabPipelineSchedules(ctx, fc.Client, glClient, printer, r.Owner, r.Repo, targetRepo.DefaultBranch)
if schedErr != nil {
printer.StepWarn(fmt.Sprintf("[%s] Pipeline schedule setup failed: %v", repoFullName, schedErr))
}
}
}

// Phase 2: converge already-installed repos (variable reconciliation + ref upgrade).
var alreadyInstalled []repos.InstallResult
for _, r := range result.Skipped {
Expand Down Expand Up @@ -963,6 +995,32 @@ func runReposUninstall(ctx context.Context, opts *reposUninstallConfig, repoArgs
printer.StepInfo(fmt.Sprintf(" FAILED: %s/%s — %v", r.Owner, r.Repo, r.Error))
}
}

// GitLab post-uninstall: clean up pipeline schedules and bot tokens.
if !opts.dryRun {
for _, r := range results {
if !r.Success {
continue
}
rc, ok := manifest.ResolveConfigWithGlobs(r.Owner, r.Repo)
if !ok || rc.Forge != repos.ForgeGitLab {
continue
}
repoFullName := r.Owner + "/" + r.Repo
printer.Blank()
printer.StepStart(fmt.Sprintf("[%s] GitLab cleanup", repoFullName))

fc, fcErr := clients.ConfigFor(repos.ForgeGitLab)
if fcErr != nil {
printer.StepWarn(fmt.Sprintf("[%s] Could not get GitLab client: %v", repoFullName, fcErr))
continue
}
glClient, _ := fc.Client.(*gl.LiveClient)

_ = cleanupGitLabPipelineSchedules(ctx, fc.Client, printer, r.Owner, r.Repo)
_ = cleanupGitLabBotToken(ctx, glClient, printer, r.Owner, r.Repo)
}
}
} else {
succeededRepos = concreteRepos
}
Expand Down
Loading
Loading