diff --git a/go-api/cmd/importagents/main.go b/go-api/cmd/importagents/main.go index b26c871..f1e308b 100644 --- a/go-api/cmd/importagents/main.go +++ b/go-api/cmd/importagents/main.go @@ -7,11 +7,16 @@ // // What it does NOT do, deliberately: // -// - It does not create versions. §3 says specs are immutable once published -// and editing publishes a new version; this re-publishes in place, which is -// right for a curated set shipped with the deployment and wrong for -// authored ones. Version immutability is Phase 3's, and this command is the -// thing that makes Phase 3 worth doing rather than a substitute for it. +// - It does not validate every spec against a running model. Parsing and +// dependency checks happen here; behaviour is what the eval suites are for. +// +// It DOES record versions, in the same transaction as the definitions. §3 says +// a published version is immutable and editing publishes a new one, and a +// command that re-published in place was the one path that ignored that: the +// live row took the new text and nothing recorded what the old one said, so +// every deploy quietly rewrote v1. A spec whose content has changed without +// its `version:` being raised is now refused, and refused for the whole set — +// see the note above the import loop. // - It does not validate tool names against the registry. §3 wants an unknown // tool to fail at publish; today the runtime records and drops one. The // check is cheap to add and belongs here — see the note in run(). @@ -31,9 +36,12 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + "github.com/krow/krow-backend/go-api/internal/authctx" "github.com/krow/krow-backend/go-api/internal/config" "github.com/krow/krow-backend/go-api/internal/db" "github.com/krow/krow-backend/go-api/internal/definition" + "github.com/krow/krow-backend/go-api/internal/domain" + "github.com/krow/krow-backend/go-api/internal/repo" ) func main() { @@ -164,7 +172,22 @@ func run(dir, skillDir, orgSlug string, dryRun bool, timeout time.Duration) erro skillsWritten++ } - inserted, updated := 0, 0 + // Versions are recorded through the same transaction, so the history and + // the definition it describes cannot disagree: either both land or neither + // does. + // + // Snapshot is what refuses a spec that changed without raising its + // `version:`. Every such spec is collected rather than the first one + // returned, for the same reason the parse errors above are — an operator + // who forgot to bump three files should see three. Collecting is safe here + // because that refusal comes from comparing a row this code read, not from + // a failed statement: the INSERT is ON CONFLICT DO NOTHING, so the + // transaction is still healthy and the remaining specs can be checked. + versions := repo.NewVersionsRepo(tx) + ident := authctx.Identity{OrgID: orgID, UserID: author} + + inserted, updated, versioned := 0, 0, 0 + var rewrites []string for _, s := range specs { wasNew, err := upsert(ctx, tx, orgID, author, s) if err != nil { @@ -175,13 +198,42 @@ func run(dir, skillDir, orgSlug string, dryRun bool, timeout time.Duration) erro } else { updated++ } + + err = versions.Snapshot(ctx, ident, repo.SnapshotInput{ + Kind: repo.KindAgent, + DefinitionID: s.parsed.ID, + Version: s.parsed.Version, + Markdown: s.raw, + Name: s.parsed.Name, + Description: s.parsed.Description, + Pages: s.parsed.Pages, + }) + var apiErr *domain.Error + switch { + case err == nil: + versioned++ + case errors.As(err, &apiErr) && apiErr.Code == "conflict": + rewrites = append(rewrites, fmt.Sprintf(" %s: %s", s.name, apiErr.Message)) + default: + return fmt.Errorf("%s: record version: %w", s.name, err) + } } + + if len(rewrites) > 0 { + return fmt.Errorf( + "%d spec(s) would rewrite a version that is already published:\n%s\n\n"+ + "Nothing was written. Raise `version:` in the frontmatter of each, or "+ + "restore the published text.", + len(rewrites), strings.Join(rewrites, "\n")) + } + if err := tx.Commit(ctx); err != nil { return fmt.Errorf("commit: %w", err) } - fmt.Printf("\n%d agent(s) published, %d updated, %d skill(s) written, into %s\n", - inserted, updated, skillsWritten, orgSlug) + fmt.Printf("\n%d agent(s) published, %d updated, %d version(s) recorded, "+ + "%d skill(s) written, into %s\n", + inserted, updated, versioned, skillsWritten, orgSlug) return nil }