-
Notifications
You must be signed in to change notification settings - Fork 0
Pipeline Design 204
ADR written to .claude/pipeline-artifacts/design.md.
Summary of the architecture decision:
-
Chosen approach: Add
quickstartsubcommand to the existingsw-ci.shrouter, withshipwright init --cias a 3-line delegation insw-init.sh -
New file:
scripts/lib/ci-quickstart.sh— pure functions for detection, YAML generation, and README section output -
5 modified files:
sw-ci.sh(routing + orchestrator),sw-init.sh(--ci flag),sw-doctor.sh(--ci validation), plus test files -
Key decisions: library/orchestrator separation for testability,
fast+standardas defaults, skip-existing-files safety, atomic writes, and post-write validation via existingcmd_validate() -
Zero new dependencies — reuses
pipeline-detection.sh,helpers.sh, andtemplates/pipelines/*.json -
15 validation criteria covering happy path, error cases, edge cases, Bash 3.2 compat, and air-gapped mode
ine-detection.sh
—detect_repo_environments_json()returns a JSON array of{id, marker}objects anddefault_test_cmd_for_environment()` maps env IDs to test commands - Pipeline templates live in
templates/pipelines/*.jsonwith astages[]array of{id, enabled, gate, config}objects - The existing
scripts/sw-ci.shalready handles workflow generation (cmd_generate), validation (cmd_validate), and nine other CI subcommands via a case-statement router (line 566) -
scripts/sw-init.shuses awhile/caseflag-parsing loop and delegates to other scripts (e.g., doctor at the end) -
scripts/sw-doctor.shusescheck_pass/check_warn/check_failcounters with sectioned validation - Air-gapped mode (
NO_GITHUB=1) must be respected — noghAPI calls when set
Approach B: Add quickstart subcommand to sw-ci.sh, with shipwright init --ci as a thin delegation.
-
CI logic stays in the CI module.
sw-ci.shalready owns workflow generation, validation, badges, and secrets management. Quickstart is a natural extension — it composes existing primitives (detect_repo_environments_json, template JSON reading,cmd_validate) into a guided flow. -
Minimal blast radius. Only 3 lines change in
sw-init.sh(flag parse + delegation).sw-ci.shgains one new case entry and one new orchestrator function. The core generation logic lives in a new sourced library (scripts/lib/ci-quickstart.sh) that is independently testable. -
Consistent with existing patterns. Other commands delegate similarly —
initcallsdoctorat the end,pipeline startdelegates toloop. Two entry points (init --ciandci quickstart) is the established pattern.
┌─────────────────────────────────────────────────────────┐
│ Entry Points │
│ sw-init.sh --ci sw-ci.sh quickstart │
│ (3 lines: parse + exec) (direct invocation) │
└─────────┬─────────────────────────────┬─────────────────┘
│ exec │ call
└──────────┐ ┌──────────────┘
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ sw-ci.sh :: cmd_quickstart() │
│ Orchestrator: parse flags → detect → generate → validate │
└────┬──────────┬──────────┬──────────────┬───────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌────────────┐ ┌──────────────┐
│pipeline-│ │templates/│ │ci-quick- │ │cmd_validate()│
│detection│ │pipelines/│ │start.sh │ │(existing in │
│.sh │ │*.json │ │(new lib) │ │sw-ci.sh) │
│(existing)│ │(existing)│ │ │ │ │
└─────────┘ └─────────┘ └────────────┘ └──────────────┘
1. User invokes: `shipwright init --ci` or `shipwright ci quickstart`
├── init --ci: sw-init.sh parses --ci, execs sw-ci.sh quickstart "$@"
└── ci quickstart: sw-ci.sh router dispatches to cmd_quickstart()
2. cmd_quickstart() parses flags:
--template <list> (default: "fast,standard")
--test-cmd <cmd> (override auto-detection)
--force (overwrite existing files)
3. detect_project_for_ci() → { env_id, test_cmd }
└── calls detect_repo_environments_json() → picks first env
└── calls default_test_cmd_for_environment(env_id) → test command
└── --test-cmd flag overrides detected command
4. For each template in the comma-separated list:
a. Validate template exists: templates/pipelines/${template}.json
b. Read template JSON → extract enabled stages for workflow comment
c. generate_quickstart_workflow(template, test_cmd, env_id) → YAML string
d. Atomic write: tmp file → mv to .github/workflows/shipwright-${template}.yml
e. Validate with cmd_validate() on the written file
f. emit_event "ci_quickstart_generated" "template=$template"
5. generate_readme_section(templates[], owner, repo) → stdout
└── Badges + usage instructions printed for user to paste
6. Print summary: files created, next steps
# ── scripts/lib/ci-quickstart.sh ── # detect_project_for_ci() # Stdout: two lines — env_id on line 1, test_cmd on line 2 # Exit 0: detection succeeded (may return "unknown" + empty cmd) # Side effects: none # Dependencies: pipeline-detection.sh must be sourced # generate_quickstart_workflow(template_name, test_cmd, env_id) # 1ドル: template name (e.g., "fast", "standard") # 2ドル: test command (e.g., "npm test") # 3ドル: detected environment ID (e.g., "node", "python") # Stdout: complete GitHub Actions YAML # Exit 0: success # Exit 1: template JSON not found # Side effects: none (pure function) # Dependencies: reads templates/pipelines/${template_name}.json # generate_readme_section(templates_csv, owner, repo) # 1ドル: comma-separated template names (e.g., "fast,standard") # 2ドル: GitHub owner (e.g., "ezigus") # 3ドル: GitHub repo name (e.g., "shipwright") # Stdout: markdown snippet with badges and usage # Exit 0: always # Side effects: none # ── scripts/sw-ci.sh ── # cmd_quickstart([--template <list>] [--test-cmd <cmd>] [--force]) # Orchestrates the full quickstart flow # Exit 0: all workflows generated successfully # Exit 1: template not found, write failure, or validation failure # Side effects: writes .github/workflows/shipwright-*.yml, emits events # Dependencies: sources lib/ci-quickstart.sh, lib/pipeline-detection.sh # ── scripts/sw-doctor.sh ── # doctor_check_ci() # Checks: workflow files exist, valid YAML structure (name/on/jobs), # issue-label trigger present, secrets references valid # Side effects: increments PASS/WARN/FAIL counters # Exit: does not exit (caller checks FAIL count)
| Error | Component | Handling |
|---|---|---|
| No project type detected | detect_project_for_ci() |
Returns env_id="unknown", test_cmd="" — cmd_quickstart() warns and inserts echo "TODO: add test command" placeholder |
| Template JSON not found | generate_quickstart_workflow() |
Exits 1 with message listing available templates from templates/pipelines/
|
.github/workflows/ dir doesn't exist |
cmd_quickstart() |
Creates it with mkdir -p before writing |
Existing workflow file, no --force
|
cmd_quickstart() |
Skips file, prints warning with exact path. Continues with remaining templates |
| Generated YAML fails validation | cmd_quickstart() |
Prints error, removes the invalid file, exits 1 |
jq not available |
detect_project_for_ci() |
Checked at function entry; exits 1 with install instructions |
| Write permission denied |
atomic_write via set -euo pipefail
|
ERR trap fires, prints $BASH_SOURCE:$LINENO
|
Each shipwright-{template}.yml contains:
name: "Shipwright Pipeline ({Template})" on: issues: types: [labeled] push: branches: [main] pull_request: branches: [main] workflow_dispatch: inputs: issue_number: description: "Issue number to process" required: false env: SHIPWRIGHT_PIPELINE: "{template}" jobs: pipeline: runs-on: ubuntu-latest if: >- github.event_name \!= 'issues' || github.event.label.name == 'ready-to-build' steps: - uses: actions/checkout@v4 - name: Setup run: | # Install shipwright, detect project, install deps - name: Secrets check run: | # Verify GITHUB_TOKEN exists; warn for optional secrets - name: Run pipeline env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | shipwright pipeline start \ --issue "${{ github.event.issue.number || inputs.issue_number }}" \ --template {template} - name: Upload artifacts if: always() uses: actions/upload-artifact@v4 with: name: pipeline-artifacts path: .claude/pipeline-artifacts/ - name: Report status if: always() && env.NO_GITHUB \!= '1' run: | # Comment on issue with result
D1: Library + Orchestrator separation
-
Context: Generation logic needs to be independently testable without invoking the full
sw-ci.shrouter. -
Decision: Pure functions in
scripts/lib/ci-quickstart.sh, orchestration incmd_quickstart()withinsw-ci.sh. - Consequence: Tests can source the library directly and call individual functions with controlled inputs.
D2: Default to fast + standard templates
-
Context: New users need sensible defaults.
fastcovers hotfixes and simple PRs;standardcovers feature work with review gates. -
Decision:
--templatedefaults to"fast,standard". Users can specify any combination. -
Consequence: Two workflow files by default. Users who want
fullorautonomousmust opt in.
D3: Skip-by-default for existing files
- Context: Users may have customized their workflow files. Overwriting silently would destroy work.
-
Decision: Check for existing files before writing. Skip with warning unless
--forceis passed. - Consequence: Safe for re-running, but users must explicitly opt in to regenerate.
D4: Atomic writes via tmp + mv
-
Context:
set -euo pipefailmeans a partial write leaves a corrupted file. -
Decision: Use the
atomic_writepattern fromhelpers.sh(write to tmp, thenmv). - Consequence: Either the full file exists or it doesn't — no partial states.
D5: Post-write validation
- Context: Generated YAML could have syntax errors from string interpolation bugs.
-
Decision: Run
cmd_validate()on each generated file immediately after writing. Remove file if validation fails. - Consequence: Users never end up with broken workflows committed to their repo.
-
Approach A: Extend
sw-init.shdirectly — Pros: Single entry point, reuses existing flag infrastructure. Cons:sw-init.shis already ~500 lines handling local setup (CLAUDE.md, deploy platforms, doctor delegation). Adding workflow generation would violate single responsibility and make the file harder to maintain. CI generation is conceptually different from local init. -
Approach C: Standalone
sw-ci-quickstart.shscript — Pros: Complete isolation, zero risk to existing files. Cons: Duplicates the helper-sourcing boilerplate, adds another top-level script to maintain, can't reusecmd_validate()without sourcingsw-ci.sh, inconsistent with the subcommand pattern used everywhere else.
-
Files to create:
-
scripts/lib/ci-quickstart.sh— Core library (~200 lines):detect_project_for_ci(),generate_quickstart_workflow(),generate_readme_section()
-
-
Files to modify:
-
scripts/sw-ci.sh— Addquickstart)case entry (line ~574) +cmd_quickstart()orchestrator function (~40 lines) -
scripts/sw-init.sh— Add--ciflag parse +exec "$SCRIPT_DIR/sw-ci.sh" quickstart "$@"delegation (~10 lines) -
scripts/sw-doctor.sh— Add--ciflag +doctor_check_ci()validation section (~30 lines) -
scripts/sw-ci-test.sh— Add 9 quickstart test cases (~80 lines) -
scripts/sw-doctor-test.sh— Add 2 doctor--citest cases (~20 lines)
-
-
Dependencies: None new. All dependencies are existing internal libraries:
-
scripts/lib/pipeline-detection.sh(detection) -
scripts/lib/helpers.sh(output, events, atomic writes) -
templates/pipelines/*.json(template definitions) -
jq(already a documented prerequisite)
-
-
Risk areas:
-
Bash 3.2 case conversion: The existing
cmd_generate()insw-ci.shuses${var^}. New code must usetr '[:lower:]' '[:upper:]'instead. Grep for${var^}and${var,,}in new code during review. -
YAML generation via heredoc: Multi-line YAML built from bash variables is fragile. Indentation errors produce invalid YAML. Mitigated by post-write
cmd_validate(). -
Template schema drift: If
templates/pipelines/*.jsonschema changes (e.g., stages renamed), generated workflow comments become stale. Mitigated by reading template dynamically rather than hardcoding stage lists.
-
Bash 3.2 case conversion: The existing
-
shipwright ci quickstartin a Node.js repo (withpackage.json) generatesshipwright-fast.ymlandshipwright-standard.ymlin.github/workflows/withnpm testas the test command -
shipwright init --ciproduces identical output toshipwright ci quickstart(delegation works) - Generated YAML passes
shipwright ci validatewithout errors - Generated YAML includes
on.issues.types: [labeled]trigger withready-to-buildfilter - Generated YAML includes
actions/upload-artifact@v4step - Generated YAML includes
NO_GITHUBconditional on status reporting step -
--template fullgenerates onlyshipwright-full.yml -
--test-cmd "pytest -x"overrides detected test command in generated YAML -
--forceoverwrites existing files; without--force, existing files are skipped with warning - Running in a directory with no detectable project type produces workflows with placeholder test command and a warning
-
shipwright doctor --cipasses when valid workflows exist -
shipwright doctor --cireports failure when no workflows exist - All functions work with
NO_GITHUB=1— noghAPI calls - All new code passes
bash -nsyntax check on Bash 3.2 (no${var^}, nodeclare -A, noreadarray) -
npm testpasses with no regressions