How GitLab Pipeline Jobs Work in no-mistakes Detached-HEAD Worktree Contexts

The no-mistakes daemon bypasses the glab ci get CLI limitation by calling the GitLab REST API directly from detached-HEAD worktrees, parsing job metadata including finished_at timestamps to monitor pipeline status and trigger auto-fix logic.

The no-mistakes tool operates a background daemon that monitors merge requests within isolated detached-HEAD worktrees. When integrating with GitLab CI, the standard CLI tooling expects a branch-based checkout that does not exist in this context, forcing the implementation to use direct REST API calls. This architecture enables continuous pipeline monitoring without modifying the local repository state.

Why Detached-HEAD Worktrees Break Standard GitLab CLI Tools

When running in a detached-HEAD state, the glab ci get command refuses to execute because it requires the current checkout to track a branch reference. This limitation prevents standard GitLab CLI usage within no-mistakes' isolated worktree environments, as the command errors out when no branch is active. The tool must therefore construct API requests manually to fetch pipeline job status while operating from the detached working directory.

Direct GitLab REST API Integration in no-mistakes

The GitLab-specific implementation resides in internal/scm/gitlab/gitlab.go, where the Host type encapsulates all provider interactions and API communication logic.

The Host Type and pipelineJobsArgs Construction

The Host struct maintains a pipelineJobsArgs field that stores the pre-constructed API endpoint path for fetching jobs. This string follows the format required by the glab api subcommand: projects/<url-encoded group%2Fproject>/pipelines/<id>/jobs. During initialization, the host encodes the project path and pipeline ID to build this argument string, which is executed via glab api rather than glab ci.

// internal/scm/gitlab/gitlab.go (excerpt)
type Host struct {
    // …
    pipelineJobsArgs string // “glab api projects/<url‑encoded group%2Fproject>/pipelines/<id>/jobs”
}

Mapping Pipeline Jobs to Internal Check Structures

The GetChecks method executes the constructed API call using exec.CommandContext with the glab binary, then unmarshals the JSON response to extract job metadata. Each job's finished_at field maps to the Check.CompletedAt timestamp, providing the completion time required for auto-fix timing decisions. This transformation feeds directly into the CI step's monitoring loop according to the implementation in internal/scm/gitlab/gitlab.go.

func (h *Host) GetChecks(ctx context.Context, pr *scm.PR) ([]scm.Check, error) {
    // Build the API URL:  glab api projects/<group%2Fproject>/pipelines/<id>/jobs
    apiArgs := h.pipelineJobsArgs // set during Host construction
    out, err := exec.CommandContext(ctx, "glab", "api", apiArgs).CombinedOutput()
    if err != nil {
        return nil, fmt.Errorf("glab api %s: %w – %s", apiArgs, err, out)
    }
    var jobs []gitlabJob // struct matching the JSON fields, incl. finished_at
    if err := json.Unmarshal(out, &jobs); err != nil {
        return nil, err
    }
    checks := make([]scm.Check, 0, len(jobs))
    for _, job := range jobs {
        checks = append(checks, scm.Check{
            Name:        job.Name,
            Status:      job.Status,     // success / failed / running …
            CompletedAt: parseGitLabTime(job.FinishedAt),
        })
    }
    return checks, nil
}

The CI Monitoring Loop Architecture

The main polling logic lives in internal/pipeline/steps/ci.go within the Execute method, which implements a continuous monitoring loop for each merge request. At lines 63-70, the step resolves the PR number and constructs the Host for the detected provider before entering the polling cycle.

Polling PR State and Mergeability

Each iteration checks the PR state via host.GetPRState to detect merges or closures that terminate monitoring early. When the provider supports mergeability queries, host.GetMergeableState verifies whether the merge request can be safely integrated. These checks prevent wasted resources on obsolete or blocked merge requests while the daemon waits for CI completion.

Re-arming Timeouts via Default Branch Resolution

Because detached-HEAD worktrees lack a stable HEAD reference, the daemon explicitly resolves the default branch tip using resolveDefaultBranchTip. When the tip advances beyond the previously cached value (as implemented at lines 24-33 in internal/pipeline/steps/ci.go), the idle timeout re-arms to keep the monitor alive during long-running pull requests. This mechanism compensates for upstream activity without requiring local branch synchronization.

func (s *CIStep) Execute(sctx *pipeline.StepContext) (*pipeline.StepOutcome, error) {
    // … host construction omitted …
    for {
        // 1. Re‑arm timeout if default branch advanced
        tip, resolved := baseBranchTip(ctx)
        if resolved && tip != lastBaseTip { … }

        // 2. Get PR state; exit on merge/close
        state, err := host.GetPRState(ctx, pr)
        // 3. Get mergeable state (optional)
        // 4. Get CI checks (pipeline jobs)
        checks, err := host.GetChecks(ctx, pr)
        // 5. Auto‑fix or wait based on pending/failing checks
        // 6. Sleep until next poll interval
    }
}

Auto-Fix Logic for Failed Jobs

When GetChecks identifies failed pipeline jobs, the step may invoke autoFixCI to attempt resolution. This logic pushes corrective commits while guarded by force-push lease mechanisms that prevent overwriting unseen upstream changes. The auto-fix trigger depends on the finished_at timestamps parsed from the GitLab API response, ensuring fixes only apply after jobs definitively complete.

Key Implementation Files

  • internal/pipeline/steps/ci.go: Contains the main CI monitoring loop, host construction at lines 63-70, PR state polling, timeout re-arming logic at lines 24-33, and auto-fix triggers.

  • internal/scm/gitlab/gitlab.go: Implements the GitLab Host type, constructs pipelineJobsArgs, and provides GetChecks for REST API communication via glab api.

  • internal/pipeline/steps/ci_gitlab_test.go: Unit tests validating GitLab-specific CI behaviors including pass/merge detection, failure handling, auto-fix sequences, and pending-checks logic.

Summary

  • The no-mistakes daemon avoids glab ci get because the command fails in detached-HEAD states that lack branch references.
  • Direct GitLab REST API calls via glab api fetch pipeline job lists using pre-constructed endpoint arguments stored in pipelineJobsArgs.
  • Job metadata maps to internal Check structures, with finished_at timestamps driving completion detection and auto-fix timing.
  • Default branch tip resolution re-arms idle timeouts without requiring local branch checkouts, preventing premature termination.
  • Failed jobs trigger autoFixCI attempts protected by force-push lease guards to avoid discarding upstream commits.

Frequently Asked Questions

Why can't glab ci get run in a detached HEAD state?

The glab ci get command requires the repository to be on a named branch to determine repository context, and exits with an error when executed from a detached HEAD where no branch reference exists. This design assumption prevents standard CLI usage in no-mistakes' isolated worktrees, necessitating the direct REST API approach implemented in internal/scm/gitlab/gitlab.go.

How does no-mistakes parse GitLab job completion times?

The GetChecks implementation in internal/scm/gitlab/gitlab.go calls the GitLab REST API endpoint defined in pipelineJobsArgs, unmarshals the JSON response into job structs, and maps each job's finished_at field to Check.CompletedAt. This timestamp enables the daemon to determine precisely when jobs finish and whether to trigger auto-fix logic.

What triggers the idle timeout re-arming mechanism?

When resolveDefaultBranchTip detects that the default branch has advanced beyond the previously cached tip (as checked in the polling loop at internal/pipeline/steps/ci.go lines 24-33), the daemon re-arms the idle timeout to prevent premature termination during long-running pull requests. This check compensates for the lack of local branch tracking in detached-HEAD contexts.

How does auto-fix handle failed pipeline jobs?

When the polling loop identifies failed checks via GetChecks, it invokes autoFixCI to generate and push a corrective commit, guarded by force-push lease logic that aborts if upstream changes would be overwritten. This process repeats until the pipeline succeeds, the merge request closes, or the idle timeout expires.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →