Skip to content

feat(cli): add luca run command for pipeline execution#81

Merged
albertodebortoli merged 14 commits into
mainfrom
feature/pipelines
May 17, 2026
Merged

feat(cli): add luca run command for pipeline execution#81
albertodebortoli merged 14 commits into
mainfrom
feature/pipelines

Conversation

@albertodebortoli
Copy link
Copy Markdown
Member

@albertodebortoli albertodebortoli commented May 15, 2026

Description

Adds luca run — a new command that loads a YAML pipeline file and executes its tasks sequentially in a shell. This turns Luca into both a tool manager and a pipeline runner, so the same tool that pins your toolchain can also orchestrate the workflows that use it.

Each task runs under /usr/bin/env bash -c "set -eo pipefail && <command>", inheriting stdout, stderr, and stdin from the parent process. Tools used by the pipelines are validated against PATH before execution begins.

Pipeline File Format

A pipeline file is a YAML file with a tasks: list. Environment variables and a default working directory can be set at the pipeline or task level.

---
env:
  LANG: en_US.UTF-8

working-directory: ios/

tasks:
  - name: Select Xcode
    command: sudo xcode-select --switch /Applications/Xcode-26.4.app
    tools: [xcode-select]

  - name: Install tools
    command: luca install

  - name: Generate project
    command: tuist generate

  - name: Run tests
    command: xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16'
    env:
      DEVELOPER_DIR: /Applications/Xcode-26.4.app

  - name: Upload coverage (optional)
    command: xcov report
    continue-on-error: true

Task fields

Field Required Description
name Yes Label shown in the terminal before execution
command Yes Shell command to run
tools No Explicit list of tools to validate; defaults to first token of command
env No Extra environment variables for this task (merged on top of pipeline-level env)
working-directory No Override the pipeline-level working directory for this task
continue-on-error No When true, a non-zero exit logs a warning and execution continues

Pipeline fields

Field Required Description
tasks Yes Ordered list of tasks
env No Environment variables applied to all tasks
working-directory No Default working directory for all tasks (relative to invocation directory)

Usage

Convention-based lookup

luca run <name>

Searches for the pipeline file in order:

  1. ./<name>.yml
  2. ./<name>
  3. ./pipelines/<name>.yml
  4. ./pipelines/<name>
luca run ci          # loads ./ci.yml or ./pipelines/ci.yml
luca run deploy      # loads ./deploy.yml or ./pipelines/deploy.yml

Explicit file path

luca run --file <path>
luca run --file .luca/pipelines/release.yml

Dry run

Validates tool availability and prints all tasks without executing any command:

luca run <name> --dry-run
────────────────────────────────────────────────
[DRY RUN] Pipeline: ci

  Task 1: Select Xcode
    Command: sudo xcode-select --switch /Applications/Xcode-26.4.app
    Tools:   xcode-select ✓

  Task 2: Install tools
    Command: luca install
    Tools:   luca ✓

  Task 3: Generate project
    Command: tuist generate
    Tools:   tuist ✗

No tasks executed.

Normal execution

── Task 1/4: Select Xcode ──────────────────────
Password:
/Applications/Xcode-26.4.app

── Task 2/4: Install tools ─────────────────────
...

── Task 3/4: Generate project ──────────────────
...

── Task 4/4: Run tests ─────────────────────────
...

── Pipeline complete (4 tasks, 142.3s) ─────────

Changes

  • SubprocessRunner: extended with workingDirectory and inheritStdin parameters (standardInput stays nullDevice for all existing install paths)
  • New models: Pipeline, PipelineTask (Codable value types)
  • New core components: PipelineLoader, PipelineValidator, PipelineRunner — each with its own *ing protocol and nested error type
  • New CLI command: RunCommand registered under a new "Pipelines" group
  • Design spec committed to Documentation/specs/

Type of Change

  • Feature
  • Bug fix
  • Maintenance / Refactor
  • Documentation
  • CI / Tooling
  • Other (specify)

How Has This Been Tested?

  • Added / updated unit tests (PipelineLoaderTests, PipelineValidatorTests, PipelineRunnerTests)
  • Manually tested locally with a real pipeline against a Swift project
  • Tested on macOS (arm64)

Checklist

  • Swift code builds locally (swift build)
  • Tests pass locally (swift test)
  • Code style / formatting respected
  • Documentation updated (DocC comments on all new public types)

Breaking Changes?

  • No

…Running protocol

