fix(ax): remove proc.go wrapper layer
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
a757ca81e3
commit
3c2575f45b
22 changed files with 270 additions and 456 deletions
|
|
@ -21,7 +21,7 @@ core/go provides the primitives. core/agent composes them.
|
||||||
cmd/core-agent/main.go — entry point: core.New + Run
|
cmd/core-agent/main.go — entry point: core.New + Run
|
||||||
pkg/agentic/ — orchestration (dispatch, prep, verify, scan, commands)
|
pkg/agentic/ — orchestration (dispatch, prep, verify, scan, commands)
|
||||||
pkg/agentic/actions.go — named Action handlers (ctx, Options) → Result
|
pkg/agentic/actions.go — named Action handlers (ctx, Options) → Result
|
||||||
pkg/agentic/proc.go — process helpers via s.Core().Process()
|
pkg/agentic/pid.go — PID lifecycle helpers
|
||||||
pkg/agentic/handlers.go — IPC completion pipeline handlers
|
pkg/agentic/handlers.go — IPC completion pipeline handlers
|
||||||
pkg/agentic/status.go — workspace status (WriteAtomic + JSONMarshalString)
|
pkg/agentic/status.go — workspace status (WriteAtomic + JSONMarshalString)
|
||||||
pkg/agentic/paths.go — paths, fs (NewUnrestricted), helpers
|
pkg/agentic/paths.go — paths, fs (NewUnrestricted), helpers
|
||||||
|
|
@ -145,23 +145,13 @@ All handlers use `c.ACTION(messages.X{})` — no ChannelNotifier, no callbacks.
|
||||||
All commands via `s.Core().Process()`. Returns `core.Result` — Value is always a string.
|
All commands via `s.Core().Process()`. Returns `core.Result` — Value is always a string.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *PrepSubsystem) runCmd(ctx context.Context, dir, command string, args ...string) core.Result {
|
process := s.Core().Process()
|
||||||
return s.Core().Process().RunIn(ctx, dir, command, args...)
|
r := process.RunIn(ctx, dir, "git", "log", "--oneline", "-20")
|
||||||
|
if r.OK {
|
||||||
|
output := core.Trim(r.Value.(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PrepSubsystem) runCmdOK(ctx context.Context, dir, command string, args ...string) bool {
|
r = process.RunWithEnv(ctx, dir, []string{"GOWORK=off"}, "go", "test", "./...")
|
||||||
return s.runCmd(ctx, dir, command, args...).OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result {
|
|
||||||
return s.runCmd(ctx, dir, "git", args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...string) string {
|
|
||||||
r := s.gitCmd(ctx, dir, args...)
|
|
||||||
if !r.OK { return "" }
|
|
||||||
return core.Trim(r.Value.(string))
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
go-process is fully Result-native. `Start`, `Run`, `StartWithOptions`, `RunWithOptions` all return `core.Result`. Value is `*Process` for Start, `string` for Run. OK=true guarantees the type.
|
go-process is fully Result-native. `Start`, `Run`, `StartWithOptions`, `RunWithOptions` all return `core.Result`. Value is `*Process` for Start, `string` for Run. OK=true guarantees the type.
|
||||||
|
|
@ -409,10 +399,9 @@ c.Drive().New(core.NewOptions(
|
||||||
Every exported function MUST have a usage-example comment:
|
Every exported function MUST have a usage-example comment:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// gitCmd runs a git command in a directory.
|
// Process runs a git command in a directory.
|
||||||
//
|
//
|
||||||
// r := s.gitCmd(ctx, "/repo", "log", "--oneline")
|
// r := s.Core().Process().RunIn(ctx, "/repo", "git", "log", "--oneline")
|
||||||
func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result {
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -436,6 +425,7 @@ func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string)
|
||||||
|
|
||||||
- 2026-03-29: cmd/core-agent no longer rewrites `os.Args` before startup. The binary-owned commands now use named handlers, keeping the entrypoint on Core CLI primitives instead of repo-local argument mutation.
|
- 2026-03-29: cmd/core-agent no longer rewrites `os.Args` before startup. The binary-owned commands now use named handlers, keeping the entrypoint on Core CLI primitives instead of repo-local argument mutation.
|
||||||
- 2026-03-30: transport helpers preserve request and read causes, brain direct API calls surface upstream bodies, and review queue retry parsing no longer uses `MustCompile`.
|
- 2026-03-30: transport helpers preserve request and read causes, brain direct API calls surface upstream bodies, and review queue retry parsing no longer uses `MustCompile`.
|
||||||
|
- 2026-03-30: direct Core process calls replaced the `proc.go` wrapper layer; PID helpers now live in `pid.go` and the workspace template documents `c.Process()` directly.
|
||||||
- 2026-03-26: net/http consolidated to transport.go (ONE file). net/url + io/fs eliminated. RFC-025 updated with 3 new quality gates (net/http, net/url, io/fs). 1:1 test + example test coverage. Array[T].Deduplicate replaces custom helpers.
|
- 2026-03-26: net/http consolidated to transport.go (ONE file). net/url + io/fs eliminated. RFC-025 updated with 3 new quality gates (net/http, net/url, io/fs). 1:1 test + example test coverage. Array[T].Deduplicate replaces custom helpers.
|
||||||
- 2026-03-25: Quality gates pass. Zero disallowed imports (all 10). encoding/json→Core JSON. path/filepath→Core Path. os→Core Env/Fs. io→Core ReadAll/WriteAll. go-process fully Result-native. ServiceRuntime on all subsystems. 22 named Actions + Task pipeline. ChannelNotifier→IPC. Reference docs synced.
|
- 2026-03-25: Quality gates pass. Zero disallowed imports (all 10). encoding/json→Core JSON. path/filepath→Core Path. os→Core Env/Fs. io→Core ReadAll/WriteAll. go-process fully Result-native. ServiceRuntime on all subsystems. 22 named Actions + Task pipeline. ChannelNotifier→IPC. Reference docs synced.
|
||||||
- 2026-03-25: Initial spec — written with full core/go v0.8.0 domain context.
|
- 2026-03-25: Initial spec — written with full core/go v0.8.0 domain context.
|
||||||
|
|
|
||||||
12
AGENTS.md
12
AGENTS.md
|
|
@ -59,7 +59,7 @@ dispatch → prep workspace → spawn agent in Docker container
|
||||||
- **UK English**: colour, organisation, centre, initialise
|
- **UK English**: colour, organisation, centre, initialise
|
||||||
- **Errors**: `core.E("pkg.Method", "message", err)` — NEVER `fmt.Errorf`
|
- **Errors**: `core.E("pkg.Method", "message", err)` — NEVER `fmt.Errorf`
|
||||||
- **File I/O**: Package-level `fs` (go-io Medium) — NEVER `os.ReadFile/WriteFile`
|
- **File I/O**: Package-level `fs` (go-io Medium) — NEVER `os.ReadFile/WriteFile`
|
||||||
- **Processes**: `proc.go` helpers (go-process) — NEVER `os/exec` directly
|
- **Processes**: `s.Core().Process()` / go-process — NEVER `os/exec` directly
|
||||||
- **Strings**: `core.Contains/Split/Trim/HasPrefix/Sprintf` — NEVER `strings.*`
|
- **Strings**: `core.Contains/Split/Trim/HasPrefix/Sprintf` — NEVER `strings.*`
|
||||||
- **Returns**: `core.Result{Value, OK}` — NEVER `(value, error)` pairs
|
- **Returns**: `core.Result{Value, OK}` — NEVER `(value, error)` pairs
|
||||||
- **Comments**: Usage examples showing HOW with real values, not descriptions
|
- **Comments**: Usage examples showing HOW with real values, not descriptions
|
||||||
|
|
@ -86,12 +86,14 @@ One test file per source file. No catch-all files. Names must sort cleanly.
|
||||||
|
|
||||||
## Process Execution
|
## Process Execution
|
||||||
|
|
||||||
All external commands go through `pkg/agentic/proc.go` → go-process:
|
All external commands go through `s.Core().Process()` → go-process:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
out, err := runCmd(ctx, dir, "git", "log", "--oneline")
|
process := s.Core().Process()
|
||||||
ok := gitCmdOK(ctx, dir, "fetch", "origin", "main")
|
out := process.RunIn(ctx, dir, "git", "log", "--oneline")
|
||||||
branch := gitOutput(ctx, dir, "rev-parse", "--abbrev-ref", "HEAD")
|
ok := process.RunIn(ctx, dir, "git", "fetch", "origin", "main").OK
|
||||||
|
branchResult := process.RunIn(ctx, dir, "git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
branch := core.Trim(branchResult.Value.(string))
|
||||||
```
|
```
|
||||||
|
|
||||||
**NEVER import `os/exec`.** Zero source files do.
|
**NEVER import `os/exec`.** Zero source files do.
|
||||||
|
|
|
||||||
2
CODEX.md
2
CODEX.md
|
|
@ -42,7 +42,7 @@ c.Run()
|
||||||
- Use UK English in comments and docs.
|
- Use UK English in comments and docs.
|
||||||
- Use `core.E("pkg.Method", "message", err)` for errors. Never use `fmt.Errorf` or `errors.New`.
|
- Use `core.E("pkg.Method", "message", err)` for errors. Never use `fmt.Errorf` or `errors.New`.
|
||||||
- Use Core filesystem helpers or package-level `fs`. Never use raw `os.ReadFile`, `os.WriteFile`, or `filepath.*`.
|
- Use Core filesystem helpers or package-level `fs`. Never use raw `os.ReadFile`, `os.WriteFile`, or `filepath.*`.
|
||||||
- Route external commands through `pkg/agentic/proc.go` or `s.Core().Process()`. Never import `os/exec`.
|
- Route external commands through `s.Core().Process()`. Never import `os/exec`.
|
||||||
- Use Core string helpers such as `core.Contains`, `core.Trim`, and `core.Split` instead of `strings.*`.
|
- Use Core string helpers such as `core.Contains`, `core.Trim`, and `core.Split` instead of `strings.*`.
|
||||||
- Prefer `core.Result{Value: x, OK: true}` over `(value, error)` return pairs in Core-facing code.
|
- Prefer `core.Result{Value: x, OK: true}` over `(value, error)` return pairs in Core-facing code.
|
||||||
- Comments should show real usage examples, not restate the signature.
|
- Comments should show real usage examples, not restate the signature.
|
||||||
|
|
|
||||||
28
docs/RFC.md
28
docs/RFC.md
|
|
@ -21,7 +21,7 @@ core/go provides the primitives. core/agent composes them.
|
||||||
cmd/core-agent/main.go — entry point: core.New + Run
|
cmd/core-agent/main.go — entry point: core.New + Run
|
||||||
pkg/agentic/ — orchestration (dispatch, prep, verify, scan, commands)
|
pkg/agentic/ — orchestration (dispatch, prep, verify, scan, commands)
|
||||||
pkg/agentic/actions.go — named Action handlers (ctx, Options) → Result
|
pkg/agentic/actions.go — named Action handlers (ctx, Options) → Result
|
||||||
pkg/agentic/proc.go — process helpers via s.Core().Process()
|
pkg/agentic/pid.go — PID lifecycle helpers
|
||||||
pkg/agentic/handlers.go — IPC completion pipeline handlers
|
pkg/agentic/handlers.go — IPC completion pipeline handlers
|
||||||
pkg/agentic/status.go — workspace status (WriteAtomic + JSONMarshalString)
|
pkg/agentic/status.go — workspace status (WriteAtomic + JSONMarshalString)
|
||||||
pkg/agentic/paths.go — paths, fs (NewUnrestricted), helpers
|
pkg/agentic/paths.go — paths, fs (NewUnrestricted), helpers
|
||||||
|
|
@ -145,23 +145,13 @@ All handlers use `c.ACTION(messages.X{})` — no ChannelNotifier, no callbacks.
|
||||||
All commands via `s.Core().Process()`. Returns `core.Result` — Value is always a string.
|
All commands via `s.Core().Process()`. Returns `core.Result` — Value is always a string.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *PrepSubsystem) runCmd(ctx context.Context, dir, command string, args ...string) core.Result {
|
process := s.Core().Process()
|
||||||
return s.Core().Process().RunIn(ctx, dir, command, args...)
|
r := process.RunIn(ctx, dir, "git", "log", "--oneline", "-20")
|
||||||
|
if r.OK {
|
||||||
|
output := core.Trim(r.Value.(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PrepSubsystem) runCmdOK(ctx context.Context, dir, command string, args ...string) bool {
|
r = process.RunWithEnv(ctx, dir, []string{"GOWORK=off"}, "go", "test", "./...")
|
||||||
return s.runCmd(ctx, dir, command, args...).OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result {
|
|
||||||
return s.runCmd(ctx, dir, "git", args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...string) string {
|
|
||||||
r := s.gitCmd(ctx, dir, args...)
|
|
||||||
if !r.OK { return "" }
|
|
||||||
return core.Trim(r.Value.(string))
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
go-process is fully Result-native. `Start`, `Run`, `StartWithOptions`, `RunWithOptions` all return `core.Result`. Value is `*Process` for Start, `string` for Run. OK=true guarantees the type.
|
go-process is fully Result-native. `Start`, `Run`, `StartWithOptions`, `RunWithOptions` all return `core.Result`. Value is `*Process` for Start, `string` for Run. OK=true guarantees the type.
|
||||||
|
|
@ -409,10 +399,9 @@ c.Drive().New(core.NewOptions(
|
||||||
Every exported function MUST have a usage-example comment:
|
Every exported function MUST have a usage-example comment:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// gitCmd runs a git command in a directory.
|
// Process runs a git command in a directory.
|
||||||
//
|
//
|
||||||
// r := s.gitCmd(ctx, "/repo", "log", "--oneline")
|
// r := s.Core().Process().RunIn(ctx, "/repo", "git", "log", "--oneline")
|
||||||
func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result {
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -435,6 +424,7 @@ func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string)
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
- 2026-03-30: transport helpers preserve request and read causes, brain direct API calls surface upstream bodies, and review queue retry parsing no longer uses `MustCompile`.
|
- 2026-03-30: transport helpers preserve request and read causes, brain direct API calls surface upstream bodies, and review queue retry parsing no longer uses `MustCompile`.
|
||||||
|
- 2026-03-30: direct Core process calls replaced the `proc.go` wrapper layer; PID helpers now live in `pid.go` and the workspace template documents `c.Process()` directly.
|
||||||
- 2026-03-30: main now logs startup failures with structured context, and the workspace contract reference restored usage-example comments for the Action lifecycle messages.
|
- 2026-03-30: main now logs startup failures with structured context, and the workspace contract reference restored usage-example comments for the Action lifecycle messages.
|
||||||
- 2026-03-30: plan IDs now come from core.ID(), workspace prep validates org/repo names with core.ValidateName, and plan paths use core.SanitisePath.
|
- 2026-03-30: plan IDs now come from core.ID(), workspace prep validates org/repo names with core.ValidateName, and plan paths use core.SanitisePath.
|
||||||
- 2026-03-29: cmd/core-agent no longer rewrites `os.Args` before startup. The binary-owned commands now use named handlers, keeping the entrypoint on Core CLI primitives instead of repo-local argument mutation.
|
- 2026-03-29: cmd/core-agent no longer rewrites `os.Args` before startup. The binary-owned commands now use named handlers, keeping the entrypoint on Core CLI primitives instead of repo-local argument mutation.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ After loading all three, you have the full domain model:
|
||||||
|
|
||||||
After loading, you should be able to answer without looking at code:
|
After loading, you should be able to answer without looking at code:
|
||||||
- What does `c.Action("agentic.dispatch").Run(ctx, opts)` do?
|
- What does `c.Action("agentic.dispatch").Run(ctx, opts)` do?
|
||||||
- Why is `proc.go` being deleted?
|
- How do direct `s.Core().Process()` calls replace the old process wrapper layer?
|
||||||
- What replaces the ACTION cascade in `handlers.go`?
|
- What replaces the ACTION cascade in `handlers.go`?
|
||||||
- Which imports are disallowed and what replaces each one?
|
- Which imports are disallowed and what replaces each one?
|
||||||
- What does `c.Entitled("agentic.concurrency", 1)` check?
|
- What does `c.Entitled("agentic.concurrency", 1)` check?
|
||||||
|
|
@ -43,7 +43,7 @@ Priority order:
|
||||||
5. Replace `core.ID()` (plan.go)
|
5. Replace `core.ID()` (plan.go)
|
||||||
6. Register capabilities as named Actions (OnStartup)
|
6. Register capabilities as named Actions (OnStartup)
|
||||||
7. Replace ACTION cascade with Task pipeline (handlers.go)
|
7. Replace ACTION cascade with Task pipeline (handlers.go)
|
||||||
8. Delete `proc.go` → `s.Core().Process()` (after go-process v0.8.0)
|
8. Use `s.Core().Process()` directly in call sites. The old `proc.go` wrapper layer has been removed.
|
||||||
9. AX-7 test rename + gap fill
|
9. AX-7 test rename + gap fill
|
||||||
10. Example tests per source file
|
10. Example tests per source file
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
> Written by Cladius with full core/go + core/agent domain context (2026-03-25).
|
> Written by Cladius with full core/go + core/agent domain context (2026-03-25).
|
||||||
> Read core/go docs/RFC.md for the full spec. This plan covers what core/agent needs to change.
|
> Read core/go docs/RFC.md for the full spec. This plan covers what core/agent needs to change.
|
||||||
|
>
|
||||||
|
> Status note: the proc.go migration described below has shipped. core/agent now uses direct `s.Core().Process()` calls and `pid.go` for PID helpers. Keep this file as the original migration record.
|
||||||
|
|
||||||
## What Changed in core/go
|
## What Changed in core/go
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,16 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
repoDir := WorkspaceRepoDir(wsDir)
|
repoDir := WorkspaceRepoDir(wsDir)
|
||||||
|
process := s.Core().Process()
|
||||||
|
|
||||||
// PRs target dev — agents never merge directly to main
|
// PRs target dev — agents never merge directly to main
|
||||||
base := "dev"
|
base := "dev"
|
||||||
|
|
||||||
out := s.gitOutput(ctx, repoDir, "log", "--oneline", core.Concat("origin/", base, "..HEAD"))
|
r := process.RunIn(ctx, repoDir, "git", "log", "--oneline", core.Concat("origin/", base, "..HEAD"))
|
||||||
|
if !r.OK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := core.Trim(r.Value.(string))
|
||||||
if out == "" {
|
if out == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +42,7 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) {
|
||||||
|
|
||||||
// Push the branch to forge
|
// Push the branch to forge
|
||||||
forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo)
|
forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo)
|
||||||
if !s.gitCmdOK(ctx, repoDir, "push", forgeRemote, st.Branch) {
|
if !process.RunIn(ctx, repoDir, "git", "push", forgeRemote, st.Branch).OK {
|
||||||
if st2, err := ReadStatus(wsDir); err == nil {
|
if st2, err := ReadStatus(wsDir); err == nil {
|
||||||
st2.Question = "PR push failed"
|
st2.Question = "PR push failed"
|
||||||
writeStatus(wsDir, st2)
|
writeStatus(wsDir, st2)
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,15 @@ func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir,
|
||||||
if !r.OK {
|
if !r.OK {
|
||||||
return // no go.mod — not a Go project
|
return // no go.mod — not a Go project
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse requires from go.mod
|
// Parse requires from go.mod
|
||||||
deps := parseCoreDeps(r.Value.(string))
|
deps := parseCoreDeps(r.Value.(string))
|
||||||
if len(deps) == 0 {
|
if len(deps) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if s.ServiceRuntime == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
process := s.Core().Process()
|
||||||
|
|
||||||
// Deduplicate (dappco.re and forge.lthn.ai may map to same repo)
|
// Deduplicate (dappco.re and forge.lthn.ai may map to same repo)
|
||||||
dedupSeen := make(map[string]bool)
|
dedupSeen := make(map[string]bool)
|
||||||
|
|
@ -52,7 +55,7 @@ func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
repoURL := forgeSSHURL(org, dep.repo)
|
repoURL := forgeSSHURL(org, dep.repo)
|
||||||
if result := s.gitCmd(ctx, wsDir, "clone", "--depth=1", repoURL, dep.dir); result.OK {
|
if result := process.RunIn(ctx, wsDir, "git", "clone", "--depth=1", repoURL, dep.dir); result.OK {
|
||||||
cloned = append(cloned, dep.dir)
|
cloned = append(cloned, dep.dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, er
|
||||||
func (s *PrepSubsystem) runQA(wsDir string) bool {
|
func (s *PrepSubsystem) runQA(wsDir string) bool {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
repoDir := WorkspaceRepoDir(wsDir)
|
repoDir := WorkspaceRepoDir(wsDir)
|
||||||
|
process := s.Core().Process()
|
||||||
|
|
||||||
if fs.IsFile(core.JoinPath(repoDir, "go.mod")) {
|
if fs.IsFile(core.JoinPath(repoDir, "go.mod")) {
|
||||||
for _, args := range [][]string{
|
for _, args := range [][]string{
|
||||||
|
|
@ -450,7 +451,7 @@ func (s *PrepSubsystem) runQA(wsDir string) bool {
|
||||||
{"go", "vet", "./..."},
|
{"go", "vet", "./..."},
|
||||||
{"go", "test", "./...", "-count=1", "-timeout", "120s"},
|
{"go", "test", "./...", "-count=1", "-timeout", "120s"},
|
||||||
} {
|
} {
|
||||||
if !s.runCmdOK(ctx, repoDir, args[0], args[1:]...) {
|
if !process.RunIn(ctx, repoDir, args[0], args[1:]...).OK {
|
||||||
core.Warn("QA failed", "cmd", core.Join(" ", args...))
|
core.Warn("QA failed", "cmd", core.Join(" ", args...))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -459,17 +460,17 @@ func (s *PrepSubsystem) runQA(wsDir string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs.IsFile(core.JoinPath(repoDir, "composer.json")) {
|
if fs.IsFile(core.JoinPath(repoDir, "composer.json")) {
|
||||||
if !s.runCmdOK(ctx, repoDir, "composer", "install", "--no-interaction") {
|
if !process.RunIn(ctx, repoDir, "composer", "install", "--no-interaction").OK {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.runCmdOK(ctx, repoDir, "composer", "test")
|
return process.RunIn(ctx, repoDir, "composer", "test").OK
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs.IsFile(core.JoinPath(repoDir, "package.json")) {
|
if fs.IsFile(core.JoinPath(repoDir, "package.json")) {
|
||||||
if !s.runCmdOK(ctx, repoDir, "npm", "install") {
|
if !process.RunIn(ctx, repoDir, "npm", "install").OK {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.runCmdOK(ctx, repoDir, "npm", "test")
|
return process.RunIn(ctx, repoDir, "npm", "test").OK
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
||||||
if maxFiles <= 0 {
|
if maxFiles <= 0 {
|
||||||
maxFiles = 50
|
maxFiles = 50
|
||||||
}
|
}
|
||||||
|
process := s.Core().Process()
|
||||||
|
|
||||||
basePath := s.codePath
|
basePath := s.codePath
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
|
|
@ -83,7 +84,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch github to get current state
|
// Fetch github to get current state
|
||||||
s.gitCmdOK(ctx, repoDir, "fetch", "github")
|
process.RunIn(ctx, repoDir, "git", "fetch", "github")
|
||||||
|
|
||||||
// Check how far ahead local default branch is vs github
|
// Check how far ahead local default branch is vs github
|
||||||
localBase := s.DefaultBranch(repoDir)
|
localBase := s.DefaultBranch(repoDir)
|
||||||
|
|
@ -119,7 +120,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
||||||
|
|
||||||
// Push local main to github dev (explicit main, not HEAD)
|
// Push local main to github dev (explicit main, not HEAD)
|
||||||
base := s.DefaultBranch(repoDir)
|
base := s.DefaultBranch(repoDir)
|
||||||
if r := s.gitCmd(ctx, repoDir, "push", "github", core.Concat(base, ":refs/heads/dev"), "--force"); !r.OK {
|
if r := process.RunIn(ctx, repoDir, "git", "push", "github", core.Concat(base, ":refs/heads/dev"), "--force"); !r.OK {
|
||||||
sync.Skipped = core.Sprintf("push failed: %s", r.Value)
|
sync.Skipped = core.Sprintf("push failed: %s", r.Value)
|
||||||
synced = append(synced, sync)
|
synced = append(synced, sync)
|
||||||
continue
|
continue
|
||||||
|
|
@ -148,9 +149,10 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
||||||
// createGitHubPR creates a PR from dev → main using the gh CLI.
|
// createGitHubPR creates a PR from dev → main using the gh CLI.
|
||||||
func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string, commits, files int) (string, error) {
|
func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string, commits, files int) (string, error) {
|
||||||
ghRepo := core.Sprintf("%s/%s", GitHubOrg(), repo)
|
ghRepo := core.Sprintf("%s/%s", GitHubOrg(), repo)
|
||||||
|
process := s.Core().Process()
|
||||||
|
|
||||||
// Check if there's already an open PR from dev
|
// Check if there's already an open PR from dev
|
||||||
r := s.runCmd(ctx, repoDir, "gh", "pr", "list", "--repo", ghRepo, "--head", "dev", "--state", "open", "--json", "url", "--limit", "1")
|
r := process.RunIn(ctx, repoDir, "gh", "pr", "list", "--repo", ghRepo, "--head", "dev", "--state", "open", "--json", "url", "--limit", "1")
|
||||||
if r.OK {
|
if r.OK {
|
||||||
out := r.Value.(string)
|
out := r.Value.(string)
|
||||||
if core.Contains(out, "url") {
|
if core.Contains(out, "url") {
|
||||||
|
|
@ -166,7 +168,7 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string
|
||||||
|
|
||||||
title := core.Sprintf("[sync] %s: %d commits, %d files", repo, commits, files)
|
title := core.Sprintf("[sync] %s: %d commits, %d files", repo, commits, files)
|
||||||
|
|
||||||
r = s.runCmd(ctx, repoDir, "gh", "pr", "create",
|
r = process.RunIn(ctx, repoDir, "gh", "pr", "create",
|
||||||
"--repo", ghRepo, "--head", "dev", "--base", "main",
|
"--repo", ghRepo, "--head", "dev", "--base", "main",
|
||||||
"--title", title, "--body", body)
|
"--title", title, "--body", body)
|
||||||
if !r.OK {
|
if !r.OK {
|
||||||
|
|
@ -183,23 +185,31 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string
|
||||||
|
|
||||||
// ensureDevBranch creates the dev branch on GitHub if it doesn't exist.
|
// ensureDevBranch creates the dev branch on GitHub if it doesn't exist.
|
||||||
func (s *PrepSubsystem) ensureDevBranch(repoDir string) {
|
func (s *PrepSubsystem) ensureDevBranch(repoDir string) {
|
||||||
s.gitCmdOK(context.Background(), repoDir, "push", "github", "HEAD:refs/heads/dev")
|
s.Core().Process().RunIn(context.Background(), repoDir, "git", "push", "github", "HEAD:refs/heads/dev")
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasRemote checks if a git remote exists.
|
// hasRemote checks if a git remote exists.
|
||||||
func (s *PrepSubsystem) hasRemote(repoDir, name string) bool {
|
func (s *PrepSubsystem) hasRemote(repoDir, name string) bool {
|
||||||
return s.gitCmdOK(context.Background(), repoDir, "remote", "get-url", name)
|
return s.Core().Process().RunIn(context.Background(), repoDir, "git", "remote", "get-url", name).OK
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitsAhead returns how many commits HEAD is ahead of the ref.
|
// commitsAhead returns how many commits HEAD is ahead of the ref.
|
||||||
func (s *PrepSubsystem) commitsAhead(repoDir, base, head string) int {
|
func (s *PrepSubsystem) commitsAhead(repoDir, base, head string) int {
|
||||||
out := s.gitOutput(context.Background(), repoDir, "rev-list", core.Concat(base, "..", head), "--count")
|
r := s.Core().Process().RunIn(context.Background(), repoDir, "git", "rev-list", core.Concat(base, "..", head), "--count")
|
||||||
|
if !r.OK {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
out := core.Trim(r.Value.(string))
|
||||||
return parseInt(out)
|
return parseInt(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filesChanged returns the number of files changed between two refs.
|
// filesChanged returns the number of files changed between two refs.
|
||||||
func (s *PrepSubsystem) filesChanged(repoDir, base, head string) int {
|
func (s *PrepSubsystem) filesChanged(repoDir, base, head string) int {
|
||||||
out := s.gitOutput(context.Background(), repoDir, "diff", "--name-only", core.Concat(base, "..", head))
|
r := s.Core().Process().RunIn(context.Background(), repoDir, "git", "diff", "--name-only", core.Concat(base, "..", head))
|
||||||
|
if !r.OK {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
out := core.Trim(r.Value.(string))
|
||||||
if out == "" {
|
if out == "" {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,14 +156,16 @@ func AgentName() string {
|
||||||
// base := s.DefaultBranch("./src")
|
// base := s.DefaultBranch("./src")
|
||||||
func (s *PrepSubsystem) DefaultBranch(repoDir string) string {
|
func (s *PrepSubsystem) DefaultBranch(repoDir string) string {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if ref := s.gitOutput(ctx, repoDir, "symbolic-ref", "refs/remotes/origin/HEAD", "--short"); ref != "" {
|
process := s.Core().Process()
|
||||||
|
if r := process.RunIn(ctx, repoDir, "git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short"); r.OK {
|
||||||
|
ref := core.Trim(r.Value.(string))
|
||||||
if core.HasPrefix(ref, "origin/") {
|
if core.HasPrefix(ref, "origin/") {
|
||||||
return core.TrimPrefix(ref, "origin/")
|
return core.TrimPrefix(ref, "origin/")
|
||||||
}
|
}
|
||||||
return ref
|
return ref
|
||||||
}
|
}
|
||||||
for _, branch := range []string{"main", "master"} {
|
for _, branch := range []string{"main", "master"} {
|
||||||
if s.gitCmdOK(ctx, repoDir, "rev-parse", "--verify", branch) {
|
if process.RunIn(ctx, repoDir, "git", "rev-parse", "--verify", branch).OK {
|
||||||
return branch
|
return branch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
pkg/agentic/pid.go
Normal file
25
pkg/agentic/pid.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package agentic
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
// PIDAlive checks if an OS process is still alive via PID signal check.
|
||||||
|
//
|
||||||
|
// if agentic.PIDAlive(pid) { ... }
|
||||||
|
func PIDAlive(pid int) bool {
|
||||||
|
if pid > 0 {
|
||||||
|
return syscall.Kill(pid, 0) == nil
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PIDTerminate terminates a process via SIGTERM.
|
||||||
|
//
|
||||||
|
// if agentic.PIDTerminate(pid) { ... }
|
||||||
|
func PIDTerminate(pid int) bool {
|
||||||
|
if pid > 0 {
|
||||||
|
return syscall.Kill(pid, syscall.SIGTERM) == nil
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
19
pkg/agentic/pid_example_test.go
Normal file
19
pkg/agentic/pid_example_test.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package agentic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExamplePIDAlive() {
|
||||||
|
core.Println(PIDAlive(os.Getpid()))
|
||||||
|
// Output: true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExamplePIDTerminate() {
|
||||||
|
core.Println(PIDTerminate(0))
|
||||||
|
// Output: false
|
||||||
|
}
|
||||||
107
pkg/agentic/pid_test.go
Normal file
107
pkg/agentic/pid_test.go
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package agentic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
|
"dappco.re/go/core/process"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testPrep is the package-level PrepSubsystem for tests that need process execution.
|
||||||
|
var testPrep *PrepSubsystem
|
||||||
|
|
||||||
|
// testCore is the package-level Core with go-process registered.
|
||||||
|
var testCore *core.Core
|
||||||
|
|
||||||
|
// TestMain sets up a PrepSubsystem with go-process registered for all tests in the package.
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
testCore = core.New(
|
||||||
|
core.WithService(ProcessRegister),
|
||||||
|
)
|
||||||
|
testCore.ServiceStartup(context.Background(), nil)
|
||||||
|
|
||||||
|
// Enable pipeline feature flags (matches Register defaults)
|
||||||
|
testCore.Config().Enable("auto-qa")
|
||||||
|
testCore.Config().Enable("auto-pr")
|
||||||
|
testCore.Config().Enable("auto-merge")
|
||||||
|
testCore.Config().Enable("auto-ingest")
|
||||||
|
|
||||||
|
testPrep = &PrepSubsystem{
|
||||||
|
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
||||||
|
backoff: make(map[string]time.Time),
|
||||||
|
failCount: make(map[string]int),
|
||||||
|
}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPrepWithProcess creates a PrepSubsystem wired to testCore for tests that
|
||||||
|
// need process execution via s.Core().Process().
|
||||||
|
func newPrepWithProcess() *PrepSubsystem {
|
||||||
|
return &PrepSubsystem{
|
||||||
|
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
||||||
|
backoff: make(map[string]time.Time),
|
||||||
|
failCount: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PIDAlive ---
|
||||||
|
|
||||||
|
func TestPid_PIDAlive_Good(t *testing.T) {
|
||||||
|
pid, _ := strconv.Atoi(core.Env("PID"))
|
||||||
|
assert.True(t, PIDAlive(pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPid_PIDAlive_Bad(t *testing.T) {
|
||||||
|
assert.False(t, PIDAlive(999999))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPid_PIDAlive_Ugly(t *testing.T) {
|
||||||
|
assert.False(t, PIDAlive(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PIDTerminate ---
|
||||||
|
|
||||||
|
func TestPid_PIDTerminate_Good(t *testing.T) {
|
||||||
|
r := testCore.Process().Start(context.Background(), core.NewOptions(
|
||||||
|
core.Option{Key: "command", Value: "sleep"},
|
||||||
|
core.Option{Key: "args", Value: []string{"30"}},
|
||||||
|
core.Option{Key: "detach", Value: true},
|
||||||
|
))
|
||||||
|
require.True(t, r.OK)
|
||||||
|
|
||||||
|
proc, ok := r.Value.(*process.Process)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pid := proc.Info().PID
|
||||||
|
require.NotZero(t, pid)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = proc.Kill()
|
||||||
|
}()
|
||||||
|
|
||||||
|
assert.True(t, PIDTerminate(pid))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-proc.Done():
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("PIDTerminate did not stop the process")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, PIDAlive(pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPid_PIDTerminate_Bad(t *testing.T) {
|
||||||
|
assert.False(t, PIDTerminate(999999))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPid_PIDTerminate_Ugly(t *testing.T) {
|
||||||
|
assert.False(t, PIDTerminate(0))
|
||||||
|
}
|
||||||
|
|
@ -65,11 +65,15 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
|
||||||
}
|
}
|
||||||
|
|
||||||
if st.Branch == "" {
|
if st.Branch == "" {
|
||||||
branch := s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD")
|
process := s.Core().Process()
|
||||||
if branch == "" {
|
r := process.RunIn(ctx, repoDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
if !r.OK {
|
||||||
|
return nil, CreatePROutput{}, core.E("createPR", "failed to detect branch", nil)
|
||||||
|
}
|
||||||
|
st.Branch = core.Trim(r.Value.(string))
|
||||||
|
if st.Branch == "" {
|
||||||
return nil, CreatePROutput{}, core.E("createPR", "failed to detect branch", nil)
|
return nil, CreatePROutput{}, core.E("createPR", "failed to detect branch", nil)
|
||||||
}
|
}
|
||||||
st.Branch = branch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
org := st.Org
|
org := st.Org
|
||||||
|
|
@ -107,7 +111,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
|
||||||
|
|
||||||
// Push branch to Forge (origin is the local clone, not Forge)
|
// Push branch to Forge (origin is the local clone, not Forge)
|
||||||
forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo)
|
forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo)
|
||||||
r := s.gitCmd(ctx, repoDir, "push", forgeRemote, st.Branch)
|
r := s.Core().Process().RunIn(ctx, repoDir, "git", "push", forgeRemote, st.Branch)
|
||||||
if !r.OK {
|
if !r.OK {
|
||||||
return nil, CreatePROutput{}, core.E("createPR", core.Concat("git push failed: ", r.Value.(string)), nil)
|
return nil, CreatePROutput{}, core.E("createPR", core.Concat("git push failed: ", r.Value.(string)), nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -431,6 +431,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
||||||
|
|
||||||
// Source repo path — org and repo were validated by workspaceDirResult.
|
// Source repo path — org and repo were validated by workspaceDirResult.
|
||||||
repoPath := core.JoinPath(s.codePath, input.Org, input.Repo)
|
repoPath := core.JoinPath(s.codePath, input.Org, input.Repo)
|
||||||
|
process := s.Core().Process()
|
||||||
|
|
||||||
// Ensure meta directory exists
|
// Ensure meta directory exists
|
||||||
if r := fs.EnsureDir(metaDir); !r.OK {
|
if r := fs.EnsureDir(metaDir); !r.OK {
|
||||||
|
|
@ -445,17 +446,21 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
||||||
// Preserve the current branch on resume. Pull it only if it exists on
|
// Preserve the current branch on resume. Pull it only if it exists on
|
||||||
// origin; otherwise refresh the default branch refs without abandoning the
|
// origin; otherwise refresh the default branch refs without abandoning the
|
||||||
// workspace branch.
|
// workspace branch.
|
||||||
currentBranch := s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD")
|
r := process.RunIn(ctx, repoDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
currentBranch := ""
|
||||||
|
if r.OK {
|
||||||
|
currentBranch = core.Trim(r.Value.(string))
|
||||||
|
}
|
||||||
defaultBranch := s.DefaultBranch(repoDir)
|
defaultBranch := s.DefaultBranch(repoDir)
|
||||||
if currentBranch == "" || currentBranch == "HEAD" {
|
if currentBranch == "" || currentBranch == "HEAD" {
|
||||||
currentBranch = defaultBranch
|
currentBranch = defaultBranch
|
||||||
}
|
}
|
||||||
if currentBranch != "" {
|
if currentBranch != "" {
|
||||||
s.gitCmd(ctx, repoDir, "checkout", currentBranch)
|
process.RunIn(ctx, repoDir, "git", "checkout", currentBranch)
|
||||||
if s.gitCmdOK(ctx, repoDir, "ls-remote", "--exit-code", "--heads", "origin", currentBranch) {
|
if process.RunIn(ctx, repoDir, "git", "ls-remote", "--exit-code", "--heads", "origin", currentBranch).OK {
|
||||||
s.gitCmd(ctx, repoDir, "pull", "--ff-only", "origin", currentBranch)
|
process.RunIn(ctx, repoDir, "git", "pull", "--ff-only", "origin", currentBranch)
|
||||||
} else if defaultBranch != "" {
|
} else if defaultBranch != "" {
|
||||||
s.gitCmd(ctx, repoDir, "fetch", "origin", defaultBranch)
|
process.RunIn(ctx, repoDir, "git", "fetch", "origin", defaultBranch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -470,7 +475,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
||||||
|
|
||||||
if !resumed {
|
if !resumed {
|
||||||
// Clone repo into repo/
|
// Clone repo into repo/
|
||||||
if r := s.gitCmd(ctx, ".", "clone", repoPath, repoDir); !r.OK {
|
if r := process.RunIn(ctx, ".", "git", "clone", repoPath, repoDir); !r.OK {
|
||||||
return nil, PrepOutput{}, core.E("prep", core.Concat("git clone failed for ", input.Repo), nil)
|
return nil, PrepOutput{}, core.E("prep", core.Concat("git clone failed for ", input.Repo), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,13 +492,16 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
||||||
}
|
}
|
||||||
branchName := core.Sprintf("agent/%s", taskSlug)
|
branchName := core.Sprintf("agent/%s", taskSlug)
|
||||||
|
|
||||||
if r := s.gitCmd(ctx, repoDir, "checkout", "-b", branchName); !r.OK {
|
if r := process.RunIn(ctx, repoDir, "git", "checkout", "-b", branchName); !r.OK {
|
||||||
return nil, PrepOutput{}, core.E("prep.branch", core.Sprintf("failed to create branch %q", branchName), nil)
|
return nil, PrepOutput{}, core.E("prep.branch", core.Sprintf("failed to create branch %q", branchName), nil)
|
||||||
}
|
}
|
||||||
out.Branch = branchName
|
out.Branch = branchName
|
||||||
} else {
|
} else {
|
||||||
// Resume: read branch from existing checkout
|
// Resume: read branch from existing checkout
|
||||||
out.Branch = s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD")
|
r := process.RunIn(ctx, repoDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
if r.OK {
|
||||||
|
out.Branch = core.Trim(r.Value.(string))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite CODEX.md with language-specific version if needed.
|
// Overwrite CODEX.md with language-specific version if needed.
|
||||||
|
|
@ -517,7 +525,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
||||||
if !fs.IsDir(docsDir) {
|
if !fs.IsDir(docsDir) {
|
||||||
docsRepo := core.JoinPath(s.codePath, input.Org, "docs")
|
docsRepo := core.JoinPath(s.codePath, input.Org, "docs")
|
||||||
if fs.IsDir(core.JoinPath(docsRepo, ".git")) {
|
if fs.IsDir(core.JoinPath(docsRepo, ".git")) {
|
||||||
s.gitCmd(ctx, ".", "clone", "--depth", "1", docsRepo, docsDir)
|
process.RunIn(ctx, ".", "git", "clone", "--depth", "1", docsRepo, docsDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -789,7 +797,11 @@ func (s *PrepSubsystem) findConsumersList(repo string) (string, int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PrepSubsystem) getGitLog(repoPath string) string {
|
func (s *PrepSubsystem) getGitLog(repoPath string) string {
|
||||||
return s.gitOutput(context.Background(), repoPath, "log", "--oneline", "-20")
|
r := s.Core().Process().RunIn(context.Background(), repoPath, "git", "log", "--oneline", "-20")
|
||||||
|
if !r.OK {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return core.Trim(r.Value.(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PrepSubsystem) pullWikiContent(ctx context.Context, org, repo string) string {
|
func (s *PrepSubsystem) pullWikiContent(ctx context.Context, org, repo string) string {
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
|
||||||
|
|
||||||
// Process execution helpers — routes all commands through s.Core().Process().
|
|
||||||
// No direct os/exec or go-process imports.
|
|
||||||
//
|
|
||||||
// Requires go-process to be registered with Core via:
|
|
||||||
//
|
|
||||||
// core.New(core.WithService(agentic.ProcessRegister))
|
|
||||||
|
|
||||||
package agentic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
core "dappco.re/go/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
// runCmd executes a command in a directory. Returns Result{Value: string, OK: bool}.
|
|
||||||
//
|
|
||||||
// r := s.runCmd(ctx, repoDir, "git", "log", "--oneline", "-20")
|
|
||||||
// if r.OK { output := r.Value.(string) }
|
|
||||||
func (s *PrepSubsystem) runCmd(ctx context.Context, dir string, command string, args ...string) core.Result {
|
|
||||||
return s.Core().Process().RunIn(ctx, dir, command, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCmdEnv executes a command with additional environment variables.
|
|
||||||
//
|
|
||||||
// r := s.runCmdEnv(ctx, repoDir, []string{"GOWORK=off"}, "go", "test", "./...")
|
|
||||||
func (s *PrepSubsystem) runCmdEnv(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
|
|
||||||
return s.Core().Process().RunWithEnv(ctx, dir, env, command, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCmdOK executes a command and returns true if it exits 0.
|
|
||||||
//
|
|
||||||
// if s.runCmdOK(ctx, repoDir, "go", "build", "./...") { ... }
|
|
||||||
func (s *PrepSubsystem) runCmdOK(ctx context.Context, dir string, command string, args ...string) bool {
|
|
||||||
return s.runCmd(ctx, dir, command, args...).OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// gitCmd runs a git command in the given directory.
|
|
||||||
//
|
|
||||||
// r := s.gitCmd(ctx, repoDir, "log", "--oneline", "-20")
|
|
||||||
func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result {
|
|
||||||
return s.runCmd(ctx, dir, "git", args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// gitCmdOK runs a git command and returns true if it exits 0.
|
|
||||||
//
|
|
||||||
// if s.gitCmdOK(ctx, repoDir, "fetch", "origin", "main") { ... }
|
|
||||||
func (s *PrepSubsystem) gitCmdOK(ctx context.Context, dir string, args ...string) bool {
|
|
||||||
return s.gitCmd(ctx, dir, args...).OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// gitOutput runs a git command and returns trimmed stdout.
|
|
||||||
//
|
|
||||||
// branch := s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD")
|
|
||||||
func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...string) string {
|
|
||||||
r := s.gitCmd(ctx, dir, args...)
|
|
||||||
if !r.OK {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return core.Trim(r.Value.(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Process lifecycle helpers ---
|
|
||||||
|
|
||||||
// PIDAlive checks if an OS process is still alive via PID signal check.
|
|
||||||
//
|
|
||||||
// if agentic.PIDAlive(st.PID) { ... }
|
|
||||||
func PIDAlive(pid int) bool {
|
|
||||||
if pid > 0 {
|
|
||||||
return syscall.Kill(pid, 0) == nil
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// PIDTerminate terminates a process via SIGTERM.
|
|
||||||
//
|
|
||||||
// if agentic.PIDTerminate(st.PID) { ... }
|
|
||||||
func PIDTerminate(pid int) bool {
|
|
||||||
if pid > 0 {
|
|
||||||
return syscall.Kill(pid, syscall.SIGTERM) == nil
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
|
||||||
|
|
||||||
package agentic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
core "dappco.re/go/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExamplePrepSubsystem_runCmd() {
|
|
||||||
r := testPrep.runCmd(context.Background(), ".", "echo", "hello")
|
|
||||||
core.Println(r.OK)
|
|
||||||
// Output: true
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExamplePrepSubsystem_gitCmd() {
|
|
||||||
r := testPrep.gitCmd(context.Background(), ".", "--version")
|
|
||||||
core.Println(r.OK)
|
|
||||||
// Output: true
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExamplePrepSubsystem_gitOutput() {
|
|
||||||
version := testPrep.gitOutput(context.Background(), ".", "--version")
|
|
||||||
core.Println(core.HasPrefix(version, "git version"))
|
|
||||||
// Output: true
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExamplePrepSubsystem_runCmdOK() {
|
|
||||||
ok := testPrep.runCmdOK(context.Background(), ".", "echo", "test")
|
|
||||||
core.Println(ok)
|
|
||||||
// Output: true
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExamplePrepSubsystem_gitCmdOK() {
|
|
||||||
ok := testPrep.gitCmdOK(context.Background(), ".", "--version")
|
|
||||||
core.Println(ok)
|
|
||||||
// Output: true
|
|
||||||
}
|
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
|
||||||
|
|
||||||
package agentic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
core "dappco.re/go/core"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// testPrep is the package-level PrepSubsystem for tests that need process execution.
|
|
||||||
var testPrep *PrepSubsystem
|
|
||||||
|
|
||||||
// testCore is the package-level Core with go-process registered.
|
|
||||||
var testCore *core.Core
|
|
||||||
|
|
||||||
// TestMain sets up a PrepSubsystem with go-process registered for all tests in the package.
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
testCore = core.New(
|
|
||||||
core.WithService(ProcessRegister),
|
|
||||||
)
|
|
||||||
testCore.ServiceStartup(context.Background(), nil)
|
|
||||||
|
|
||||||
// Enable pipeline feature flags (matches Register defaults)
|
|
||||||
testCore.Config().Enable("auto-qa")
|
|
||||||
testCore.Config().Enable("auto-pr")
|
|
||||||
testCore.Config().Enable("auto-merge")
|
|
||||||
testCore.Config().Enable("auto-ingest")
|
|
||||||
|
|
||||||
testPrep = &PrepSubsystem{
|
|
||||||
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
|
||||||
backoff: make(map[string]time.Time),
|
|
||||||
failCount: make(map[string]int),
|
|
||||||
}
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPrepWithProcess creates a PrepSubsystem wired to testCore for tests that
|
|
||||||
// need process execution via s.Core().Process().
|
|
||||||
func newPrepWithProcess() *PrepSubsystem {
|
|
||||||
return &PrepSubsystem{
|
|
||||||
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
|
||||||
backoff: make(map[string]time.Time),
|
|
||||||
failCount: make(map[string]int),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- runCmd ---
|
|
||||||
|
|
||||||
func TestProc_RunCmd_Good(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
r := testPrep.runCmd(context.Background(), dir, "echo", "hello")
|
|
||||||
assert.True(t, r.OK)
|
|
||||||
assert.Contains(t, core.Trim(r.Value.(string)), "hello")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_RunCmd_Bad(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
r := testPrep.runCmd(context.Background(), dir, "nonexistent-command-xyz")
|
|
||||||
assert.False(t, r.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_RunCmd_Ugly(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
// Empty command string — should error
|
|
||||||
r := testPrep.runCmd(context.Background(), dir, "")
|
|
||||||
assert.False(t, r.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- runCmdEnv ---
|
|
||||||
|
|
||||||
func TestProc_RunCmdEnv_Good(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
r := testPrep.runCmdEnv(context.Background(), dir, []string{"MY_CUSTOM_VAR=hello_test"}, "env")
|
|
||||||
assert.True(t, r.OK)
|
|
||||||
assert.Contains(t, r.Value.(string), "MY_CUSTOM_VAR=hello_test")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_RunCmdEnv_Bad(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
r := testPrep.runCmdEnv(context.Background(), dir, []string{"FOO=bar"}, "nonexistent-command-xyz")
|
|
||||||
assert.False(t, r.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_RunCmdEnv_Ugly(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
// Empty env slice — should work fine, just no extra vars
|
|
||||||
r := testPrep.runCmdEnv(context.Background(), dir, []string{}, "echo", "works")
|
|
||||||
assert.True(t, r.OK)
|
|
||||||
assert.Contains(t, core.Trim(r.Value.(string)), "works")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- runCmdOK ---
|
|
||||||
|
|
||||||
func TestProc_RunCmdOK_Good(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
assert.True(t, testPrep.runCmdOK(context.Background(), dir, "echo", "ok"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_RunCmdOK_Bad(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
assert.False(t, testPrep.runCmdOK(context.Background(), dir, "nonexistent-command-xyz"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_RunCmdOK_Ugly(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
// "false" command returns exit 1
|
|
||||||
assert.False(t, testPrep.runCmdOK(context.Background(), dir, "false"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- gitCmd ---
|
|
||||||
|
|
||||||
func TestProc_GitCmd_Good(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
r := testPrep.gitCmd(context.Background(), dir, "--version")
|
|
||||||
assert.True(t, r.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_GitCmd_Bad(t *testing.T) {
|
|
||||||
// git log in a non-git dir should fail
|
|
||||||
dir := t.TempDir()
|
|
||||||
r := testPrep.gitCmd(context.Background(), dir, "log")
|
|
||||||
assert.False(t, r.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_GitCmd_Ugly(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
// Empty args — git with no arguments exits 1
|
|
||||||
r := testPrep.gitCmd(context.Background(), dir)
|
|
||||||
assert.False(t, r.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- gitCmdOK ---
|
|
||||||
|
|
||||||
func TestProc_GitCmdOK_Good(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
assert.True(t, testPrep.gitCmdOK(context.Background(), dir, "--version"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_GitCmdOK_Bad(t *testing.T) {
|
|
||||||
// git log in non-git dir returns false
|
|
||||||
dir := t.TempDir()
|
|
||||||
assert.False(t, testPrep.gitCmdOK(context.Background(), dir, "log"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_GitCmdOK_Ugly(t *testing.T) {
|
|
||||||
// Empty dir string — git may use cwd, which may or may not be a repo
|
|
||||||
// Just ensure no panic
|
|
||||||
assert.NotPanics(t, func() {
|
|
||||||
testPrep.gitCmdOK(context.Background(), "", "--version")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- gitOutput ---
|
|
||||||
|
|
||||||
func TestProc_GitOutput_Good(t *testing.T) {
|
|
||||||
dir := initTestRepo(t)
|
|
||||||
branch := testPrep.gitOutput(context.Background(), dir, "rev-parse", "--abbrev-ref", "HEAD")
|
|
||||||
assert.Equal(t, "main", branch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_GitOutput_Bad(t *testing.T) {
|
|
||||||
// Non-git dir returns empty string
|
|
||||||
dir := t.TempDir()
|
|
||||||
out := testPrep.gitOutput(context.Background(), dir, "rev-parse", "--abbrev-ref", "HEAD")
|
|
||||||
assert.Equal(t, "", out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_GitOutput_Ugly(t *testing.T) {
|
|
||||||
// Failed command returns empty string
|
|
||||||
dir := t.TempDir()
|
|
||||||
out := testPrep.gitOutput(context.Background(), dir, "log", "--oneline", "-5")
|
|
||||||
assert.Equal(t, "", out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PIDAlive ---
|
|
||||||
|
|
||||||
func TestProc_PIDAlive_Good(t *testing.T) {
|
|
||||||
// Own PID should be running
|
|
||||||
pid, _ := strconv.Atoi(core.Env("PID"))
|
|
||||||
assert.True(t, PIDAlive(pid))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_PIDAlive_Bad(t *testing.T) {
|
|
||||||
// PID 999999 should not be running (extremely unlikely to exist)
|
|
||||||
assert.False(t, PIDAlive(999999))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_PIDAlive_Ugly(t *testing.T) {
|
|
||||||
// PID 0 — should return false (invalid PID guard: pid > 0 is false for 0)
|
|
||||||
assert.False(t, PIDAlive(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PIDTerminate ---
|
|
||||||
|
|
||||||
func TestProc_PIDTerminate_Good(t *testing.T) {
|
|
||||||
t.Skip("would need real process to kill")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_PIDTerminate_Bad(t *testing.T) {
|
|
||||||
// PID 999999 should fail to kill
|
|
||||||
assert.False(t, PIDTerminate(999999))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProc_PIDTerminate_Ugly(t *testing.T) {
|
|
||||||
// PID 0 — pid > 0 guard returns false
|
|
||||||
assert.False(t, PIDTerminate(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- initTestRepo creates a git repo with commits for proc tests ---
|
|
||||||
|
|
||||||
func initTestRepo(t *testing.T) string {
|
|
||||||
t.Helper()
|
|
||||||
dir := t.TempDir()
|
|
||||||
gitEnv := []string{
|
|
||||||
"GIT_AUTHOR_NAME=Test",
|
|
||||||
"GIT_AUTHOR_EMAIL=test@test.com",
|
|
||||||
"GIT_COMMITTER_NAME=Test",
|
|
||||||
"GIT_COMMITTER_EMAIL=test@test.com",
|
|
||||||
}
|
|
||||||
run := func(args ...string) {
|
|
||||||
t.Helper()
|
|
||||||
r := testCore.Process().RunWithEnv(context.Background(), dir, gitEnv, args[0], args[1:]...)
|
|
||||||
require.True(t, r.OK, "cmd %v failed: %s", args, r.Value)
|
|
||||||
}
|
|
||||||
run("git", "init", "-b", "main")
|
|
||||||
run("git", "config", "user.name", "Test")
|
|
||||||
run("git", "config", "user.email", "test@test.com")
|
|
||||||
require.True(t, fs.Write(core.JoinPath(dir, "README.md"), "# Test").OK)
|
|
||||||
run("git", "add", "README.md")
|
|
||||||
run("git", "commit", "-m", "initial commit")
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
@ -163,6 +163,7 @@ func (s *PrepSubsystem) findReviewCandidates(basePath string) []string {
|
||||||
// reviewRepo runs CodeRabbit on a single repo and takes action.
|
// reviewRepo runs CodeRabbit on a single repo and takes action.
|
||||||
func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer string, dryRun, localOnly bool) ReviewResult {
|
func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer string, dryRun, localOnly bool) ReviewResult {
|
||||||
result := ReviewResult{Repo: repo}
|
result := ReviewResult{Repo: repo}
|
||||||
|
process := s.Core().Process()
|
||||||
|
|
||||||
// Check saved rate limit
|
// Check saved rate limit
|
||||||
if rl := s.loadRateLimitState(); rl != nil && rl.Limited && time.Now().Before(rl.RetryAt) {
|
if rl := s.loadRateLimitState(); rl != nil && rl.Limited && time.Now().Before(rl.RetryAt) {
|
||||||
|
|
@ -176,7 +177,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer
|
||||||
reviewer = "coderabbit"
|
reviewer = "coderabbit"
|
||||||
}
|
}
|
||||||
command, args := s.buildReviewCommand(repoDir, reviewer)
|
command, args := s.buildReviewCommand(repoDir, reviewer)
|
||||||
r := s.runCmd(ctx, repoDir, command, args...)
|
r := process.RunIn(ctx, repoDir, command, args...)
|
||||||
output, _ := r.Value.(string)
|
output, _ := r.Value.(string)
|
||||||
|
|
||||||
// Parse rate limit (both reviewers use similar patterns)
|
// Parse rate limit (both reviewers use similar patterns)
|
||||||
|
|
@ -250,14 +251,15 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer
|
||||||
|
|
||||||
// pushAndMerge pushes to GitHub dev and merges the PR.
|
// pushAndMerge pushes to GitHub dev and merges the PR.
|
||||||
func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string) error {
|
func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string) error {
|
||||||
if r := s.gitCmd(ctx, repoDir, "push", "github", "HEAD:refs/heads/dev", "--force"); !r.OK {
|
process := s.Core().Process()
|
||||||
|
if r := process.RunIn(ctx, repoDir, "git", "push", "github", "HEAD:refs/heads/dev", "--force"); !r.OK {
|
||||||
return core.E("pushAndMerge", core.Concat("push failed: ", r.Value.(string)), nil)
|
return core.E("pushAndMerge", core.Concat("push failed: ", r.Value.(string)), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark PR ready if draft
|
// Mark PR ready if draft
|
||||||
s.runCmdOK(ctx, repoDir, "gh", "pr", "ready", "--repo", core.Concat(GitHubOrg(), "/", repo))
|
process.RunIn(ctx, repoDir, "gh", "pr", "ready", "--repo", core.Concat(GitHubOrg(), "/", repo))
|
||||||
|
|
||||||
if r := s.runCmd(ctx, repoDir, "gh", "pr", "merge", "--merge", "--delete-branch"); !r.OK {
|
if r := process.RunIn(ctx, repoDir, "gh", "pr", "merge", "--merge", "--delete-branch"); !r.OK {
|
||||||
return core.E("pushAndMerge", core.Concat("merge failed: ", r.Value.(string)), nil)
|
return core.E("pushAndMerge", core.Concat("merge failed: ", r.Value.(string)), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,14 +104,15 @@ func (s *PrepSubsystem) attemptVerifyAndMerge(repoDir, org, repo, branch string,
|
||||||
// rebaseBranch rebases the current branch onto the default branch and force-pushes.
|
// rebaseBranch rebases the current branch onto the default branch and force-pushes.
|
||||||
func (s *PrepSubsystem) rebaseBranch(repoDir, branch string) bool {
|
func (s *PrepSubsystem) rebaseBranch(repoDir, branch string) bool {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
process := s.Core().Process()
|
||||||
base := s.DefaultBranch(repoDir)
|
base := s.DefaultBranch(repoDir)
|
||||||
|
|
||||||
if !s.gitCmdOK(ctx, repoDir, "fetch", "origin", base) {
|
if !process.RunIn(ctx, repoDir, "git", "fetch", "origin", base).OK {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.gitCmdOK(ctx, repoDir, "rebase", core.Concat("origin/", base)) {
|
if !process.RunIn(ctx, repoDir, "git", "rebase", core.Concat("origin/", base)).OK {
|
||||||
s.gitCmdOK(ctx, repoDir, "rebase", "--abort")
|
process.RunIn(ctx, repoDir, "git", "rebase", "--abort")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,7 +126,7 @@ func (s *PrepSubsystem) rebaseBranch(repoDir, branch string) bool {
|
||||||
repo = st.Repo
|
repo = st.Repo
|
||||||
}
|
}
|
||||||
forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, repo)
|
forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, repo)
|
||||||
return s.gitCmdOK(ctx, repoDir, "push", "--force-with-lease", forgeRemote, branch)
|
return process.RunIn(ctx, repoDir, "git", "push", "--force-with-lease", forgeRemote, branch).OK
|
||||||
}
|
}
|
||||||
|
|
||||||
// flagForReview adds the "needs-review" label to the PR via Forge API.
|
// flagForReview adds the "needs-review" label to the PR via Forge API.
|
||||||
|
|
@ -217,7 +218,8 @@ func (s *PrepSubsystem) runVerification(repoDir string) verifyResult {
|
||||||
|
|
||||||
func (s *PrepSubsystem) runGoTests(repoDir string) verifyResult {
|
func (s *PrepSubsystem) runGoTests(repoDir string) verifyResult {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
r := s.runCmdEnv(ctx, repoDir, []string{"GOWORK=off"}, "go", "test", "./...", "-count=1", "-timeout", "120s")
|
process := s.Core().Process()
|
||||||
|
r := process.RunWithEnv(ctx, repoDir, []string{"GOWORK=off"}, "go", "test", "./...", "-count=1", "-timeout", "120s")
|
||||||
out := resultText(r)
|
out := resultText(r)
|
||||||
exitCode := 0
|
exitCode := 0
|
||||||
if !r.OK {
|
if !r.OK {
|
||||||
|
|
@ -228,10 +230,11 @@ func (s *PrepSubsystem) runGoTests(repoDir string) verifyResult {
|
||||||
|
|
||||||
func (s *PrepSubsystem) runPHPTests(repoDir string) verifyResult {
|
func (s *PrepSubsystem) runPHPTests(repoDir string) verifyResult {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
r := s.runCmd(ctx, repoDir, "composer", "test", "--no-interaction")
|
process := s.Core().Process()
|
||||||
|
r := process.RunIn(ctx, repoDir, "composer", "test", "--no-interaction")
|
||||||
if !r.OK {
|
if !r.OK {
|
||||||
// Try pest as fallback
|
// Try pest as fallback
|
||||||
r2 := s.runCmd(ctx, repoDir, "./vendor/bin/pest", "--no-interaction")
|
r2 := process.RunIn(ctx, repoDir, "./vendor/bin/pest", "--no-interaction")
|
||||||
if !r2.OK {
|
if !r2.OK {
|
||||||
return verifyResult{passed: false, testCmd: "none", output: "No PHP test runner found (composer test and vendor/bin/pest both unavailable)", exitCode: 1}
|
return verifyResult{passed: false, testCmd: "none", output: "No PHP test runner found (composer test and vendor/bin/pest both unavailable)", exitCode: 1}
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +257,8 @@ func (s *PrepSubsystem) runNodeTests(repoDir string) verifyResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
r = s.runCmd(ctx, repoDir, "npm", "test")
|
process := s.Core().Process()
|
||||||
|
r = process.RunIn(ctx, repoDir, "npm", "test")
|
||||||
out := resultText(r)
|
out := resultText(r)
|
||||||
exitCode := 0
|
exitCode := 0
|
||||||
if !r.OK {
|
if !r.OK {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ if run.OK {
|
||||||
- Use UK English in comments and docs.
|
- Use UK English in comments and docs.
|
||||||
- Use `core.E("pkg.Method", "message", err)` for errors. Never use `fmt.Errorf` or `errors.New`.
|
- Use `core.E("pkg.Method", "message", err)` for errors. Never use `fmt.Errorf` or `errors.New`.
|
||||||
- Use `c.Fs()` or a package-level `fs` helper for file I/O. Never use raw `os.ReadFile`, `os.WriteFile`, or `filepath.*`.
|
- Use `c.Fs()` or a package-level `fs` helper for file I/O. Never use raw `os.ReadFile`, `os.WriteFile`, or `filepath.*`.
|
||||||
- Route external commands through `c.Process()` or the repo's process helper layer. Never import `os/exec`.
|
- Route external commands through `c.Process()`. Never import `os/exec`.
|
||||||
- Use Core string and path helpers such as `core.Contains`, `core.Trim`, `core.Split`, `core.Concat`, and `core.JoinPath` instead of raw `strings.*` or path concatenation.
|
- Use Core string and path helpers such as `core.Contains`, `core.Trim`, `core.Split`, `core.Concat`, and `core.JoinPath` instead of raw `strings.*` or path concatenation.
|
||||||
- Prefer `core.Result{Value: x, OK: true}` over `(value, error)` pairs in Core-facing code.
|
- Prefer `core.Result{Value: x, OK: true}` over `(value, error)` pairs in Core-facing code.
|
||||||
- Comments should show HOW with real values, not restate the signature.
|
- Comments should show HOW with real values, not restate the signature.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue