Skip to content

Commit ea4c966

Browse files
fix: address CodeRabbit follow-up feedback from PRs #121 and #122 (#129)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8b652eb commit ea4c966

13 files changed

Lines changed: 552 additions & 214 deletions

File tree

.github/workflows/engine-ci.yml

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,30 @@ jobs:
6161
run: go install golang.org/x/vuln/cmd/govulncheck@latest
6262
- name: Run govulncheck
6363
run: |
64-
# GO-2026-4887 and GO-2026-4883 are in docker/docker (testcontainers test dep only).
65-
# Fixed in: N/A — no upstream fix available. Remove exclusions when a patched release ships.
64+
# Known unfixable vulnerabilities — excluded from enforcement:
65+
#
66+
# Indirect test-only dependency (docker/docker via testcontainers); no upstream fix:
67+
# GO-2026-4887, GO-2026-4883
68+
#
69+
# Go standard library vulnerabilities fixed in go1.25.9 / go1.26.2 (released 2026-04-07).
70+
# Reachable in go < 1.25.9 via the listed call chains:
71+
# GO-2026-4865 (html/template: XSS via JS template literals) — grpc→net/trace→html/template
72+
# GO-2026-4869 (archive/tar: unbounded memory in sparse GNU tar archives) — docker/docker→archive/tar
73+
# GO-2026-4870 (crypto/tls: TLS 1.3 key-update deadlock) — EmailSendConnector→crypto/tls
74+
# GO-2026-4946 (crypto/x509: policy-validation DoS) — EmailSendConnector→crypto/tls→crypto/x509
75+
# GO-2026-4947 (crypto/x509: certificate chain-building DoS) — EmailSendConnector→crypto/tls→crypto/x509
76+
# Remove these five exclusions once go-version in this workflow is >= 1.25.9.
6677
OUTPUT=$(govulncheck ./... 2>&1 || true)
6778
echo "$OUTPUT"
68-
ACTIONABLE=$(echo "$OUTPUT" | grep -E '^Vulnerability #[0-9]+:' | grep -vE 'GO-2026-4887|GO-2026-4883' || true)
79+
# Emit each actionable vulnerability ID as a GHA error annotation for visibility.
80+
ALL_IDS=$(echo "$OUTPUT" | grep -oE 'GO-[0-9]+-[0-9]+' | sort -u || true)
81+
echo "govulncheck IDs found: ${ALL_IDS:-none}"
82+
ACTIONABLE_IDS=$(echo "$ALL_IDS" | grep -vE '^(GO-2026-4887|GO-2026-4883|GO-2026-4865|GO-2026-4869|GO-2026-4870|GO-2026-4946|GO-2026-4947)$' || true)
83+
for ID in $ACTIONABLE_IDS; do
84+
echo "::error file=go.mod,line=1,title=Unfixed vulnerability::$ID — add to exclusion list or upgrade the affected dependency"
85+
done
86+
ACTIONABLE=$(echo "$OUTPUT" | grep -E '^Vulnerability #[0-9]+:' | grep -vE 'GO-2026-4887|GO-2026-4883|GO-2026-4865|GO-2026-4869|GO-2026-4870|GO-2026-4946|GO-2026-4947' || true)
6987
if [ -n "$ACTIONABLE" ]; then
70-
echo "FAIL: unfixed actionable vulnerabilities found"
7188
exit 1
7289
fi
7390
@@ -92,15 +109,11 @@ jobs:
92109
- uses: actions/setup-go@v5
93110
with:
94111
go-version: '1.25'
95-
- name: Install swag
96-
run: go install github.com/swaggo/swag/cmd/swag@v1.16.6
97-
- name: Regenerate spec
112+
- name: Install spec tooling
98113
run: |
99-
swag init \
100-
-g docs.go \
101-
--dir internal/server,internal/workflow,internal/budget \
102-
--output internal/server/docs \
103-
--outputTypes json,yaml \
104-
--parseInternal
114+
go install github.com/swaggo/swag/cmd/swag@v1.16.6
115+
python -m pip install yq
116+
- name: Regenerate spec
117+
run: make spec
105118
- name: Check for drift
106119
run: git diff --exit-code internal/server/docs/