Pipeline tasks need to run in specific directories and may be interactive (e.g.
TTY-aware tools that read stdin). Extend the protocol with these two parameters
and provide backward-compatible convenience overloads so existing callers are
unchanged.
Define the YAML-decodable DSL models for the pipeline executor. PipelineTask
maps continue-on-error and working-directory CodingKeys; Pipeline carries the
top-level task list, shared env, and optional working-directory default.
…to FileManaging

PipelineValidator needs to check executable availability on disk. Follow the
existing fine-grained FileManaging protocol pattern: new narrow protocol, wired
into the umbrella FileManaging composition, and implemented in FileManagerWrapper
and FileManagerWrapperMock.
YAML-based pipeline loader following the SpecLoader pattern. Reads a file URL,
decodes it into Pipeline via YAMLDecoder, and surfaces missingPipeline and
invalidPipeline errors with clean, human-readable descriptions (no raw
NSUnderlyingError leakage).
Pre-flight tool availability check before any task runs. Uses the explicit
tools: list when provided; falls back to the first whitespace-delimited token
of the command string. Absolute-path tools are checked directly; bare names
are searched across PATH directories via isExecutableFile. toolCheckResults(for:)
returns per-task [[ToolCheckResult]] for dry-run display.
Sequential task executor. Wraps each command in /bin/bash -c "set -eo pipefail
&& <cmd>" with stdin inherited so TTY-aware tools work. Merges env in layers
(pipeline-level then task-level). Resolves relative working-directory paths
against the invocation directory. continue-on-error tasks log a warning and
proceed; any other non-zero exit throws taskFailed and halts the pipeline.
New RunCommand under an "Execution" group. Accepts a pipeline name (resolved
via convention: ./name.yml, ./name, ./pipelines/name.yml, ./pipelines/name) or
an explicit --file path. --dry-run runs pre-flight validation and prints per-task
tool availability (✓/✗) without executing anything. Adds the pipelinesFolder
constant to LucaCore.
…PipelineRunner

PipelineLoaderTests: valid/missing/malformed YAML, error description readability.
PipelineValidatorTests: explicit tools list, first-word fallback, absolute paths,
toolCheckResults, error descriptions.
PipelineRunnerTests: single/multi-task execution, stdin inheritance, env merging
(pipeline vs task precedence), working-directory resolution (relative, absolute,
task vs pipeline default), failure propagation, continue-on-error, error descriptions.
- PipelineLoaderError: add Equatable conformance; fold Error into a
  pre-formatted String so synthesis is automatic and round-trip equality
  is predictable (matches SpecLoaderError intent)
- PipelineLoader: add // MARK: - PipelineLoading divider
- PipelineRunner: replace hardcoded /bin/bash with /usr/bin/env bash
  for portability across Linux layouts; align summary line padding
  constant with header width (60)
- PipelineValidating: add toolCheckResults(for:) to the protocol so
  RunCommand depends on the abstraction, not the concrete type
- RunCommand: accept PipelineValidating in printDryRun; add Equatable
  to RunCommandError
- Pipeline/PipelineTask: add explicit public memberwise inits so public
  visibility is unambiguous and external callers don't rely on the
  synthesized internal init
- Tests: fix arguments index for new bash/env invocation shape; add
  test for empty command (validator passes silently) and empty task list
  (runner completes without subprocess calls)
@albertodebortoli albertodebortoli added this to the 0.19.0 milestone May 15, 2026
@albertodebortoli albertodebortoli added the feature New feature or enhancement label May 15, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…ner, and SubprocessRunner

Cover previously missing lines: dataCorrupted and keyNotFound DecodingError paths in
PipelineLoader, the public init in PipelineRunner, and the workingDirectory branch in
SubprocessRunner.
Add fixtures and tests to exercise the non-DecodingError guard path (invalid
UTF-8 data) and the valueNotFound path (null task element in array), reducing
missing lines in PipelineLoader from 3 to 1 (the unreachable @unknown default).
…r path

Use a single 0x80 byte which fails both UTF-8 and UTF-16 decoding, causing
Yams to throw a YamlError (not wrapped in DecodingError). This covers the
guard-else branch in formattedParseError that was previously unreachable
with the old fixture.
Move formattedParseError to an internal PipelineLoader extension so tests
can call it directly. Add test for the .valueNotFound DecodingError case
(line 83) which is unreachable via YAML loading because Yams converts null
scalars to strings. Collapse the @unknown default body to a single-line
break to minimise unreachable code surface.
@albertodebortoli albertodebortoli marked this pull request as ready for review May 17, 2026 19:25
@albertodebortoli albertodebortoli merged commit 268e4f6 into main May 17, 2026
3 checks passed
@albertodebortoli albertodebortoli deleted the feature/pipelines branch May 17, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant