Skip to content

feat(stovepipe): add SourceControl extension contract#278

Merged
behinddwalls merged 1 commit into
mainfrom
preetam/stovepipe-sourcecontrol
Jun 26, 2026
Merged

feat(stovepipe): add SourceControl extension contract#278
behinddwalls merged 1 commit into
mainfrom
preetam/stovepipe-sourcecontrol

Conversation

@behinddwalls

@behinddwalls behinddwalls commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Why?

The Request.URI field is "empty until SourceControl resolution is wired in" — Stovepipe has no way to resolve a queue's head commit, compare two commits for history rewrites, or enumerate a ref's recent commits. The workflow RFC already names SourceControl as the sole owner of URI semantics with exactly these three responsibilities. This adds that contract so downstream stages (ingest, process) and a future status view can be built against it.

What?

New stovepipe/extension/sourcecontrol package holding the contract only (interface + Config + Factory interface + sentinel error), per the extension-design rules — no factory impl or routing.

The SourceControl interface is bound to a single queue by its Factory, so methods take no queue argument:

  • Latest(ctx) — latest commit URI on the queue's ref (VCS-agnostic name, not "Head").
  • IsAncestor(ctx, ancestor, descendant) — ancestry check; false is the history-rewrite signal that drives a full build.
  • History(ctx, cursor, limit) — cursor-paginated, newest-first page of commit URIs; bounded so a remote backend stays cheap.

URIs and the pagination cursor are opaque tokens interpreted only by the implementation.

Pagination uses a new shared generic platform/base/page.Page[T any] (Items + opaque NextCursor) rather than a one-off struct — the repo's first generic, reusable for any future paginated read across domains. Method-result data types live outside the extension package, matching existing precedent (entity.BuildStatus, entity.Conflict).

