{"record":{"id":"58bff1fa6656393b","repo":"gastownhall/beads","slug":"import-batch-actor-must-not-be-empty","errorCode":null,"errorMessage":"import batch: actor must not be empty","messagePattern":"import batch: actor must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/uow/importer.go","lineNumber":51,"sourceCode":"\tprovider UnitOfWorkProvider\n}\n\nvar _ publicops.Importer = (*importer)(nil)\n\n// ImportBatch writes the whole batch in ONE unit of work and commits it as\n// ONE history entry: the issue rows through the SAME batch-upsert engine the\n// classic stores run (internal/storage/issueops.CreateIssuesInTxWithResult —\n// conditional row upsert, idempotent label/comment/dependency merge, child\n// counters, blocked recompute), the memory records, and the optional\n// issue_prefix reconciliation. A request-level failure rolls all of it back.\n//\n// The engine's callbacks land in the result instead of being exposed on the\n// request: RunTxResult retries the whole attempt on a serialization failure,\n// and result state declared inside the attempt cannot leak between retries\n// the way a caller's callback accumulator would.\nfunc (o *importer) ImportBatch(ctx context.Context, request publicops.ImportBatchRequest) (publicops.ImportBatchResult, error) {\n\tif request.Actor == \"\" {\n\t\treturn publicops.ImportBatchResult{}, fmt.Errorf(\"import batch: actor must not be empty\")\n\t}\n\treturn RunTxResult(ctx, o.provider, func(ctx context.Context, uw UnitOfWork) (publicops.ImportBatchResult, string, error) {\n\t\tvar result publicops.ImportBatchResult\n\n\t\tif len(request.Issues) > 0 {\n\t\t\trunner, err := importStatementRunner(uw)\n\t\t\tif err != nil {\n\t\t\t\treturn publicops.ImportBatchResult{}, \"\", err\n\t\t\t}\n\t\t\tstaleRejected := make(map[string]struct{})\n\t\t\tskippedSeen := make(map[string]struct{})\n\t\t\topts := storage.BatchCreateOptions{\n\t\t\t\tSkipPrefixValidation:           request.SkipPrefixValidation,\n\t\t\t\tRejectStaleUpserts:             !request.AllowStale,\n\t\t\t\tSkipDependencyValidationErrors: true,\n\t\t\t\tOnSkippedDependency: func(issueID, dependsOnID, reason string) {\n\t\t\t\t\tkey := issueID + \"\\x00\" + dependsOnID + \"\\x00\" + reason\n\t\t\t\t\tif _, ok := skippedSeen[key]; ok {","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/uow/importer.go#L33-L69","documentation":"ImportBatch validates up front that ImportBatchRequest.Actor is non-empty and rejects the whole batch before any transaction is started. The actor identifies who performed the import and is threaded into the batch-upsert engine; without it the write would be unattributable, so the library refuses to run.","triggerScenarios":"Calling ImportBatch (publicops.Importer) with a zero-value ImportBatchRequest, or a request built programmatically/tests where the Actor field was never set — including when Issues and Memories are both empty (validation happens before content checks).","commonSituations":"Scripted `bd import` pipelines that construct ImportBatchRequest themselves and forget to propagate the current user; refactors that renamed the actor field and dropped the assignment; tests that build requests via composite literals without Actor.","solutions":["Set request.Actor to the acting identity (e.g. os.Getenv(\"USER\")/git config user, or the value the CLI already computes) before calling ImportBatch.","Validate the actor in your own request-builder constructor so an empty actor never reaches the importer.","If migrating from the classic path, pass the same actor string the batch-upsert engine previously received."],"exampleFix":"// before\nreq := publicops.ImportBatchRequest{Issues: issues, Source: \"bd import\"}\nresult, err := imp.ImportBatch(ctx, req) // \"actor must not be empty\"\n// after\nreq := publicops.ImportBatchRequest{Actor: currentUser(), Issues: issues, Source: \"bd import\"}\nresult, err := imp.ImportBatch(ctx, req)","handlingStrategy":"validation","validationCode":"func validateImportRequest(req publicops.ImportBatchRequest) error {\n\tif req.Actor == \"\" {\n\t\treturn errors.New(\"import batch: actor must not be empty\")\n\t}\n\treturn nil\n}\n// call before ImportBatch\nif err := validateImportRequest(req); err != nil {\n\treturn fmt.Errorf(\"build import request: %w\", err)\n}","typeGuard":"func hasActor(req publicops.ImportBatchRequest) bool {\n\treturn strings.TrimSpace(req.Actor) != \"\"\n}","tryCatchPattern":"result, err := imp.ImportBatch(ctx, req)\nif err != nil {\n\tif strings.Contains(err.Error(), \"actor must not be empty\") {\n\t\treturn fmt.Errorf(\"caller bug: set ImportBatchRequest.Actor (e.g. current user) before importing: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Default Actor at request-construction time from the same identity the CLI uses.","Make request builders require the actor as a constructor argument.","Add a unit test asserting ImportBatch rejects empty actors, so builders stay honest.","Never serialize/deserialize requests dropping the Actor field."],"tags":["validation","import","api-misuse"],"backgroundTag":"missing-actor","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}