featureops

package
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package featureops performs the operations on features that need git or the network: creating a feature's worktree, removing it, and publishing it as a pull request (Task 20341). The model — layout, records, discovery — is in pkg/feature, which runs nothing and is what the dashboard reads.

Every function here is called from the `cloop feature` CLI, which the hub dispatches through the project's executor rather than running itself. So the git that runs is the git of wherever the project runs: the hub host for a local project, the sandbox for an isolated one — with that sandbox's credentials and nothing else.

Index

Constants

View Source
const EnvAPIURL = "CLOOP_GITHUB_API_URL"

EnvAPIURL overrides the forge API endpoint (GitHub Enterprise, tests).

View Source
const EnvToken = "CLOOP_GITHUB_TOKEN"

EnvToken names the variable a caller — the hub, dispatching this for a project it can mint a token for — uses to hand this process a token for the forge's API. It is read before the ambient GITHUB_TOKEN so that a deliberate hand-over is never shadowed by whatever the environment holds.

Variables

View Source
var ErrExists = errors.New("feature already exists")

ErrExists is returned when the feature's directory or branch is taken.

View Source
var ErrNested = errors.New("a feature cannot have features of its own — create it on the project instead")

ErrNested is returned when a feature is asked for inside a feature.

View Source
var ErrNoToken = errors.New("no GitHub token was found to open the pull request with")

ErrNoToken is returned when no credential for the forge's API was found.

View Source
var ErrNothingToPropose = errors.New("the feature has no commits that its base branch does not already have — there is nothing to propose yet")

ErrNothingToPropose is returned when a feature's branch has no commits its base does not already have. GitHub refuses such a pull request too, but with a message about "no commits between" two refs the user never named.

Functions

func APIBaseURL

func APIBaseURL(host string) string

APIBaseURL returns the REST endpoint for a forge host: api.github.com for github.com, and the GitHub Enterprise convention otherwise.

func Create

func Create(ctx context.Context, opts CreateOptions) (*feature.Info, error)

Create makes a new feature: a worktree of the project's repository on a new branch, locked against pruning, with its own cloop state seeded from the parent's settings.

On failure after the worktree exists, everything this call created is torn down again — the worktree, the branch, the exclude entry is left (it is harmless and shared) — so a failed create never leaves a half-feature that the next attempt trips over.

func DefaultBase

func DefaultBase(ctx context.Context, dir string) (string, error)

DefaultBase picks the branch a new feature is cut from and its pull request targets when the caller names none: the remote's default branch, then main, then master, then whatever the project has checked out.

The remote's default comes first because the pull request is opened against the forge, and the forge's idea of "main" is the one that matters there. A clone whose origin/HEAD was never set falls through to the conventional names.

func Instructions

func Instructions(m *feature.Meta, parentName string) string

Instructions is the note every feature's harness is given about where it is working. It is part of the feature's instructions — visible and editable on its Overview — rather than injected invisibly, so a person reading what the agent was told sees the same thing the agent did.

func RefreshPR

func RefreshPR(ctx context.Context, dir string, token, apiURL string) (*feature.PR, error)

RefreshPR re-reads a feature's pull request from the forge and records its current state — open, closed or merged — so the dashboard can say so.

func ResolveToken

func ResolveToken(ctx context.Context, dir string, remote Remote, explicit, apiOverride string) (token, source string)

ResolveToken finds a token for the forge's API, and names where it came from. The order runs from most to least specific to this project:

  1. explicit — the --token flag;
  2. GITHUB_TOKEN and GH_TOKEN — what a grant or a CI job exports;
  3. github.token in the feature's config;
  4. git's own credential helpers for this repository — the host's credential store, or the scoped helper a secret grant installs;
  5. the gh CLI's login for the host;
  6. CLOOP_GITHUB_TOKEN — the hub's own token, handed over as a last resort, so that a project's credentials are always preferred to the operator's.

Sources 2, 3 and 6 are only consulted for a host a GitHub token may be sent to (see tokenMayReach). Only the source is ever reported; the token itself stays in this process.

func WithHubEnv added in v0.0.5

func WithHubEnv(ctx context.Context, env HubEnv) context.Context

WithHubEnv makes every operation in this package run under ctx in hub mode (Task 20367): the hub is managing a feature of a project that runs on an isolating executor, so the git that runs is the hub's own, on the hub's copy of the repository.

Two things change, and both are about authority. Git runs with env rather than the process's environment, so nothing configured for the hub's own account — a credential helper, an insteadOf rewrite, a hook directory — applies to a tenant's repository, and the repository's own hooks and program-valued settings are switched off (executor.HardenedGitConfig). And the API token comes only from the caller: the hub's credential helpers, its gh login and its environment belong to the operator, not to the project whose pull request is being opened.

Types

type CreateOptions

type CreateOptions struct {
	// ProjectDir is the parent project: the top level of a git work tree
	// that is not itself a feature. Must be absolute.
	ProjectDir string
	// Name is the human name. The slug is derived from it unless Slug is set.
	Name string
	Slug string
	// Description is the feature's goal. Empty uses Name.
	Description string
	// Base is the branch to cut the feature from and to target with its pull
	// request. Empty picks DefaultBase.
	Base string

	// Run settings, independent of the parent's.
	AutoEvolve  bool
	Innovate    bool
	Parallel    bool
	MaxParallel int

	// Tasks seeds the feature's plan, one title per task, in order. Empty
	// leaves the plan empty, and the first run decomposes the goal.
	Tasks []string

	// AutoPR asks the hub to open the pull request when the plan completes.
	AutoPR bool

	CreatedBy string
	// Now is the clock; nil uses time.Now.
	Now func() time.Time
}