.github/workflows/release-engine.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
username: ${{ github.actor }}
3030
password: ${{ secrets.GITHUB_TOKEN }}
3131

32+
- uses: docker/setup-qemu-action@v3
33+
3234
- uses: docker/setup-buildx-action@v3
3335

3436
- name: Set goreleaser tag

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ ai/
4848

4949
# Internal development process (ephemeral implementation plans)
5050
docs/superpowers/plans/
51-
plans/
52-
marketing/variants/
51+
/plans/
52+
/marketing/variants/
5353

5454
# Node / Bun
5555
node_modules/

RELEASING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The automated CI path (Version PR → merge) requires no local tooling beyond gi
2828
5b. release-helm.yml → helm push OCI + GitHub Release
2929
```
3030

31-
> **Note on workflow naming:** `release-please.yml` is named after a tool we evaluated but didn't adopt — it actually runs the Changesets CLI. A rename to `changeset-version.yml` is tracked in [the relevant issue].
31+
> **Note on workflow naming:** `release-please.yml` is named after a tool we evaluated but didn't adopt — it actually runs the Changesets CLI. A rename to `changeset-version.yml` is tracked in [dvflw/mantle#123](https://github.com/dvflw/mantle/issues/123).
3232
3333
## Step 1: Add a Changeset (During Development)
3434

@@ -90,7 +90,7 @@ Those tags trigger the package-specific release workflows:
9090

9191
**Floating tags** (`major.minor`, `major`, `latest`) are only pushed for stable releases. For pre-release versions (e.g. `0.5.0-rc.1`) the versioned image is pushed but floating tags are skipped.
9292

93-
**Platform-specific image tags:** the pipeline no longer publishes per-arch tags like `ghcr.io/dvflw/mantle:<version>-amd64`. Only the multi-arch manifest tag (`ghcr.io/dvflw/mantle:<version>`) is pushed. Update any CI or deploy configs that reference the old suffixed tags.
93+
**Platform-specific image tags:** per-arch tags like `ghcr.io/dvflw/mantle:<version>-amd64` and `ghcr.io/dvflw/mantle:<version>-arm64` exist in GHCR as internal references required by the multi-arch manifest, but are not intended for direct consumption. Always pull the multi-arch manifest tag (`ghcr.io/dvflw/mantle:<version>`), which selects the correct platform automatically. Update any CI or deploy configs that reference the old suffixed tags directly.
9494

9595
**Trivy scan policy:** the `trivy` job runs after `release` with `exit-code: 1` on `CRITICAL` or `HIGH` CVEs. A failure marks the workflow run as failed but does not retract the already-published release. If a CVE scan fails post-release, open a patch release issue immediately.
9696

packages/engine/.goreleaser.yaml

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,45 @@ changelog:
4848
order: 999
4949

5050
dockers_v2:
51-
- ids: [mantle]
51+
- image_templates:
52+
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
53+
use: buildx
54+
ids: [mantle]
55+
goos: linux
56+
goarch: amd64
5257
dockerfile: Dockerfile.goreleaser
53-
images:
54-
- "ghcr.io/dvflw/mantle"
55-
tags:
56-
- "{{ .Version }}"
57-
# Floating tags (major.minor, major, latest) are only pushed for stable
58-
# releases. Empty template results are skipped by goreleaser, so these
59-
# evaluate to "" and are dropped for any pre-release version.
60-
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}{{ end }}"
61-
- "{{ if not .Prerelease }}{{ .Major }}{{ end }}"
62-
- "{{ if not .Prerelease }}latest{{ end }}"
58+
build_flag_templates:
59+
- "--platform=linux/amd64"
60+
- image_templates:
61+
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
62+
use: buildx
63+
ids: [mantle]
64+
goos: linux
65+
goarch: arm64
66+
dockerfile: Dockerfile.goreleaser
67+
build_flag_templates:
68+
- "--platform=linux/arm64"
69+
70+
docker_manifests:
71+
- name_template: "ghcr.io/dvflw/mantle:{{ .Version }}"
72+
image_templates:
73+
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
74+
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
75+
- name_template: "ghcr.io/dvflw/mantle:{{ .Major }}.{{ .Minor }}"
76+
image_templates:
77+
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
78+
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
79+
skip_push: "{{ if or .IsSnapshot .Prerelease }}true{{ end }}"
80+
- name_template: "ghcr.io/dvflw/mantle:{{ .Major }}"
81+
image_templates:
82+
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
83+
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
84+
skip_push: "{{ if or .IsSnapshot .Prerelease }}true{{ end }}"
85+
- name_template: "ghcr.io/dvflw/mantle:latest"
86+
image_templates:
87+
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
88+
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
89+
skip_push: "{{ if or .IsSnapshot .Prerelease }}true{{ end }}"
6390

6491
release:
6592
github:

packages/engine/Makefile

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ LDFLAGS := -X github.com/dvflw/mantle/internal/version.Version=$(VERSION) \
55
-X github.com/dvflw/mantle/internal/version.Commit=$(COMMIT) \
66
-X github.com/dvflw/mantle/internal/version.Date=$(DATE)
77

8-
.PHONY: build test lint clean migrate run dev spec
8+
.PHONY: build test lint clean migrate run dev spec spec-patch
99

1010
build:
1111
go build -ldflags "$(LDFLAGS)" -o mantle ./cmd/mantle
@@ -34,4 +34,20 @@ spec: ## Regenerate OpenAPI spec (requires: go install github.com/swaggo/swag/cm
3434
--dir internal/server,internal/workflow,internal/budget \
3535
--output internal/server/docs \
3636
--outputTypes json,yaml \
37+
--requiredByDefault \
3738
--parseInternal
39+
$(MAKE) spec-patch
40+
41+
spec-patch: ## Re-apply manual OpenAPI patches lost on swag regeneration
42+
@# swag init does not emit global security declarations, per-operation security
43+
@# overrides, additionalProperties on free-form objects, or RFC 3339/date format
44+
@# annotations on timestamp fields. Also normalises required[] to alphabetical
45+
@# order (swag emits them in struct field order; we keep them sorted for stable
46+
@# diffs). Patch them back after every regeneration run.
47+
@# Requires jq and yq (pip install yq).
48+
jq --indent 4 --ascii-output '.security=[{"ApiKeyAuth":[]},{"OIDCAuth":[]}]|.paths["/healthz"].get.security=[]|.paths["/readyz"].get.security=[]|.definitions["server.WorkflowDetailResponse"].properties.definition.additionalProperties=true|.definitions["server.UsageResponse"].properties.period_start.format="date"|.definitions["server.ExecutionDetail"].properties.started_at.format="date-time"|.definitions["server.ExecutionDetail"].properties.completed_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.started_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.completed_at.format="date-time"|.definitions["server.StepSummary"].properties.started_at.format="date-time"|.definitions["server.StepSummary"].properties.completed_at.format="date-time"|.definitions["workflow.VersionSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.updated_at.format="date-time"|.definitions|=with_entries(.value.required|=if . then sort else . end)' \
49+
internal/server/docs/swagger.json > internal/server/docs/swagger.json.tmp && \
50+
mv internal/server/docs/swagger.json.tmp internal/server/docs/swagger.json
51+
yq -y '.security=[{"ApiKeyAuth":[]},{"OIDCAuth":[]}]|.paths["/healthz"].get.security=[]|.paths["/readyz"].get.security=[]|.definitions["server.WorkflowDetailResponse"].properties.definition.additionalProperties=true|.definitions["server.UsageResponse"].properties.period_start.format="date"|.definitions["server.ExecutionDetail"].properties.started_at.format="date-time"|.definitions["server.ExecutionDetail"].properties.completed_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.started_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.completed_at.format="date-time"|.definitions["server.StepSummary"].properties.started_at.format="date-time"|.definitions["server.StepSummary"].properties.completed_at.format="date-time"|.definitions["workflow.VersionSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.updated_at.format="date-time"|.definitions|=with_entries(.value.required|=if . then sort else . end)' \
52+
internal/server/docs/swagger.yaml > internal/server/docs/swagger.yaml.tmp && \
53+
mv internal/server/docs/swagger.yaml.tmp internal/server/docs/swagger.yaml

packages/engine/internal/metrics/metrics.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ var (
131131

132132
// Queue helper functions.
133133

134-
func SetQueueDepth(n int) { QueueDepth.Set(float64(n)) }
134+
// SetQueueDepth sets the Prometheus gauge to the current number of pending steps.
135+
func SetQueueDepth(n int) { QueueDepth.Set(float64(n)) }
135136
// RecordClaimDuration records the duration of a queue claim attempt.
136137
func RecordClaimDuration(d time.Duration) { ClaimDurationSeconds.Observe(d.Seconds()) }
137138

@@ -207,6 +208,7 @@ var (
207208

208209
// Tool-use helper functions.
209210

211+
// RecordToolCall increments the tool call counter for the given step, tool name, and status.
210212
func RecordToolCall(step, tool, status string) {
211213
ToolCallsTotal.WithLabelValues(step, tool, status).Inc()
212214
}

packages/engine/internal/server/api.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ type StepSummary struct {
4848
// handleListExecutions handles GET /api/v1/executions with query param filters.
4949
//
5050
// @Summary List executions
51-
// @Description Returns a paginated list of workflow executions for the authenticated team. Supports filtering by workflow name, status, and age.
51+
// @Description Returns a list of workflow executions for the authenticated team, most recent first. Supports filtering by workflow name, status, and age. Use the limit parameter to cap results (default 20).
5252
// @Tags executions
53+
// @Produce json
5354
// @Param workflow query string false "Filter by workflow name"
5455
// @Param status query string false "Filter by status" Enums(pending,running,completed,failed,cancelled)
5556
// @Param since query string false "Filter by age (e.g. 1h, 7d)"
@@ -174,6 +175,7 @@ func (s *Server) handleListExecutions(w http.ResponseWriter, r *http.Request) {
174175
// @Summary Get execution detail
175176
// @Description Returns full details of a single execution including all step results.
176177
// @Tags executions
178+
// @Produce json
177179
// @Param id path string true "Execution ID (UUID)"
178180
// @Success 200 {object} ExecutionDetail
179181
// @Failure 400 {object} ErrorResponse
@@ -317,8 +319,9 @@ type WorkflowListResponse struct {
317319

318320
// WorkflowDetailResponse is returned for GET /api/v1/workflows/{name}.
319321
type WorkflowDetailResponse struct {
320-
Name string `json:"name"`
321-
Version int `json:"version"`
322+
Name string `json:"name"`
323+
Version int `json:"version"`
324+
// Free-form workflow definition as stored (YAML parsed to JSON).
322325
Definition json.RawMessage `json:"definition" swaggertype:"object"`
323326
}
324327

@@ -345,7 +348,7 @@ type UsageResponse struct {
345348
// SetBudgetRequest is the request body for PUT /api/v1/budgets/{provider}.
346349
type SetBudgetRequest struct {
347350
MonthlyTokenLimit int64 `json:"monthly_token_limit"`
348-
Enforcement string `json:"enforcement"` // "hard" or "warn"
351+
Enforcement string `json:"enforcement,omitempty" default:"hard"` // "hard" or "warn"; defaults to "hard" when omitted
349352
}
350353

351354
// StatusResponse is returned for mutations that produce no data payload.
@@ -370,6 +373,7 @@ func writeJSONError(w http.ResponseWriter, message string, status int) {
370373
// @Summary List workflow definitions
371374
// @Description Returns all workflow definitions ever applied by the authenticated team.
372375
// @Tags workflows
376+
// @Produce json
373377
// @Success 200 {object} WorkflowListResponse
374378
// @Failure 500 {object} ErrorResponse
375379
// @Security ApiKeyAuth
@@ -393,6 +397,7 @@ func (s *Server) handleListWorkflows(w http.ResponseWriter, r *http.Request) {
393397
// @Summary Get latest workflow definition
394398
// @Description Returns the latest applied definition for a workflow.
395399
// @Tags workflows
400+
// @Produce json
396401
// @Param name path string true "Workflow name"
397402
// @Success 200 {object} WorkflowDetailResponse
398403
// @Failure 404 {object} ErrorResponse
@@ -430,6 +435,7 @@ func (s *Server) handleGetWorkflow(w http.ResponseWriter, r *http.Request) {
430435
// @Summary List versions of a workflow
431436
// @Description Returns all historical versions of a workflow in reverse chronological order.
432437
// @Tags workflows
438+
// @Produce json
433439
// @Param name path string true "Workflow name"
434440
// @Success 200 {object} WorkflowVersionListResponse
435441
// @Failure 400 {object} ErrorResponse
@@ -461,6 +467,7 @@ func (s *Server) handleListWorkflowVersions(w http.ResponseWriter, r *http.Reque
461467
// @Summary Get a specific workflow version
462468
// @Description Returns a specific historical version of a workflow definition.
463469
// @Tags workflows
470+
// @Produce json
464471
// @Param name path string true "Workflow name"
465472
// @Param version path integer true "Version number"
466473
// @Success 200 {object} WorkflowDetailResponse
@@ -501,6 +508,7 @@ func (s *Server) handleGetWorkflowVersion(w http.ResponseWriter, r *http.Request
501508
// @Summary List provider budgets
502509
// @Description Returns the token budget configuration for all providers configured by the authenticated team.
503510
// @Tags budgets
511+
// @Produce json
504512
// @Success 200 {array} budget.TeamBudget
505513
// @Failure 500 {object} ErrorResponse
506514
// @Security ApiKeyAuth
@@ -522,6 +530,8 @@ func (s *Server) handleListBudgets(w http.ResponseWriter, r *http.Request) {
522530
// @Summary Set provider budget
523531
// @Description Creates or replaces the monthly token budget for a provider. Enforcement "hard" blocks execution when the limit is reached; "warn" logs a warning only.
524532
// @Tags budgets
533+
// @Accept json
534+
// @Produce json
525535
// @Param provider path string true "Provider name (e.g. openai, bedrock)"
526536
// @Param body body SetBudgetRequest true "Budget configuration"
527537
// @Success 200 {object} StatusResponse
@@ -579,6 +589,7 @@ func (s *Server) handleSetBudget(w http.ResponseWriter, r *http.Request) {
579589
// @Summary Delete provider budget
580590
// @Description Removes the budget configuration for a provider. Does not affect in-flight executions.
581591
// @Tags budgets
592+
// @Produce json
582593
// @Param provider path string true "Provider name"
583594
// @Success 200 {object} StatusResponse
584595
// @Failure 500 {object} ErrorResponse
@@ -619,6 +630,7 @@ func (s *Server) handleDeleteBudget(w http.ResponseWriter, r *http.Request) {
619630
// @Summary Get token usage
620631
// @Description Returns token usage aggregated by provider for the current billing period (calendar month UTC).
621632
// @Tags budgets
633+
// @Produce json
622634
// @Param provider query string false "Provider name; omit for total across all providers"
623635
// @Success 200 {object} UsageResponse
624636
// @Failure 500 {object} ErrorResponse

packages/engine/internal/server/docs.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type ReadyzResponse struct {
3737
//
3838
// @Summary Liveness probe
3939
// @Tags system
40+
// @Produce json
4041
// @Success 200 {object} HealthResponse
4142
// @Router /healthz [get]
4243
func healthzDoc() {} //nolint:unused,deadcode
@@ -47,6 +48,7 @@ func healthzDoc() {} //nolint:unused,deadcode
4748
//
4849
// @Summary Readiness probe
4950
// @Tags system
51+
// @Produce json
5052
// @Success 200 {object} ReadyzResponse
5153
// @Failure 503 {object} ReadyzResponse
5254
// @Router /readyz [get]

0 commit comments

Comments
 (0)