feat: add artifact pass/fail/needs-fix lifecycle for SDLC execution phases
- Add pass/fail/needs-fix CLI commands to cmd/sdlc/cmd_artifact.go
- Add 3 new methods to SDLCExecutor interface in internal/port
- Implement methods in kubernetes adapter
- Add service methods to SDLCService
- Add HTTP handlers for POST .../artifacts/{type}/pass|fail|needs-fix
- Update 6 skeleton commands to evaluate and set artifact status
- Update test mocks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
64ccf0b85d
commit
6e8f5821af
@ -35,6 +35,7 @@ Run Claude Code instances in isolated Kubernetes pods with REST API control. Ena
|
|||||||
| **DNS / Cloudflare** | [services/dns-cloudflare.md](.claude/guides/services/dns-cloudflare.md) |
|
| **DNS / Cloudflare** | [services/dns-cloudflare.md](.claude/guides/services/dns-cloudflare.md) |
|
||||||
| **Network policies / internal routing** | [ops/networking.md](.claude/guides/ops/networking.md) |
|
| **Network policies / internal routing** | [ops/networking.md](.claude/guides/ops/networking.md) |
|
||||||
| **SDLC orchestration** | [services/sdlc.md](.claude/guides/services/sdlc.md) |
|
| **SDLC orchestration** | [services/sdlc.md](.claude/guides/services/sdlc.md) |
|
||||||
|
| **Visual verification (Playwright)** | [services/visual-verification.md](.claude/guides/services/visual-verification.md) |
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
@ -171,6 +172,7 @@ cookbooks/ # End-to-end workflow guides
|
|||||||
| Build Orchestration | Planned | Structured build specs via API |
|
| Build Orchestration | Planned | Structured build specs via API |
|
||||||
| SDLC Orchestration | **Done** | Deterministic feature lifecycle with classifier engine, API, orchestrator, and 15 skeleton commands |
|
| SDLC Orchestration | **Done** | Deterministic feature lifecycle with classifier engine, API, orchestrator, and 15 skeleton commands |
|
||||||
| Composable Monorepo Templates | **Done** | Monorepo skeleton + component templates (service, worker, app-astro, app-react, cli) |
|
| Composable Monorepo Templates | **Done** | Monorepo skeleton + component templates (service, worker, app-astro, app-react, cli) |
|
||||||
|
| Visual Verification | Planned | Playwright screenshots/video + AI evaluation for feature completeness |
|
||||||
|
|
||||||
**Current Version:** v0.10.25
|
**Current Version:** v0.10.25
|
||||||
|
|
||||||
|
|||||||
@ -151,6 +151,147 @@ var artifactRejectCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var artifactPassCmd = &cobra.Command{
|
||||||
|
Use: "pass <feature> <type>",
|
||||||
|
Short: "Mark an artifact as passed (for execution artifacts: review, audit, qa_results)",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
root := mustResolveRoot()
|
||||||
|
slug, artTypeStr := args[0], args[1]
|
||||||
|
artType := sdlc.ArtifactType(artTypeStr)
|
||||||
|
|
||||||
|
f, err := sdlc.LoadFeature(root, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
art := f.GetArtifact(artType)
|
||||||
|
if art == nil {
|
||||||
|
return sdlc.ErrArtifactNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
art.MarkPassed()
|
||||||
|
if err := f.Save(root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record in state
|
||||||
|
state, err := sdlc.LoadState(root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.RecordAction("PASS_ARTIFACT", slug, "user", "success")
|
||||||
|
if err := state.Save(root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
return printJSON(map[string]string{
|
||||||
|
"feature": slug,
|
||||||
|
"artifact": string(artType),
|
||||||
|
"status": "passed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Passed: %s/%s\n", slug, artType)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var artifactFailCmd = &cobra.Command{
|
||||||
|
Use: "fail <feature> <type>",
|
||||||
|
Short: "Mark an artifact as failed (for execution artifacts: review, audit, qa_results)",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
root := mustResolveRoot()
|
||||||
|
slug, artTypeStr := args[0], args[1]
|
||||||
|
artType := sdlc.ArtifactType(artTypeStr)
|
||||||
|
|
||||||
|
f, err := sdlc.LoadFeature(root, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
art := f.GetArtifact(artType)
|
||||||
|
if art == nil {
|
||||||
|
return sdlc.ErrArtifactNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
art.MarkFailed()
|
||||||
|
if err := f.Save(root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record in state
|
||||||
|
state, err := sdlc.LoadState(root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.RecordAction("FAIL_ARTIFACT", slug, "user", "success")
|
||||||
|
if err := state.Save(root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
return printJSON(map[string]string{
|
||||||
|
"feature": slug,
|
||||||
|
"artifact": string(artType),
|
||||||
|
"status": "failed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Failed: %s/%s\n", slug, artType)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var artifactNeedsFixCmd = &cobra.Command{
|
||||||
|
Use: "needs-fix <feature> <type>",
|
||||||
|
Short: "Mark an artifact as needing fixes (for execution artifacts: review, audit)",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
root := mustResolveRoot()
|
||||||
|
slug, artTypeStr := args[0], args[1]
|
||||||
|
artType := sdlc.ArtifactType(artTypeStr)
|
||||||
|
|
||||||
|
f, err := sdlc.LoadFeature(root, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
art := f.GetArtifact(artType)
|
||||||
|
if art == nil {
|
||||||
|
return sdlc.ErrArtifactNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
art.MarkNeedsFix()
|
||||||
|
if err := f.Save(root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record in state
|
||||||
|
state, err := sdlc.LoadState(root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.RecordAction("NEEDS_FIX_ARTIFACT", slug, "user", "success")
|
||||||
|
if err := state.Save(root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
return printJSON(map[string]string{
|
||||||
|
"feature": slug,
|
||||||
|
"artifact": string(artType),
|
||||||
|
"status": "needs_fix",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Needs fix: %s/%s\n", slug, artType)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var artifactStatusCmd = &cobra.Command{
|
var artifactStatusCmd = &cobra.Command{
|
||||||
Use: "status <feature>",
|
Use: "status <feature>",
|
||||||
Short: "Show all artifact statuses for a feature",
|
Short: "Show all artifact statuses for a feature",
|
||||||
@ -185,6 +326,9 @@ func init() {
|
|||||||
artifactCreateCmd,
|
artifactCreateCmd,
|
||||||
artifactApproveCmd,
|
artifactApproveCmd,
|
||||||
artifactRejectCmd,
|
artifactRejectCmd,
|
||||||
|
artifactPassCmd,
|
||||||
|
artifactFailCmd,
|
||||||
|
artifactNeedsFixCmd,
|
||||||
artifactStatusCmd,
|
artifactStatusCmd,
|
||||||
)
|
)
|
||||||
rootCmd.AddCommand(artifactCmd)
|
rootCmd.AddCommand(artifactCmd)
|
||||||
|
|||||||
@ -210,6 +210,21 @@ func (e *SDLCExecutor) RejectArtifact(ctx context.Context, podName, slug string,
|
|||||||
return e.execSDLCNoOutput(ctx, podName, "artifact", "reject", slug, string(artType))
|
return e.execSDLCNoOutput(ctx, podName, "artifact", "reject", slug, string(artType))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PassArtifact marks a feature artifact as passed.
|
||||||
|
func (e *SDLCExecutor) PassArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
return e.execSDLCNoOutput(ctx, podName, "artifact", "pass", slug, string(artType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailArtifact marks a feature artifact as failed.
|
||||||
|
func (e *SDLCExecutor) FailArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
return e.execSDLCNoOutput(ctx, podName, "artifact", "fail", slug, string(artType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsFixArtifact marks a feature artifact as needing fixes.
|
||||||
|
func (e *SDLCExecutor) NeedsFixArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
return e.execSDLCNoOutput(ctx, podName, "artifact", "needs-fix", slug, string(artType))
|
||||||
|
}
|
||||||
|
|
||||||
// ListTasks returns all tasks for a feature.
|
// ListTasks returns all tasks for a feature.
|
||||||
func (e *SDLCExecutor) ListTasks(ctx context.Context, podName, slug string) ([]sdlc.Task, error) {
|
func (e *SDLCExecutor) ListTasks(ctx context.Context, podName, slug string) ([]sdlc.Task, error) {
|
||||||
out, err := e.execSDLC(ctx, podName, "task", "list", slug)
|
out, err := e.execSDLC(ctx, podName, "task", "list", slug)
|
||||||
|
|||||||
@ -88,16 +88,32 @@ Write to `.sdlc/features/$ARGUMENTS/audit.md`:
|
|||||||
[Ordered list of actions to take]
|
[Ordered list of actions to take]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Register the Artifact
|
### 7. Register and Evaluate the Artifact
|
||||||
|
|
||||||
|
Create the artifact:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sdlc artifact create $ARGUMENTS audit
|
sdlc artifact create $ARGUMENTS audit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then evaluate the audit results and set the appropriate status:
|
||||||
|
|
||||||
|
- If the audit has **no critical or high findings**: mark as passed
|
||||||
|
```bash
|
||||||
|
sdlc artifact pass $ARGUMENTS audit
|
||||||
|
```
|
||||||
|
- If the audit has **critical or high findings**: mark as needs-fix
|
||||||
|
```bash
|
||||||
|
sdlc artifact needs-fix $ARGUMENTS audit
|
||||||
|
```
|
||||||
|
|
||||||
|
This status drives the SDLC classifier to either advance to QA or trigger remediate-audit.
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
- NEVER skip OWASP checks -- even if the feature seems low-risk
|
- NEVER skip OWASP checks -- even if the feature seems low-risk
|
||||||
- ALWAYS check for hardcoded secrets, tokens, and credentials
|
- ALWAYS check for hardcoded secrets, tokens, and credentials
|
||||||
- ALWAYS verify authentication and authorization boundaries
|
- ALWAYS verify authentication and authorization boundaries
|
||||||
- NEVER pass an audit with critical or high severity findings unresolved
|
- NEVER mark an audit as passed if it has unresolved critical or high findings
|
||||||
- ALWAYS run static analysis tools before manual review
|
- ALWAYS run static analysis tools before manual review
|
||||||
|
- ALWAYS set the artifact status (pass or needs-fix) after writing the audit
|
||||||
|
|||||||
@ -55,9 +55,22 @@ Update `.sdlc/features/$ARGUMENTS/qa-results.md`:
|
|||||||
- Add a remediation note explaining what was fixed
|
- Add a remediation note explaining what was fixed
|
||||||
- Verify the overall status (PASS only if all scenarios pass)
|
- Verify the overall status (PASS only if all scenarios pass)
|
||||||
|
|
||||||
### 8. Report
|
### 8. Update Artifact Status
|
||||||
|
|
||||||
Summarize: failures fixed, root causes, regression status (all previously passing tests still pass).
|
After all fixes are applied and tests re-run:
|
||||||
|
|
||||||
|
- If **all scenarios now pass**: mark as passed
|
||||||
|
```bash
|
||||||
|
sdlc artifact pass $ARGUMENTS qa_results
|
||||||
|
```
|
||||||
|
- If **failures remain**: keep as failed
|
||||||
|
```bash
|
||||||
|
sdlc artifact fail $ARGUMENTS qa_results
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Report
|
||||||
|
|
||||||
|
Summarize: failures fixed, root causes, regression status (all previously passing tests still pass), and artifact status.
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
@ -66,3 +79,4 @@ Summarize: failures fixed, root causes, regression status (all previously passin
|
|||||||
- NEVER fix tests to match broken behavior -- fix the implementation
|
- NEVER fix tests to match broken behavior -- fix the implementation
|
||||||
- ALWAYS keep passing tests passing -- no regressions
|
- ALWAYS keep passing tests passing -- no regressions
|
||||||
- ALWAYS update the QA results document with resolution details
|
- ALWAYS update the QA results document with resolution details
|
||||||
|
- ALWAYS set the artifact status after fixing (pass if all scenarios now pass)
|
||||||
|
|||||||
@ -55,9 +55,22 @@ go test ./... 2>/dev/null || true
|
|||||||
|
|
||||||
All tests must pass after all fixes are applied.
|
All tests must pass after all fixes are applied.
|
||||||
|
|
||||||
### 8. Report
|
### 8. Update Artifact Status
|
||||||
|
|
||||||
Summarize: findings fixed by severity, files modified, test results.
|
After all fixes are applied and tests pass, re-evaluate the review:
|
||||||
|
|
||||||
|
- If **all blockers are resolved** and tests pass: mark as passed
|
||||||
|
```bash
|
||||||
|
sdlc artifact pass $ARGUMENTS review
|
||||||
|
```
|
||||||
|
- If **blockers remain unresolved**: keep as needs-fix
|
||||||
|
```bash
|
||||||
|
sdlc artifact needs-fix $ARGUMENTS review
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Report
|
||||||
|
|
||||||
|
Summarize: findings fixed by severity, files modified, test results, and artifact status.
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
@ -66,3 +79,4 @@ Summarize: findings fixed by severity, files modified, test results.
|
|||||||
- NEVER close a finding without actually fixing it
|
- NEVER close a finding without actually fixing it
|
||||||
- NEVER introduce new issues while fixing existing ones
|
- NEVER introduce new issues while fixing existing ones
|
||||||
- ALWAYS update the review report with resolution notes
|
- ALWAYS update the review report with resolution notes
|
||||||
|
- ALWAYS set the artifact status after fixing (pass if all blockers resolved)
|
||||||
|
|||||||
@ -56,9 +56,22 @@ Update `.sdlc/features/$ARGUMENTS/audit.md` with remediation notes:
|
|||||||
| [description] | CRITICAL | REMEDIATED | [what was done] |
|
| [description] | CRITICAL | REMEDIATED | [what was done] |
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8. Report
|
### 8. Update Artifact Status
|
||||||
|
|
||||||
Summarize: findings remediated by severity, remaining items, verification results.
|
After all remediations are applied and security checks pass, re-evaluate the audit:
|
||||||
|
|
||||||
|
- If **all critical and high findings are remediated**: mark as passed
|
||||||
|
```bash
|
||||||
|
sdlc artifact pass $ARGUMENTS audit
|
||||||
|
```
|
||||||
|
- If **critical or high findings remain**: keep as needs-fix
|
||||||
|
```bash
|
||||||
|
sdlc artifact needs-fix $ARGUMENTS audit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Report
|
||||||
|
|
||||||
|
Summarize: findings remediated by severity, remaining items, verification results, and artifact status.
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
@ -68,3 +81,4 @@ Summarize: findings remediated by severity, remaining items, verification result
|
|||||||
- NEVER fix security issues with workarounds -- address root causes
|
- NEVER fix security issues with workarounds -- address root causes
|
||||||
- ALWAYS update the audit report with remediation details
|
- ALWAYS update the audit report with remediation details
|
||||||
- NEVER remove security findings from the report -- mark them as remediated
|
- NEVER remove security findings from the report -- mark them as remediated
|
||||||
|
- ALWAYS set the artifact status after remediation (pass if all critical/high resolved)
|
||||||
|
|||||||
@ -70,16 +70,32 @@ Write to `.sdlc/features/$ARGUMENTS/review.md`:
|
|||||||
[Which acceptance criteria have tests? Which are missing?]
|
[Which acceptance criteria have tests? Which are missing?]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Register the Artifact
|
### 6. Register and Evaluate the Artifact
|
||||||
|
|
||||||
|
Create the artifact:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sdlc artifact create $ARGUMENTS review
|
sdlc artifact create $ARGUMENTS review
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then evaluate the review results and set the appropriate status:
|
||||||
|
|
||||||
|
- If the review has **NO blockers**: mark as passed
|
||||||
|
```bash
|
||||||
|
sdlc artifact pass $ARGUMENTS review
|
||||||
|
```
|
||||||
|
- If the review has **blockers**: mark as needs-fix
|
||||||
|
```bash
|
||||||
|
sdlc artifact needs-fix $ARGUMENTS review
|
||||||
|
```
|
||||||
|
|
||||||
|
This status drives the SDLC classifier to either advance to audit or trigger fix-review-issues.
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
- ALWAYS read spec and design before reviewing code
|
- ALWAYS read spec and design before reviewing code
|
||||||
- NEVER skip the security review dimension
|
- NEVER skip the security review dimension
|
||||||
- ALWAYS check test coverage against acceptance criteria
|
- ALWAYS check test coverage against acceptance criteria
|
||||||
- ALWAYS provide actionable findings with file locations
|
- ALWAYS provide actionable findings with file locations
|
||||||
- NEVER approve a review with unresolved blockers
|
- NEVER mark a review as passed if it has unresolved blockers
|
||||||
|
- ALWAYS set the artifact status (pass or needs-fix) after writing the review
|
||||||
|
|||||||
@ -77,12 +77,27 @@ Write to `.sdlc/features/$ARGUMENTS/qa-results.md`:
|
|||||||
[Detailed description of each failure with reproduction steps]
|
[Detailed description of each failure with reproduction steps]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Register the Artifact
|
### 6. Register and Evaluate the Artifact
|
||||||
|
|
||||||
|
Create the artifact:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sdlc artifact create $ARGUMENTS qa_results
|
sdlc artifact create $ARGUMENTS qa_results
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then evaluate the QA results and set the appropriate status:
|
||||||
|
|
||||||
|
- If **all scenarios pass** and all acceptance criteria are covered: mark as passed
|
||||||
|
```bash
|
||||||
|
sdlc artifact pass $ARGUMENTS qa_results
|
||||||
|
```
|
||||||
|
- If **any scenario fails** or acceptance criteria have gaps: mark as failed
|
||||||
|
```bash
|
||||||
|
sdlc artifact fail $ARGUMENTS qa_results
|
||||||
|
```
|
||||||
|
|
||||||
|
This status drives the SDLC classifier to either advance to merge or trigger fix-qa-failures.
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
- ALWAYS execute every scenario from the QA plan -- no skipping
|
- ALWAYS execute every scenario from the QA plan -- no skipping
|
||||||
@ -90,3 +105,4 @@ sdlc artifact create $ARGUMENTS qa_results
|
|||||||
- ALWAYS document ALL results, including passing scenarios
|
- ALWAYS document ALL results, including passing scenarios
|
||||||
- ALWAYS verify acceptance criteria coverage explicitly
|
- ALWAYS verify acceptance criteria coverage explicitly
|
||||||
- NEVER fabricate test evidence -- run the actual tests
|
- NEVER fabricate test evidence -- run the actual tests
|
||||||
|
- ALWAYS set the artifact status (pass or fail) after writing QA results
|
||||||
|
|||||||
@ -55,6 +55,9 @@ func (h *SDLCHandler) Mount(r api.Router) {
|
|||||||
// Artifacts - write
|
// Artifacts - write
|
||||||
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/features/{slug}/artifacts/{type}/approve", h.ApproveArtifact)
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/features/{slug}/artifacts/{type}/approve", h.ApproveArtifact)
|
||||||
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/features/{slug}/artifacts/{type}/reject", h.RejectArtifact)
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/features/{slug}/artifacts/{type}/reject", h.RejectArtifact)
|
||||||
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/features/{slug}/artifacts/{type}/pass", h.PassArtifact)
|
||||||
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/features/{slug}/artifacts/{type}/fail", h.FailArtifact)
|
||||||
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/features/{slug}/artifacts/{type}/needs-fix", h.NeedsFixArtifact)
|
||||||
|
|
||||||
// Tasks - read
|
// Tasks - read
|
||||||
r.With(auth.RequireScope(auth.ScopeProjectsRead, auth.ScopeAdmin)).Get("/features/{slug}/tasks", h.ListTasks)
|
r.With(auth.RequireScope(auth.ScopeProjectsRead, auth.ScopeAdmin)).Get("/features/{slug}/tasks", h.ListTasks)
|
||||||
|
|||||||
@ -82,3 +82,87 @@ func (h *SDLCHandler) RejectArtifact(w http.ResponseWriter, r *http.Request) {
|
|||||||
"status": "rejected",
|
"status": "rejected",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PassArtifact marks a feature artifact as passed (for execution artifacts).
|
||||||
|
// POST /projects/{id}/sdlc/features/{slug}/artifacts/{type}/pass
|
||||||
|
func (h *SDLCHandler) PassArtifact(w http.ResponseWriter, r *http.Request) {
|
||||||
|
projectID := chi.URLParam(r, "id")
|
||||||
|
slug := chi.URLParam(r, "slug")
|
||||||
|
artTypeStr := chi.URLParam(r, "type")
|
||||||
|
|
||||||
|
artType := sdlc.ArtifactType(artTypeStr)
|
||||||
|
if !sdlc.IsValidArtifactType(artType) {
|
||||||
|
api.WriteBadRequest(w, r, "invalid artifact type: "+artTypeStr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), TimeoutHeavyWrite)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := h.sdlcService.PassArtifact(ctx, projectID, slug, artType); err != nil {
|
||||||
|
writeSDLCError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.WriteSuccess(w, r, map[string]any{
|
||||||
|
"feature": slug,
|
||||||
|
"artifact": artTypeStr,
|
||||||
|
"status": "passed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailArtifact marks a feature artifact as failed (for execution artifacts).
|
||||||
|
// POST /projects/{id}/sdlc/features/{slug}/artifacts/{type}/fail
|
||||||
|
func (h *SDLCHandler) FailArtifact(w http.ResponseWriter, r *http.Request) {
|
||||||
|
projectID := chi.URLParam(r, "id")
|
||||||
|
slug := chi.URLParam(r, "slug")
|
||||||
|
artTypeStr := chi.URLParam(r, "type")
|
||||||
|
|
||||||
|
artType := sdlc.ArtifactType(artTypeStr)
|
||||||
|
if !sdlc.IsValidArtifactType(artType) {
|
||||||
|
api.WriteBadRequest(w, r, "invalid artifact type: "+artTypeStr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), TimeoutHeavyWrite)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := h.sdlcService.FailArtifact(ctx, projectID, slug, artType); err != nil {
|
||||||
|
writeSDLCError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.WriteSuccess(w, r, map[string]any{
|
||||||
|
"feature": slug,
|
||||||
|
"artifact": artTypeStr,
|
||||||
|
"status": "failed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsFixArtifact marks a feature artifact as needing fixes.
|
||||||
|
// POST /projects/{id}/sdlc/features/{slug}/artifacts/{type}/needs-fix
|
||||||
|
func (h *SDLCHandler) NeedsFixArtifact(w http.ResponseWriter, r *http.Request) {
|
||||||
|
projectID := chi.URLParam(r, "id")
|
||||||
|
slug := chi.URLParam(r, "slug")
|
||||||
|
artTypeStr := chi.URLParam(r, "type")
|
||||||
|
|
||||||
|
artType := sdlc.ArtifactType(artTypeStr)
|
||||||
|
if !sdlc.IsValidArtifactType(artType) {
|
||||||
|
api.WriteBadRequest(w, r, "invalid artifact type: "+artTypeStr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), TimeoutHeavyWrite)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := h.sdlcService.NeedsFixArtifact(ctx, projectID, slug, artType); err != nil {
|
||||||
|
writeSDLCError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.WriteSuccess(w, r, map[string]any{
|
||||||
|
"feature": slug,
|
||||||
|
"artifact": artTypeStr,
|
||||||
|
"status": "needs_fix",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -64,6 +64,15 @@ func (m *testSDLCExecutor) ApproveArtifact(_ context.Context, _, _ string, _ sdl
|
|||||||
func (m *testSDLCExecutor) RejectArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
func (m *testSDLCExecutor) RejectArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
||||||
return m.err
|
return m.err
|
||||||
}
|
}
|
||||||
|
func (m *testSDLCExecutor) PassArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
func (m *testSDLCExecutor) FailArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
func (m *testSDLCExecutor) NeedsFixArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
func (m *testSDLCExecutor) ListTasks(_ context.Context, _, _ string) ([]sdlc.Task, error) {
|
func (m *testSDLCExecutor) ListTasks(_ context.Context, _, _ string) ([]sdlc.Task, error) {
|
||||||
return m.tasks, m.err
|
return m.tasks, m.err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,15 @@ type SDLCExecutor interface {
|
|||||||
// RejectArtifact rejects a feature artifact.
|
// RejectArtifact rejects a feature artifact.
|
||||||
RejectArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
RejectArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
|
|
||||||
|
// PassArtifact marks a feature artifact as passed (for execution artifacts).
|
||||||
|
PassArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
|
|
||||||
|
// FailArtifact marks a feature artifact as failed (for execution artifacts).
|
||||||
|
FailArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
|
|
||||||
|
// NeedsFixArtifact marks a feature artifact as needing fixes.
|
||||||
|
NeedsFixArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
|
|
||||||
// ListTasks returns all tasks for a feature.
|
// ListTasks returns all tasks for a feature.
|
||||||
ListTasks(ctx context.Context, podName, slug string) ([]sdlc.Task, error)
|
ListTasks(ctx context.Context, podName, slug string) ([]sdlc.Task, error)
|
||||||
|
|
||||||
|
|||||||
@ -186,6 +186,45 @@ func (s *SDLCService) RejectArtifact(ctx context.Context, projectID, slug string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PassArtifact marks a feature artifact as passed.
|
||||||
|
func (s *SDLCService) PassArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
podName, err := s.resolveProjectPod(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.sdlcExec.PassArtifact(ctx, podName, slug, artType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logger.Info("artifact passed", "project", projectID, "feature", slug, "artifact", string(artType))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailArtifact marks a feature artifact as failed.
|
||||||
|
func (s *SDLCService) FailArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
podName, err := s.resolveProjectPod(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.sdlcExec.FailArtifact(ctx, podName, slug, artType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logger.Info("artifact failed", "project", projectID, "feature", slug, "artifact", string(artType))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsFixArtifact marks a feature artifact as needing fixes.
|
||||||
|
func (s *SDLCService) NeedsFixArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
podName, err := s.resolveProjectPod(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.sdlcExec.NeedsFixArtifact(ctx, podName, slug, artType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logger.Info("artifact needs fix", "project", projectID, "feature", slug, "artifact", string(artType))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListTasks returns all tasks for a feature.
|
// ListTasks returns all tasks for a feature.
|
||||||
func (s *SDLCService) ListTasks(ctx context.Context, projectID, slug string) ([]sdlc.Task, error) {
|
func (s *SDLCService) ListTasks(ctx context.Context, projectID, slug string) ([]sdlc.Task, error) {
|
||||||
podName, err := s.resolveProjectPod(ctx, projectID)
|
podName, err := s.resolveProjectPod(ctx, projectID)
|
||||||
|
|||||||
@ -24,6 +24,9 @@ type mockSDLCExecutor struct {
|
|||||||
getArtifactStatusFn func(ctx context.Context, podName, slug string) (map[sdlc.ArtifactType]*sdlc.Artifact, error)
|
getArtifactStatusFn func(ctx context.Context, podName, slug string) (map[sdlc.ArtifactType]*sdlc.Artifact, error)
|
||||||
approveArtifactFn func(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
approveArtifactFn func(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
rejectArtifactFn func(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
rejectArtifactFn func(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
|
passArtifactFn func(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
|
failArtifactFn func(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
|
needsFixArtifactFn func(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error
|
||||||
listTasksFn func(ctx context.Context, podName, slug string) ([]sdlc.Task, error)
|
listTasksFn func(ctx context.Context, podName, slug string) ([]sdlc.Task, error)
|
||||||
addTaskFn func(ctx context.Context, podName, slug, title string) (*sdlc.Task, error)
|
addTaskFn func(ctx context.Context, podName, slug, title string) (*sdlc.Task, error)
|
||||||
startTaskFn func(ctx context.Context, podName, slug, taskID string) error
|
startTaskFn func(ctx context.Context, podName, slug, taskID string) error
|
||||||
@ -123,6 +126,27 @@ func (m *mockSDLCExecutor) RejectArtifact(ctx context.Context, podName, slug str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockSDLCExecutor) PassArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
if m.passArtifactFn != nil {
|
||||||
|
return m.passArtifactFn(ctx, podName, slug, artType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSDLCExecutor) FailArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
if m.failArtifactFn != nil {
|
||||||
|
return m.failArtifactFn(ctx, podName, slug, artType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSDLCExecutor) NeedsFixArtifact(ctx context.Context, podName, slug string, artType sdlc.ArtifactType) error {
|
||||||
|
if m.needsFixArtifactFn != nil {
|
||||||
|
return m.needsFixArtifactFn(ctx, podName, slug, artType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockSDLCExecutor) ListTasks(ctx context.Context, podName, slug string) ([]sdlc.Task, error) {
|
func (m *mockSDLCExecutor) ListTasks(ctx context.Context, podName, slug string) ([]sdlc.Task, error) {
|
||||||
if m.listTasksFn != nil {
|
if m.listTasksFn != nil {
|
||||||
return m.listTasksFn(ctx, podName, slug)
|
return m.listTasksFn(ctx, podName, slug)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user