Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Phase 1: ADR-0012 service layer (#25, #26, #27, #28)#48

Merged
cdhagmann merged 4 commits into
mainfrom
phase-1-service-layer
May 4, 2026
Merged

Phase 1: ADR-0012 service layer (#25, #26, #27, #28)#48
cdhagmann merged 4 commits into
mainfrom
phase-1-service-layer

Conversation

@cdhagmann
Copy link
Copy Markdown
Owner

Summary

Implements ADR-0012 Phase 1 — the output-free service layer that ROADMAP Phase 1 depends on. One commit per issue, four issues:

Commit Issue Title
1df0bf8 #25 adopt dry-rb; Operations::Fork/Clone return Result
a7ea469 #26 Workflow#build_adapter returns Result; retire with_workflow_rescues
d9c670a #27 extract Operations::Branch + Announce; build Operations::FixPipeline
acc6fa8 #28 migrate CLI::Fork/CLI::Fix initializers to dry-initializer

Net effect: every Operations::* class is output-free and returns Success(...) / Failure(...). The fix flow is composed by Operations::FixPipeline (a Dry::Operation subclass). The CLI verbs are thin Result-pattern-matching shells.

What changed

New deps (gem-contribute.gemspec): dry-monads ~> 1.10, dry-operation ~> 1.1, dry-initializer ~> 3.2.

Operations layer:

  • Operations::Fork, Operations::Clone lose stdout:; return Success(Result) / Failure(reason).
  • Operations::Clone::Result = Data.define(:path, :reused) (was a bare path string).
  • New Operations::Branchgit checkout -b gem-contribute/issue-<N>, returns Result.
  • New Operations::Announce — posts/skips the "working on this" comment with internal gating (allow:); returns Success(:posted | :skipped) or Failure([:announce_failed, msg]).
  • New Operations::FixPipelinedry-operation composing Fork → Clone → Branch → Announce. Announce is called outside step because its Failure is informational.

CLI verbs:

  • CLI::Fix swaps its fork: CLI::Fork dependency for pipeline: Operations::FixPipeline.
  • CLI::Fork keeps its bootstrap shape but pattern-matches the new Result return.
  • Both use dry-initializer declarations — the # rubocop:disable Metrics/ParameterLists comments are gone.

Workflow mixin:

  • build_adapter returns Success(adapter) / Failure(:unauthenticated) (was nil + stderr).
  • with_workflow_rescues removed entirely.

Other:

  • CLI::IssueAnnouncer keeps fetch_claim_index (used by scan and issues) and exposes MARKER as an alias for Operations::Announce::WORKING_MARKER.
  • Two trivial rubocop autocorrects from bcf273d (Style/Lambda in cli.rb, Layout/EmptyLinesAroundClassBody in submit.rb) bundled into commit 1.

Test plan

  • bin/rspec — 217 examples, 0 failures (was 198 on main; +19 new specs across Operations::Branch, Announce, FixPipeline, plus failure-path coverage in cli/fork_spec and cli/fix_spec)
  • bin/rubocop — clean
  • Smoke test the fix flow against a real issue once merged (e.g. gem-contribute fix gem-contribute/5)

Notes for review

  • Best read commit-by-commit. Each commit is internally consistent and self-explained; reading them in order shows the gradual move from raise-based control flow → Result-based.
  • dry-monads pattern matching: Failure([:tag, payload]) is the constructor; Failure(:tag, payload) is the pattern. Failure#deconstruct uses Array() semantics, which unwraps arrays one level. Trip-wire I hit during commit 1.
  • Dry::Operation subclassing (not include): in 1.x, Dry::Operation is a class. Body returns a raw value, framework wraps in Success.
  • Soft-fail Announce: Announce's Failure does not propagate as a pipeline-level Failure (the fix has succeeded; the comment is a nice-to-have). Pattern in FixPipeline#call is to call Announce outside step.

🤖 Generated with Claude Code

Chris Hagmann and others added 4 commits May 3, 2026 20:13
…Result

Phase 1 of ADR-0012's three-interface architecture: the service layer
becomes output-free and Result-returning so the CLI, future TUI, and
future plugin entry points can share the same code paths.

Adds three runtime deps (`dry-monads`, `dry-operation`, `dry-initializer`,
all pinned `~> latest`); subsequent commits in this branch use the latter
two. This commit only exercises `dry-monads`.

`Operations::Fork` and `Operations::Clone` lose their `stdout:` parameter
and `@stdout.puts` calls, and now return:

  Success(Operations::Fork::Result.new(...))    # or
  Success(Operations::Clone::Result.new(path:, reused:))

…on the happy path, and one of:

  Failure(:unauthenticated)
  Failure([:adapter_error, message])

…on expected failures. Adapter exceptions (`AuthRequired`, `AdapterError`)
are caught at the operation boundary and translated to tagged Failures —
they no longer propagate across the service-layer/CLI seam.

`Operations::Clone::Result` is a new `Data.define(:path, :reused)`. The
old `target` path return becomes `result.path`; the `reused` flag mirrors
`Operations::Fork::Result#reused` so callers can render the right
"reusing existing X" line without checking the filesystem themselves.

The progress messages those operations used to print ("Forking
sidekiq/sidekiq...", "Cloning into ...") move to `CLI::Fork#bootstrap`,
which is the seam both `CLI::Fork#execute` and `CLI::Fix#execute`
compose. `bootstrap` itself now returns `Success([path, fork_info])` or
propagates Operations' Failure; both verbs pattern-match the Result and
either continue or render the failure with an appropriate exit code.

`with_workflow_rescues` is intentionally still in place — it now only
catches AdapterErrors from non-Operation paths (`@git.checkout_branch`,
etc.). The next commit (#26) retires it.

Specs: Operations specs assert directly on `Result` values (no stdout
captures). CLI specs stub Operations to return `Success(...)` /
`Failure(...)` and add coverage for the failure paths.

Bundled: two trivial rubocop autocorrects from `bcf273d` that surfaced
when running `bin/rubocop` on the changed files (`Style/Lambda` in
cli.rb, `Layout/EmptyLinesAroundClassBody` in submit.rb).

Closes #25.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…w_rescues

Continues Phase 1 of ADR-0012. With Operations now Result-returning
(previous commit), the only remaining nil-and-print pattern was
`Workflow#build_adapter`, and the only remaining typed-exception
rescue shell was `Workflow#with_workflow_rescues`. Both go.

`build_adapter` now returns `Success(adapter)` or `Failure(:unauthenticated)`.
`CLI::Fix#run` and `CLI::Fork#run` pattern-match on the Result and
print the auth-login hint themselves on the Failure branch.

`with_workflow_rescues` is removed entirely. The two verbs that used it
(Fix, Fork) handle each layer's failures explicitly:

  - build_adapter Failure(:unauthenticated) → auth-login hint, exit 1
  - bootstrap Failure(:adapter_error, msg)  → "<verb> failed: <msg>", exit 1

`CLI::Fix#execute` keeps a small targeted `rescue GemContribute::AdapterError`
on the outer def, scoped to the post-bootstrap git work that still
raises (notably `@git.checkout_branch`). The next commit (#27) folds
that into `Operations::Branch` (Result-returning) and the rescue can
go away.

`CLI::Fork#execute` doesn't need a rescue — every call site after
bootstrap is already Result-aware.

No test changes: the existing "auth-login hint" specs exercise the
verb's full flow and assert on the rendered message, so they pass
unchanged through the new Result wiring.

Closes #26.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Per ADR-0012 Phase 1, the `fix` flow becomes a four-step dry-operation
pipeline that the CLI verb (and a future TUI Command) call as a single
Result-returning service.

New operations:

  * `Operations::Branch` — wraps `git checkout -b gem-contribute/issue-<N>`.
    Returns `Success(Result(name:))` or `Failure([:adapter_error, msg])`.

  * `Operations::Announce` — posts (or skips) the "working on this"
    comment. Output-free; gating (`--no-comment`, config, viewer-self)
    is pushed in via the `allow:` argument so the operation itself is
    a pure decide/post/skip. Returns `Success(:posted | :skipped)` or
    `Failure([:announce_failed, msg])` (informational, not fatal).

  * `Operations::FixPipeline` — composes Fork → Clone → Branch → Announce
    via `Dry::Operation`. The first three are `step`s (short-circuit on
    Failure); Announce is called outside `step` so its Failure does not
    propagate as a pipeline-level failure (the fix itself succeeded).
    Adds the viewer-self gate (`fork.viewer != project.owner`) on top
    of the caller's `allow_announce:` boolean.

CLI/library changes:

  * `CLI::Fix` swaps its `fork:` (CLI::Fork instance) dependency for
    `pipeline:` (FixPipeline). The verb is now a thin Result-pattern-
    matching shell: build the adapter, compute `allow_announce`, call
    the pipeline, render the summary and announce outcome. The
    targeted `rescue GemContribute::AdapterError` introduced in #26
    is gone — the branch step now returns Failure too.

  * `CLI::IssueAnnouncer` keeps `fetch_claim_index` (used by `scan` and
    `issues` to flag claimed issues) and exposes `MARKER` as an alias
    for `Operations::Announce::WORKING_MARKER` so the search query
    stays in sync with the marker the operation posts.

Specs: three new operation specs cover Branch, Announce, FixPipeline
(11 new examples). `cli/fix_spec.rb` swaps `fork: fork_cli` stubbing
for `pipeline: pipeline` stubbing and adds explicit coverage for the
allow_announce-gating cases (--no-comment, config off, per-repo
override) and the announce-outcome rendering (Success(:posted),
Success(:skipped), Failure([:announce_failed, ...])).

Closes #27.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The two `# rubocop:disable Metrics/ParameterLists` comments were
load-bearing only because each verb's initializer juggled 9-10 keyword
args plus the `value || Default.new` fallback dance. `dry-initializer`'s
`option :name, default: -> { ... }` syntax expresses the same thing
declaratively, evaluates defaults in declaration order (so a later
option's default can reference an earlier option), and produces the
same external interface.

Notable trickle-through:
  * `option :post_clone_hooks` references `stdout` / `stderr`
  * `option :pipeline` (Fix) and `option :clone_op` (Fork) reference `git`

Both work because of the in-declaration-order default semantics.

Both rubocop disable directives are gone. No behaviour change; existing
specs pass without modification (the keyword-arg constructor surface is
identical).

Closes #28.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@cdhagmann cdhagmann merged commit 60798da into main May 4, 2026
1 check passed
cdhagmann pushed a commit that referenced this pull request May 4, 2026
ROADMAP: mark Phase 1 (ADR-0012 service layer) DONE — that work merged
via #48 on 2026-05-04 but the doc still listed it as not started.
Annotate Phase 2 as in flight with the PR link, and note that basic CI
already landed via #21 even though the broader #43 ticket remains open.

CHANGELOG: backfill the [Unreleased] entry for Phase 1 internal
architecture (was missing entirely), and add user-visible Phase 2
entries for the spinner-in-TTY behavior and the tty-prompt-driven init
flow that this PR introduces.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant