checkpoint: multi-repo Gitea ingestion, per-project repos, rotation anchor_sha fix
Pre-existing uncommitted work found at session start: rotation now emits anchor_sha on TaskReleased (previously silently dropped by store.Append validation), multi-repo Gitea provider support, per-project git worktree roots, and associated test coverage. Committing as a checkpoint before starting remediation work tracked in AUDIT.md.
This commit is contained in:
@@ -21,6 +21,13 @@ var (
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
MachineAffinity []string `json:"machine_affinity"`
|
||||
// Repo and WorktreeRoot let each project resolve its own git checkout
|
||||
// (spec §2.2 — a project is first-class; nothing about the model implies
|
||||
// a single shared repo across all projects). Both optional: a project
|
||||
// that omits them falls back to whatever global default the deployment
|
||||
// wires (single-repo deployments keep working unchanged).
|
||||
Repo string `json:"repo,omitempty"`
|
||||
WorktreeRoot string `json:"worktree_root,omitempty"`
|
||||
}
|
||||
type Machine struct {
|
||||
ID string `json:"id"`
|
||||
@@ -34,8 +41,17 @@ type Herdr struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
QuotaLimit float64 `json:"quota_limit,omitempty"`
|
||||
// QuotaLimit is deprecated in favor of QuotaLimit5h/QuotaLimitWeekly; if
|
||||
// set and QuotaLimit5h is not, it is treated as the weekly limit only
|
||||
// (its historical meaning), to avoid silently inventing a 5h cap for
|
||||
// existing configuration.
|
||||
QuotaLimit float64 `json:"quota_limit,omitempty"`
|
||||
QuotaLimit5h float64 `json:"quota_limit_5h,omitempty"`
|
||||
QuotaLimitWeekly float64 `json:"quota_limit_weekly,omitempty"`
|
||||
}
|
||||
|
||||
const defaultHerdrPort = "9245"
|
||||
|
||||
type Config struct {
|
||||
Projects []Project `json:"projects"`
|
||||
Machines []Machine `json:"machines"`
|
||||
@@ -53,12 +69,58 @@ func Load(path string) (Registry, error) {
|
||||
return Registry{}, err
|
||||
}
|
||||
var c Config
|
||||
if err = json.Unmarshal(b, &c); err != nil {
|
||||
if err = json.Unmarshal(stripJSONComments(b), &c); err != nil {
|
||||
return Registry{}, fmt.Errorf("registry config: %w", err)
|
||||
}
|
||||
return New(c)
|
||||
}
|
||||
|
||||
// stripJSONComments removes // line comments and /* */ block comments from
|
||||
// JSONC input, leaving valid JSON. Comment markers inside string literals
|
||||
// (respecting backslash escapes) are left untouched. This lets deployments
|
||||
// annotate config.json in place instead of keeping a separate undocumented
|
||||
// copy (see deploy/config.example.jsonc).
|
||||
func stripJSONComments(b []byte) []byte {
|
||||
out := make([]byte, 0, len(b))
|
||||
inString, escaped, inLineComment, inBlockComment := false, false, false, false
|
||||
for i := 0; i < len(b); i++ {
|
||||
c := b[i]
|
||||
switch {
|
||||
case inLineComment:
|
||||
if c == '\n' {
|
||||
inLineComment = false
|
||||
out = append(out, c)
|
||||
}
|
||||
case inBlockComment:
|
||||
if c == '*' && i+1 < len(b) && b[i+1] == '/' {
|
||||
inBlockComment = false
|
||||
i++
|
||||
}
|
||||
case inString:
|
||||
out = append(out, c)
|
||||
if escaped {
|
||||
escaped = false
|
||||
} else if c == '\\' {
|
||||
escaped = true
|
||||
} else if c == '"' {
|
||||
inString = false
|
||||
}
|
||||
case c == '"':
|
||||
inString = true
|
||||
out = append(out, c)
|
||||
case c == '/' && i+1 < len(b) && b[i+1] == '/':
|
||||
inLineComment = true
|
||||
i++
|
||||
case c == '/' && i+1 < len(b) && b[i+1] == '*':
|
||||
inBlockComment = true
|
||||
i++
|
||||
default:
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func New(c Config) (Registry, error) {
|
||||
r := Registry{map[string]Project{}, map[string]Machine{}, map[string]Herdr{}}
|
||||
for _, p := range c.Projects {
|
||||
@@ -160,6 +222,9 @@ func (r Registry) Candidates(project string, check Reachability, timeout time.Du
|
||||
addr := h.Address
|
||||
if addr == "" {
|
||||
addr = r.machines[h.MachineID].Address
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = net.JoinHostPort(host, defaultHerdrPort)
|
||||
}
|
||||
}
|
||||
if check == nil || check.Reachable(addr, timeout) {
|
||||
out = append(out, h)
|
||||
|
||||
Reference in New Issue
Block a user