CreateOptions describes a new feature.

type DirtyError

type DirtyError struct {
	Slug  string
	Files []string
}

DirtyError is returned when removing a feature would discard work that was never committed.

func (*DirtyError) Error

func (e *DirtyError) Error() string

type GitStatus

type GitStatus struct {
	// Head is the commit the feature's branch points at.
	Head string `json:"head,omitempty"`
	// Ahead counts commits on the feature that its base does not have — what
	// a pull request would carry. Behind counts the reverse.
	Ahead  int `json:"ahead"`
	Behind int `json:"behind"`
	// Dirty counts uncommitted changes in the worktree.
	Dirty int `json:"dirty"`
	// OnBranch reports that the worktree still has the feature's branch
	// checked out.
	OnBranch bool `json:"on_branch"`
	// Error explains a status that could not be determined.
	Error string `json:"error,omitempty"`
}

GitStatus is where a feature's branch stands relative to its base.

func Status

func Status(ctx context.Context, info feature.Info) GitStatus

Status reports a feature's branch position. It never fails outright: a listing of several features should not be lost to one broken worktree, so what could not be read is described in Error instead.

type HubEnv added in v0.0.5

type HubEnv func(extra ...[2]string) []string

HubEnv renders the closed environment git runs with when the hub itself performs a feature operation, with extra configuration pairs folded into the one configuration block it carries (see executor.GitEnv for why there can only be one).

type PROptions

type PROptions struct {
	FeatureDir string
	// Title and Body override the generated ones.
	Title string
	Body  string
	// Base overrides the feature's recorded base branch.
	Base  string
	Draft bool
	// Remote is the git remote to push to and to derive the repository from.
	// Empty means origin.
	Remote string
	// NoPush skips the push, for a branch that is already published.
	NoPush bool
	// Token is an explicit API token; empty resolves one (see ResolveToken).
	Token string
	// Repo overrides the owner/name derived from the remote.
	Repo string
	// APIURL overrides the forge API base URL.
	APIURL string
	// Now is the clock; nil uses time.Now.
	Now func() time.Time
}

PROptions describes a pull request to open for a feature.

type PRResult

type PRResult struct {
	PR *feature.PR `json:"pr"`
	// Existing reports that the pull request was already open and was
	// returned rather than created.
	Existing bool `json:"existing"`
	Pushed   bool `json:"pushed"`
	// Ahead is how many commits the pull request carries.
	Ahead int `json:"ahead"`
	// Dirty lists uncommitted changes, which the pull request does not
	// contain. Reported rather than refused: the committed work is still
	// worth proposing, and the caller says so to the user.
	Dirty []string `json:"dirty,omitempty"`
	// TokenSource names where the API token came from, never the token.
	TokenSource string `json:"token_source"`
}

PRResult reports what OpenPR did.

func OpenPR

func OpenPR(ctx context.Context, opts PROptions) (*PRResult, error)

OpenPR publishes a feature: it pushes the feature's branch and opens a pull request from it into the feature's base branch, recording the result in the feature's record.

It is idempotent. Asked again for a feature whose pull request is still open, it pushes whatever was committed since — which updates that pull request — and returns it rather than failing on GitHub's "already exists".

type Remote

type Remote struct {
	// Host is the forge's host name, e.g. github.com.
	Host string
	// Repo is owner/name.
	Repo string
	// HTTPS reports that the remote is reached over https, which is when a
	// token can authenticate the push itself.
	HTTPS bool
}

Remote is a parsed git remote on a forge.

func ParseRemote

func ParseRemote(raw string) (Remote, error)

ParseRemote extracts the forge host and owner/name from a git remote URL: https://host/owner/name(.git), ssh://[user@]host[:port]/owner/name(.git) and the scp-like [user@]host:owner/name(.git).

type RemoveOptions

type RemoveOptions struct {
	ProjectDir string
	Slug       string
	// DeleteBranch also deletes the feature's branch — only when git
	// considers it merged, unless Force.
	DeleteBranch bool
	// Force discards uncommitted changes, and with DeleteBranch deletes an
	// unmerged branch.
	Force bool
}

RemoveOptions describes a feature to remove.

type RemoveResult

type RemoveResult struct {
	Path          string `json:"path"`
	Branch        string `json:"branch"`
	BranchDeleted bool   `json:"branch_deleted"`
	// BranchKept explains why a requested branch deletion did not happen.
	BranchKept string `json:"branch_kept,omitempty"`
}

RemoveResult reports what Remove did.

func Remove

func Remove(ctx context.Context, opts RemoveOptions) (*RemoveResult, error)

Remove deletes a feature's worktree — and with it the feature's task list and history — and optionally its branch.

The branch is kept by default, because it is the only copy of any commit that was never pushed or merged; removing a feature is a statement about the dashboard, not about the work. When asked to delete it, `git branch -d` decides whether it is merged, so being wrong about that fails the deletion instead of losing commits. A pull request merged by squash is not "merged" by that test; Force is the explicit way past it.

The caller is responsible for making sure no run is executing in the feature: a worktree removed under a running harness takes its working directory away mid-task.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL