f9a9873341
Implements Phase 3 of v0.1 Foundation:
- internal/model/job.go: Job + Task models with status state machines
- internal/store/migrations/0002_jobs_tasks.sql: jobs + tasks tables with FK
- internal/store/job_task_repo.go: JobRepo + TaskRepo with CRUD and lifecycle updates
- internal/jobspec/spec.go: HCL parser using hashicorp/hcl/v2 hclsimple
- internal/jobspec/spec_test.go: 4 tests for parser
- internal/engine/executor.go: parallel task executor using os/exec with Go 1.25
WaitDelay for clean process shutdown
- internal/cli/job.go: orca job {run,list,stop,logs} wired to executor
- testdata/hello.hcl, testdata/fail.hcl: smoke test fixtures
Verified: job run executes commands, captures stdout/stderr, persists state,
job stop transitions status, job logs displays captured output. All tests
pass with -race.
---ci---
project: orca
phase: 3
milestone: v0.1
status: execute
req_covered:
- REQ-004
- REQ-006
- REQ-009
- REQ-018
- REQ-020
- REQ-021
---/ci---
70 lines
1.5 KiB
Go
70 lines
1.5 KiB
Go
package jobspec
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2/hclsimple"
|
|
)
|
|
|
|
type Spec struct {
|
|
Job JobSpec `hcl:"job,block"`
|
|
Tasks []TaskSpec `hcl:"task,block"`
|
|
}
|
|
|
|
type JobSpec struct {
|
|
Name string `hcl:"name,label"`
|
|
Type string `hcl:"type,optional"`
|
|
}
|
|
|
|
type TaskSpec struct {
|
|
Name string `hcl:"name,label"`
|
|
Command string `hcl:"command"`
|
|
Args []string `hcl:"args,optional"`
|
|
Env []string `hcl:"env,optional"`
|
|
}
|
|
|
|
func ParseFile(path string) (*Spec, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read spec file: %w", err)
|
|
}
|
|
return Parse(data, path)
|
|
}
|
|
|
|
func Parse(data []byte, filename string) (*Spec, error) {
|
|
var spec Spec
|
|
err := hclsimple.Decode(filename, data, nil, &spec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode hcl: %w", err)
|
|
}
|
|
if spec.Job.Name == "" {
|
|
return nil, fmt.Errorf("spec missing job name")
|
|
}
|
|
if len(spec.Tasks) == 0 {
|
|
return nil, fmt.Errorf("spec must have at least one task")
|
|
}
|
|
for i, t := range spec.Tasks {
|
|
if t.Command == "" {
|
|
return nil, fmt.Errorf("task[%d] (%s) missing command", i, t.Name)
|
|
}
|
|
}
|
|
return &spec, nil
|
|
}
|
|
|
|
func (s *Spec) Validate() error {
|
|
if strings.TrimSpace(s.Job.Name) == "" {
|
|
return fmt.Errorf("job name is required")
|
|
}
|
|
if len(s.Tasks) == 0 {
|
|
return fmt.Errorf("at least one task is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ = hcl.Diagnostics{}
|
|
var _ = gohcl.DecodeBody
|