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:
kami
2026-07-27 18:15:02 +04:00
parent 325c684eb0
commit ce6f02f9e6
31 changed files with 2717 additions and 320 deletions
+67 -2
View File
@@ -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)