Skip to content

Phase 6c: azure-devops runner port - #61

Closed
grubmeshi wants to merge 5 commits into
refactor/single-go-binary/phase-6b-gitlabfrom
refactor/single-go-binary/phase-6c-azdevops
Closed

Phase 6c: azure-devops runner port#61
grubmeshi wants to merge 5 commits into
refactor/single-go-binary/phase-6b-gitlabfrom
refactor/single-go-binary/phase-6c-azdevops

Conversation

@grubmeshi

Copy link
Copy Markdown
Collaborator

Phase 6c — azure-devops runner port

Third of the four stacked Kotlin→Go port PRs. Base: phase-6b-gitlab.

Ports the Azure DevOps runner to Go (internal/azdevops, cmd/azdevops, cmd/bbrunner azdevops, container) — the heaviest port (sync poll loop + stage-step fan-out, injected Clock, no real sleeps). internal/azdevops new at 93.2% (gate 90). Build, race tests, lint (0 issues), coverage gate all pass on the stacked tree.

Consolidation reconciliation (STOP-C4)

06C was authored in parallel with 06B and independently shipped meshapi.Decryptor + a typed meshapi.DecryptInputs. During stacking these were reconciled with 06B's:

  • 06B's meshapi.Decryptor/NoopDecryptor/NewCertDecryptor (with the Kotlin empty-string guard) and byte-based meshapi.DecryptInputs are kept.
  • 06C's typed-DTO input decryptor is kept but renamed to meshapi.DecryptInputSpecs (internal/meshapi/decryptinputspecs.go) — the two consumers need different shapes (gitlab forwards the whole raw run document; azure-devops holds parsed []BuildingBlockInputSpecDTO). Identical STRING/CODE/FILE branch rule. internal/azdevops/handler.go call site updated accordingly.

Sanctioned deltas / deferrals

  • Kotlin single-line-PEM bug fixed (independently of 06B): additive normalizePEM fallback in internal/crypto — azure-devops is the first Go port that actually decrypts (config.ResolvePrivateKey). Coexists with 06B's multi-line re-wrap (normalize is a no-op on already-valid PEM). Prevents a silent config-compat break (D7/D10).
  • Kotlin module removal / CI flip / meshfed deferred to the acceptance-gated flip PR; Kotlin pin tests not added (gradle not runnable) — the 28-pin Go scenario suite (S/U/P/A/K/F) is the surviving pin.

🤖 Generated with Claude Code

06A specified these for 06B (gitlab) to ship as first consumer, but
phase-6b-gitlab had not landed them at authoring time (git diff
phase-6a-manual..phase-6b-gitlab was empty). Ships them here to the
06A-specified contract instead, per PLAN_DETAIL_06C_azdevops.md's STOP-C4
resolution -- flagged in CROSS_REPO_TODO.md for reconciliation if 06B lands
its own copy in parallel.

Mirrors internal/tf.Decryptor's existing shape (Decryptor interface,
certDecryptor, NoopDecryptor) and the Kotlin MeshCertDecryptionService
input-only decrypt asymmetry (STRING/CODE/FILE decrypted, other sensitive
types left as ciphertext, impl secrets untouched).
Kotlin's hand-rolled PrivateKeyLoader/MeshCertDecryptionService parser
strips the BEGIN/END markers and every newline by hand, so every Kotlin
runner image's classpath runner-config.yml ships its baked dev privateKey
with no newline after "-----BEGIN PRIVATE KEY-----". Go's stdlib
encoding/pem.Decode rejects that shape outright.

azure-devops is the first Go port to actually decrypt (manual never does),
so this was the first real exercise of config.ResolvePrivateKey against a
real Kotlin-shipped key -- without this fix, both this port's own baked dev
key and any customer's existing Kotlin runner-config.yml would silently
fail to decrypt on the new Go image (a config-compat break, D7/D10).

normalizePEM is a pure additive fallback: only tried when a bare pem.Decode
already failed, so an already-valid multi-line PEM is unaffected. Verified
end-to-end against the actual containers/azure-devops-block-runner baked
key via both a unit test and a docker-run smoke.
…s.md)

The heaviest of the four Kotlin ports (AzureDevOpsPipelinePoller/
StatusUpdater/StatusMapper had zero direct Kotlin tests): trigger a
pipeline run, then either hand over IN_PROGRESS (async) or poll to a
terminal report with per-stage steps (sync), on the 06A template
(dispatch.RunHandler, the unified report.Reporter, config compat helpers,
StandaloneClaimClassifier).

- statusmapper.go: pure S-P1-S-P6 mapping-table twins of
  AzureDevOpsStatusMapper.kt, incl. the pinned else-holes
  (completed+succeededWithIssues/absent -> IN_PROGRESS).
- client.go: the ADO wire client (trigger/get-run/get-timeline), the
  ExternalCallError type (per-package, per umbrella §4 row 14 -- not a
  shared meshapi type), renderValue (UseNumber-fidelity stringification,
  flagged JSON-not-toString delta for composite values, §16.6), redirect
  policy, explicit timeouts (§16.10), no base-URL sanitization (§16.7).
- updates.go: RunStatus builders for every U-P1-U-P8 update shape
  (trigger-success, state-only, stage-batch with the one-way COMPLETED
  dedup, final, failed, terminal abort/failed).
- handler.go: the failure ladder (register -> parse impl -> decrypt PAT ->
  decrypt inputs -> trigger), byte-identical U-P8 message pair selection.
- poll.go: the sync poll loop, ctx-aware (plan-05 H7 amendment: reports a
  TERMINAL status on shutdown cancellation, ABORTED falling back to FAILED,
  never SUCCEEDED) with escalation semantics verified line-by-line against
  the Kotlin nested try/catch (a PATCH failure during a stage/fallback
  update is silently absorbed like a GET failure; only the timeout and
  final-update reports get one escalation attempt before propagating).
- config.go/loadconfig.go/reporterfactory.go/singlerun.go: persona config,
  the R12 single-run exit tail with the sanctioned K-P2 delta, single-run
  mode wiring.

Test suite (D16 scenario style, D6 gate ≥90%, actual 93.2%, -race clean):
reproduces all 28 planned pins (6 S + 8 U + 5 P + 6 A + 2 K + 1 F) via a
fake Azure DevOps httptest transport, the shared meshapitest mock, and an
injected Clock (sleep-free, no goroutine races -- After() fires instantly
and Now() only advances via an explicit per-test bump, so even the 30-min
timeout pin runs in milliseconds).

No new Kotlin JUnit tests were added (gradle is not runnable in this
environment, matching the 6a precedent) -- the Go scenario suite is the
surviving pin; see CROSS_REPO_TODO.md.
- cmd/azdevops/main.go + polling.go: the fit azure-devops-block-runner
  binary (D1/D8) -- single-run vs polling mode selection, mgmt listener on
  8101 (PORT alias), cert-based decryptor wiring.
- cmd/bbrunner: register the azdevops persona subcommand
  (`bbrunner azdevops`) alongside tf/manual, for local-dev / mux
  replacement (§4.1).
- containers/azure-devops-block-runner/{Dockerfile,runner-config.yml}: the
  06A per-app-Dockerfile pattern, direct entrypoint, the baked local-dev
  private key (umbrella §10.5) -- verified with a real `docker build` +
  `docker run` smoke (config loads, private key resolves and parses,
  dispatch loop starts, /healthz answers OK).
- tools/coverage/thresholds.txt: gate internal/azdevops at 90% (actual
  93.2%, no exclusions -- whole package hermetically testable).
- .golangci.yml: azdevops/azdevops-test depguard groups mirroring the
  manual persona's (no k8s, no go-git, no tofu).
Adds PLAN_DETAIL_06C_azdevops.md (from the plan branch) to this stacked
branch, and records the phase-6c deferrals in CROSS_REPO_TODO.md: the
Kotlin module removal / CI flip / meshfed-release edit all wait on the §11
acceptance gate (live Azure DevOps org + side-by-side transcript
equivalence), which this environment cannot execute -- same rationale as
phase-6a. Also flags the two STOP-C4/crypto template-gap fixes this slice
made for the next port to be aware of.
@grubmeshi

Copy link
Copy Markdown
Collaborator Author

Superseded by #65, which consolidates the plan + all phases 0→7 + the phase-3/5 remediation into a single reviewable PR (with all plan/run artifacts and a code-verified FOLLOW_UP.md). Closing in favor of #65.

@grubmeshi grubmeshi closed this Jul 13, 2026
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