Ships with an in-memory fake backend (seeded with a queue's newest-first history) and a generated gomock for use in controller and pipeline tests.

Test Plan

bazel test //stovepipe/extension/sourcecontrol/... //platform/base/page/... — fake unit tests (Latest / IsAncestor / History pagination + not-found cases) pass.
make build, make gazelle, make fmt clean.

Issues

Stack

  1. feat(stovepipe): add Request entity, Ingest RPC, and thin ingest controller #276
  2. feat(stovepipe): add storage extension with RequestStore + RequestURIStore (MySQL) #277
  3. @ feat(stovepipe): add SourceControl extension contract #278
  4. feat(stovepipe): wire SourceControl + stores into ingest, publish to process #279

behinddwalls added a commit that referenced this pull request Jun 26, 2026
…roller (#276)

## Summary
### Why?

The Stovepipe workflow RFC (PR #275, this PR's base) describes a
pipeline whose entry point is **ingest**: an external poller reports
that a queue (a named repo+ref) has a new commit, and Stovepipe mints a
request to validate it. This PR lays the first foundation stones — the
domain model and the entry RPC — as a deliberately thin, log-only slice.

### What?

Three stacked commits:

- **`Request` entity** (`stovepipe/entity`) — one validation of a queue
at a particular commit: ID namespaced by the queue
(`"request/<queue>/<counter>"`), queue name, opaque VCS-agnostic commit
`URI` (empty until SourceControl resolution lands), `RequestState`
(initial state `accepted`), and `Version` for optimistic locking; with
`ToBytes`/`FromBytes` and a lightweight `RequestID`.
- **`Ingest` RPC** (`api/stovepipe`) — `Ingest(IngestRequest{queue})
returns (IngestResponse{id})` added to the single-service Stovepipe
proto; only the queue name is on the wire (commit-URI resolution via
SourceControl is a follow-up). Includes regenerated protopb stubs.
- **Thin `IngestController`** (`stovepipe/controller`) — validates the
queue, mints the ID via the counter extension, builds + logs the
`Request` (state `accepted`), returns the ID. No storage, publish, or
SourceControl yet. Wired into the example server behind a minimal
in-process counter.

Greenness is intentionally **not** modeled here: the RFC treats it as a
continuous degree (e.g. a fraction of projects broken), which an enum
cannot represent, and nothing in this slice consumes it. It will be
introduced with the `record`/project-analysis stage. Other explicit
follow-ups: SourceControl extension + commit-URI resolution; storage +
persistence; publishing onto the `process` topic; the stateful Queue
entity.

## Test Plan
- ✅ `bazel test //stovepipe/...` — `//stovepipe/entity` and
`//stovepipe/controller` tests pass (serialization round-trips; ingest
happy path / empty-queue user error / counter-error classification).
- ✅ `make proto` regenerates only the stovepipe stubs; `make lint`,
`make check-gazelle`, `make check-tidy` clean.
- ✅ `bazel build //example/stovepipe/...` builds the wired server.

## Issues


## Stack
1. @ #276
1. #277
1. #278
1. #279
@behinddwalls behinddwalls force-pushed the preetam/stovepipe-request-store branch from ff10b2f to 83961ca Compare June 26, 2026 22:19
@behinddwalls behinddwalls force-pushed the preetam/stovepipe-sourcecontrol branch from cc75a0b to 7b5f323 Compare June 26, 2026 22:19
behinddwalls added a commit that referenced this pull request Jun 26, 2026
…Store (MySQL) (#277)

## Summary
### Why?

The ingest controller (PR #276) mints a `Request` but is log-only. For
the pipeline to do real work it needs to persist requests and look them
up two ways, both flowing from the workflow RFC: by **request ID**
(every downstream stage reloads the entity), and by **(queue, commit
URI)** to find whether a commit is already being validated — the RFC's
`(Queue, head URI)` dedup key.

### What?

A new `stovepipe/extension/storage` extension plus its first backend
(MySQL), mirroring the SubmitQueue storage conventions: a factory
`Storage` interface,
`ErrNotFound`/`ErrAlreadyExists`/`ErrVersionMismatch` sentinels,
metrics-wrapped MySQL ops, and optimistic-locking CAS with version
arithmetic owned by the caller.

Two stores, **one per table**:

- **`RequestStore`** (`request` table) — `Create` (ErrAlreadyExists on
dup ID); `Get` by ID (ErrNotFound); `Update`, a pure conditional write
taking the whole `Request` plus `oldVersion`/`newVersion`, persisting
the mutable fields (uri, state) only if the stored version matches (else
ErrVersionMismatch).
- **`RequestURIStore`** (`request_uri` table) — the reverse index from a
validated commit to its request, keyed by `(queue, uri)`: `Create`
(ErrAlreadyExists on a duplicate `(queue, uri)` — the dedup signal) and
`GetIDByURI` (ErrNotFound). It's a separate store because it's a
separate table; the two are written independently (no cross-table
transaction) so the contract stays satisfiable by key/value backends.

Ships the MySQL impl, schema (`request.sql`, `request_uri.sql`),
generated mocks, and a docker-compose integration test.

Follow-ups: wire `storage` into `ingest` (dedup via `GetIDByURI`, then
`Create`); an in-memory backend + shared contract suite.

## Test Plan
- ✅ `bazel test
//test/integration/stovepipe/extension/storage/mysql:mysql_test` — real
MySQL via docker-compose: create/get/update-CAS (success, stale-version
mismatch, missing-row mismatch), not-found, duplicate-ID, and the URI
mapping (round-trip, not-found, duplicate dedup, per-queue isolation).
- ✅ `bazel build //stovepipe/...`; `make check-gazelle`, `make
check-tidy`, `make lint` clean. Mocks regenerated and idempotent.

## Issues


## Stack
1. #276
1. @ #277
1. #278
1. #279
## Summary

### Why?

The `Request.URI` field is "empty until SourceControl resolution is wired in" — Stovepipe has no way to resolve a queue's head commit, compare two commits for history rewrites, or enumerate a ref's recent commits. The workflow RFC already names SourceControl as the sole owner of URI semantics with exactly these three responsibilities. This adds that contract so downstream stages (`ingest`, `process`) and a future status view can be built against it.

### What?

New `stovepipe/extension/sourcecontrol` package holding the contract only (interface + Config + Factory interface + sentinel error), per the extension-design rules — no factory impl or routing.

The `SourceControl` interface is bound to a single queue by its Factory, so methods take no queue argument:
- `Latest(ctx)` — latest commit URI on the queue's ref (VCS-agnostic name, not "Head").
- `IsAncestor(ctx, ancestor, descendant)` — ancestry check; `false` is the history-rewrite signal that drives a full build.
- `History(ctx, cursor, limit)` — cursor-paginated, newest-first page of commit URIs; bounded so a remote backend stays cheap.

URIs and the pagination cursor are opaque tokens interpreted only by the implementation.

Pagination uses a new shared generic `platform/base/page.Page[T any]` (`Items` + opaque `NextCursor`) rather than a one-off struct — the repo's first generic, reusable for any future paginated read across domains. Method-result data types live outside the extension package, matching existing precedent (`entity.BuildStatus`, `entity.Conflict`).

Ships with an in-memory `fake` backend (seeded with a queue's newest-first history) and a generated gomock for use in controller and pipeline tests.

## Test Plan

✅ `bazel test //stovepipe/extension/sourcecontrol/... //platform/base/page/...` — fake unit tests (Latest / IsAncestor / History pagination + not-found cases) pass.
✅ `make build`, `make gazelle`, `make fmt` clean.
@behinddwalls behinddwalls force-pushed the preetam/stovepipe-sourcecontrol branch from 7b5f323 to 7ce0fd1 Compare June 26, 2026 22:20
@behinddwalls behinddwalls changed the base branch from preetam/stovepipe-request-store to main June 26, 2026 22:20
@behinddwalls behinddwalls merged commit 05b4b52 into main Jun 26, 2026
3 checks passed
@behinddwalls behinddwalls deleted the preetam/stovepipe-sourcecontrol branch June 26, 2026 22:20
behinddwalls added a commit that referenced this pull request Jun 26, 2026
…process (#279)

## Summary

### Why?

Ingest was a thin stub: it minted a request id but never resolved the
commit URI, persisted anything, or moved the request onto the pipeline,
so `Request.URI` stayed empty and nothing consumed the work. This makes
ingest the real pipeline entry and adds the first internal queue
contract so the pipeline can hand work to the next stage.

### What?

Ingest now resolves the queue's head URI via the SourceControl
extension, dedups on the (queue, URI) pair, persists the Request and its
URI mapping via storage, and publishes the request id to a new process
stage over the messaging queue. Ingestion is idempotent: a re-reported
head resolves to the already-minted request and nothing is published
again. The URI mapping is claimed before the request row is written, so
a lost race leaves no orphan row.

Adds the first internal proto message-queue contract under
`stovepipe/core/messagequeue` (proto3 + protojson, mirroring
`api/runway/messagequeue`): a `ProcessRequest` payload carrying the id,
the `TopicKeyProcess` constant, and the protojson glue, wired into the
proto codegen (`tool/proto`, `PROTO_PACKAGES`). Per CLAUDE.md, internal
contracts live under the domain's `core/`, not `api/`, and the contract
package owns both the payload and its topic keys.

Adds a minimal `process` consumer (`stovepipe/controller/process`) that
reloads the Request by id and logs it; a not-yet-visible request is
retryable so redelivery converges. The build-strategy/ancestry logic the
RFC assigns to `process` is deferred.

Wires the example server (`example/stovepipe/server`) into a MySQL
storage + MySQL queue + fake SourceControl stack with the process
consumer running, plus docker-compose (two databases) and a schema-init
make target.

## Test Plan

✅ `bazel test //stovepipe/...` — contract round-trip + topic-key
binding, ingest (happy/dedup/race/unknown-queue/infra-error paths), and
process consumer unit tests.
✅ `bazel test //test/integration/stovepipe:stovepipe_test` —
compose-backed: Ingest persists the request + URI mapping, publishes to
the process topic, and a re-ingest dedups to the same id.
✅ `bazel build //...`, `make fmt`.


## Stack
1. #276
1. #277
1. #278
1. @ #279
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants