Skip to content

Conversation

@jason-lynch
Copy link
Member

@jason-lynch jason-lynch commented Jan 14, 2026

Summary

Adds a "scope" concept to tasks to support tasks that apply to non-database entities. With this change, the task's parent is identified by the scope and entity_id fields.

This changes how tasks and task logs are stored, so this PR includes a non-destructive, idempotent migration to move tasks and task logs to a new keyspace.

Changes

  • Adds scope and entity_id fields to tasks and task logs
  • Migrates existing tasks and task logs to a new keyspace so that they're still available via the API

Testing

  • Start a server on the main branch
  • Create a database
  • Switch to this branch
  • Restart the server
  • Verify that existing tasks and task logs are still available via the API
  • Create another database
  • Verify that new tasks and tasks logs are available via the API
  • Verify that the scope and entity_id fields exist and are set correctly on all tasks

PLAT-347

Summary by CodeRabbit

  • New Features

    • Added support for non-database tasks with scope-based identification. Tasks can now be associated with different entity types (databases and hosts) using scope and entity_id fields.
    • Extended task tracking to support per-entity scope identifiers, enabling better organization of database and host-related tasks.
  • Refactor

    • Migrated task identification system from database_id-only to scope + entity_id approach for improved multi-entity support.
  • Tests

    • Added comprehensive test coverage for scoped task operations across databases and hosts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This pull request extends task-related API types and infrastructure to support multi-tenant scope and entity identification. Tasks now carry scope and entity_id fields alongside database_id, enabling support for non-database tasks (e.g., host-scoped tasks). Required fields in API payloads shift from database_id to scope and entity_id.

Changes

Cohort / File(s) Summary
API Design Layer
api/apiv1/design/task.go
Added public attributes scope and entity_id to Task, TaskLog, and ListDatabaseTasksResponse types. Updated required fields from database_id to scope and entity_id. Updated example payloads to reflect new fields. Marked database_id as deprecated in TaskLog.
Domain Model
server/internal/task/task.go, task_scope_test.go
Introduced Scope type with constants ScopeDatabase and ScopeHost. Extended Task and TaskLog structs with Scope and EntityID fields. Added Options.EntityID() method. Enhanced validation to enforce scope-specific required IDs. Added test coverage for scope validation and field behavior.
Changelog
changes/unreleased/Added-20260114-173755.yaml
Added release notes documenting support for non-database tasks and new scope/entity_id fields.
Storage Layer
server/internal/task/task_store.go, task_store_test.go
Refactored key generation from (databaseID, taskID) to (scope, entityID, taskID). Changed prefix from "tasks" to "tasks_v2". Added EntityPrefix() and updated GetAllByDatabaseIDGetAllByEntity(), Delete(), and DeleteByDatabaseID()DeleteByEntity(). Added comprehensive test suite covering scope-aware CRUD operations.
Task Log Storage
server/internal/task/task_log_store.go, task_log_store_test.go
Added Scope and EntityID fields to StoredTaskLogEntry. Renamed storage prefix to "task_log_entries". Refactored methods to use scope-based keys. Added TaskPrefix(), Key(), GetAllByTask(), DeleteByTask(), and DeleteByEntity(). Deprecated legacy database-ID methods. Added extensive test coverage for scope-aware storage operations.
Service Layer
server/internal/task/service.go, service_scoped_test.go
Updated all public methods (GetTask, GetTasks, DeleteTask, AddLogEntry, GetTaskLog, etc.) to accept scope and entityID parameters instead of databaseID. Reworked storage key resolution and paging logic. Updated task log persistence to include scope/entity fields. Added comprehensive integration test suite validating scope-aware task lifecycle.
Conversion & Serialization
server/internal/api/apiv1/convert.go
Updated taskToAPI() and taskLogToAPI() to populate Scope and EntityID fields in API responses. Made DatabaseID nullable.
Task Log Writer
server/internal/task/task_log_writer.go
Updated NewTaskLogWriter() constructor signature to accept scope and entityID parameters instead of databaseID.
Migration Infrastructure
server/internal/migrate/migrations/add_task_scope.go, all_migrations.go
Added new AddTaskScope migration to convert legacy etcd-based tasks to new scope-aware storage model. Reads old task/task_log entries, converts to new format with scope/entity fields, and writes to new stores.
API Handlers
server/internal/api/apiv1/post_init_handlers.go
Updated handlers (ListDatabaseTasks, GetDatabaseTask, GetTaskLog, etc.) to pass task.ScopeDatabase scope parameter to task service methods.
Orchestration
server/internal/orchestrator/swarm/pgbackrest_restore.go
Updated GetTask() and NewTaskLogWriter() calls to include task.ScopeDatabase scope parameter.
Workflow Activities
server/internal/workflows/activities/apply_event.go, create_pgbackrest_backup.go, log_task_event.go, update_task.go
Updated activity input structures and task service calls to use scope/entityID instead of databaseID. Renamed internal logging function. Updated all task log writer and service invocations with scope parameter.
Workflow Orchestration
server/internal/workflows/common.go, service.go
Updated logTaskEvent() and cancelTask() method signatures to accept scope and entityID. Updated all workflow files (create_pgbackrest_backup.go, delete_database.go, failover.go, pgbackrest_restore.go, refresh_current_state.go, restart_instance.go, start_instance.go, stop_instance.go, switchover.go, update_database.go) to construct UpdateTaskInput with Scope and EntityID fields instead of DatabaseID. Consistently pass task.ScopeDatabase in scope-aware task operations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops through scopes and entities with glee,
Tasks now dance in tenants wild and free,
DatabaseID steps aside with grace,
Scope and EntityID take their place!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: scoped tasks' clearly and concisely summarizes the main change: introducing a scope concept for tasks to support non-database entities.
Description check ✅ Passed The PR description includes all required sections: a clear summary explaining the scope concept, a detailed changes list, comprehensive testing instructions, and a checklist confirming added tests and changelog entry.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jason-lynch jason-lynch force-pushed the feat/PLAT-347/migrations branch from 64e7057 to 24aad70 Compare January 15, 2026 13:50
@jason-lynch jason-lynch force-pushed the feat/PLAT-347/add-scope-to-tasks branch from 9ff3ccb to 83955e8 Compare January 15, 2026 13:51
@jason-lynch jason-lynch force-pushed the feat/PLAT-347/migrations branch 2 times, most recently from 8b2d7e1 to 278a4cb Compare January 15, 2026 14:03
@jason-lynch jason-lynch force-pushed the feat/PLAT-347/add-scope-to-tasks branch from 83955e8 to 7391dab Compare January 15, 2026 14:30
@jason-lynch jason-lynch force-pushed the feat/PLAT-347/migrations branch from 278a4cb to d35513b Compare January 15, 2026 17:35
@jason-lynch jason-lynch force-pushed the feat/PLAT-347/add-scope-to-tasks branch from 7391dab to 53678c0 Compare January 15, 2026 17:35
@jason-lynch
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
server/internal/orchestrator/swarm/pgbackrest_restore.go (1)

94-97: Bug: handleError returns wrong error variable.

The closure returns err (the error from startTask on line 93) instead of cause (the parameter passed to handleError). This means all error returns from handleError calls will return the original startTask error rather than the actual failure cause.

🐛 Proposed fix
 	handleError := func(cause error) error {
 		p.failTask(logger, taskSvc, t, cause)
-		return err
+		return cause
 	}
server/internal/task/service.go (2)

274-286: Bug: Empty Statuses slice filters out all tasks.

When opts.Statuses is empty, slices.Contains(opts.Statuses, task.Status) always returns false, causing all tasks to be filtered out. The check should only apply when statuses are specified.

Proposed fix
 func matchesFilters(task *Task, opts TaskListOptions) bool {
 	if opts.Type != "" && task.Type != opts.Type {
 		return false
 	}
-	if !slices.Contains(opts.Statuses, task.Status) {
+	if len(opts.Statuses) > 0 && !slices.Contains(opts.Statuses, task.Status) {
 		return false
 	}
 	if opts.NodeName != "" && (task == nil || task.NodeName != opts.NodeName) {
 		return false
 	}

 	return true
 }

118-127: Conditional assignment for DatabaseID based on scope.

Setting DatabaseID unconditionally to entityID will store incorrect data when the scope is not ScopeDatabase (e.g., for host-scoped tasks, DatabaseID would contain a host ID).

Proposed fix
 	stored := &StoredTaskLogEntry{
 		Scope:      scope,
 		EntityID:   entityID,
-		DatabaseID: entityID, // For backward compatibility with database-scoped tasks
+		DatabaseID: "", // Set below for backward compatibility
 		TaskID:     taskID,
 		EntryID:    entryID,
 		Timestamp:  timestamp,
 		Message:    entry.Message,
 		Fields:     entry.Fields,
 	}
+	if scope == ScopeDatabase {
+		stored.DatabaseID = entityID
+	}
server/internal/workflows/common.go (1)

176-196: Bug: Error logged unconditionally regardless of logTaskEvent outcome.

Line 194 logs an error message even when err is nil, resulting in misleading log entries on successful task event logging.

🐛 Proposed fix
 	err := w.logTaskEvent(cleanupCtx, scope, entityID, taskID, task.LogEntry{
 		Message: "task successfully canceled",
 		Fields:  map[string]any{"status": "canceled"},
 	})
-	logger.With("error", err).Error("failed to log task event")
+	if err != nil {
+		logger.With("error", err).Error("failed to log task event")
+	}
 
 }
🤖 Fix all issues with AI agents
In `@server/internal/migrate/migrations/add_task_scope.go`:
- Around line 66-71: The error message for the Etcd query uses the wrong
wording: change the fmt.Errorf call that currently returns "failed to query for
old tasks: %w" (in the block using oldTaskLogsPrefix / oldTaskLogsRangeOp /
oldTaskLogs) to a correct message like "failed to query for old task logs: %w"
so the log accurately reflects the operation querying oldStoredTaskLogEntry
entries.
🧹 Nitpick comments (7)
server/internal/task/task_log_writer.go (1)

12-16: Consider renaming DatabaseID to EntityID for clarity.

The TaskLogWriter struct field DatabaseID now holds the generic entityID value. While this maintains backward compatibility, it could cause confusion since the field name no longer accurately describes its contents for non-database scopes (e.g., host-scoped tasks).

If backward compatibility is essential, consider adding a comment explaining this or introducing an EntityID field alongside for future use.

♻️ Optional: Add clarifying comment or alias
 type TaskLogWriter struct {
-	DatabaseID string
+	DatabaseID string // Legacy name; holds entityID for all scopes
 	TaskID     uuid.UUID
 	writer     *utils.LineWriter
 }

Or for a cleaner approach in a future refactor:

 type TaskLogWriter struct {
-	DatabaseID string
+	EntityID   string
 	TaskID     uuid.UUID
 	writer     *utils.LineWriter
 }

Also applies to: 18-31

server/internal/task/task_scope_test.go (1)

26-35: Consider adding host scope test case.

The test validates EntityID() for database scope. Consider adding a similar test for host scope to ensure complete coverage of the EntityID() method's switch statement.

♻️ Suggested addition
func TestOptionsWithScope(t *testing.T) {
	t.Run("database scope", func(t *testing.T) {
		opts := task.Options{
			Scope:      task.ScopeDatabase,
			DatabaseID: "my-database",
			Type:       task.TypeCreate,
		}
		assert.Equal(t, task.ScopeDatabase, opts.Scope)
		assert.Equal(t, "my-database", opts.EntityID())
	})

	t.Run("host scope", func(t *testing.T) {
		opts := task.Options{
			Scope:  task.ScopeHost,
			HostID: "host-1",
			Type:   task.TypeRemoveHost,
		}
		assert.Equal(t, task.ScopeHost, opts.Scope)
		assert.Equal(t, "host-1", opts.EntityID())
	})
}
server/internal/migrate/migrations/add_task_scope.go (1)

73-78: Consider adding idempotency logging for task logs.

For consistency with the task migration pattern (lines 54-64), consider checking for ErrAlreadyExists on task log migration and logging a skip message. This would provide better observability during repeated migration runs.

Proposed change
 	for _, oldTaskLog := range oldTaskLogs {
-		err := taskStore.TaskLogMessage.Put(oldTaskLog.convert()).Exec(ctx)
-		if err != nil {
+		err := taskStore.TaskLogMessage.Create(oldTaskLog.convert()).Exec(ctx)
+		switch {
+		case errors.Is(err, storage.ErrAlreadyExists):
+			logger.Info().
+				Stringer("entry_id", oldTaskLog.EntryID).
+				Stringer("task_id", oldTaskLog.TaskID).
+				Msg("task log entry has already been migrated, skipping")
+		case err != nil:
 			return fmt.Errorf("failed to migrate task log entry %s for task %s: %w", oldTaskLog.EntryID, oldTaskLog.TaskID, err)
 		}
 	}

Note: This assumes TaskLogMessage has a Create method similar to tasks. If only Put is available, the current approach is acceptable since the data is identical on re-runs.

server/internal/workflows/activities/log_task_event.go (1)

37-46: Update logger key to reflect scope-agnostic entity.

The logger key "database_id" is misleading for non-database scopes (e.g., host-scoped tasks). Consider updating to "entity_id" for consistency with the new scoped model.

Proposed fix
 func (a *Activities) LogTaskEvent(ctx context.Context, input *LogTaskEventInput) (*LogTaskEventOutput, error) {
-	logger := activity.Logger(ctx).With("database_id", input.EntityID)
+	logger := activity.Logger(ctx).With("scope", input.Scope, "entity_id", input.EntityID)
 	logger.Debug("logging task event")
server/internal/api/apiv1/convert.go (1)

568-572: Consider using consistent Scope conversion.

In taskToAPI (line 539), Scope is converted using string(t.Scope), while here it uses t.Scope.String(). Both work since Scope is a string type alias, but using the same pattern improves consistency.

♻️ Optional: Use consistent conversion pattern
 return &api.TaskLog{
-		Scope:       t.Scope.String(),
+		Scope:       string(t.Scope),
 		EntityID:    t.EntityID,
api/apiv1/design/task.go (2)

13-16: Consider adding Enum constraint for scope.

The description states scope can be "database or host", but there's no validation constraint. Adding an enum would prevent invalid values and provide better API documentation.

♻️ Suggested improvement
 g.Attribute("scope", g.String, func() {
+    g.Enum("database", "host")
     g.Description("The scope of the task (database or host).")
     g.Example("database")
 })

101-104: Add Enum constraint for consistency with Task type.

Same recommendation as for the Task type—adding g.Enum("database", "host") would ensure validation consistency across both types.

♻️ Suggested improvement
 g.Attribute("scope", g.String, func() {
+    g.Enum("database", "host")
     g.Description("The scope of the task (database or host).")
     g.Example("database")
 })
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d35513b and 53678c0.

⛔ Files ignored due to path filters (10)
  • api/apiv1/gen/control_plane/service.go is excluded by !**/gen/**
  • api/apiv1/gen/control_plane/views/view.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/client/encode_decode.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/client/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/server/encode_decode.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/server/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.yaml is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (32)
  • api/apiv1/design/task.go
  • changes/unreleased/Added-20260114-173755.yaml
  • server/internal/api/apiv1/convert.go
  • server/internal/api/apiv1/post_init_handlers.go
  • server/internal/migrate/all_migrations.go
  • server/internal/migrate/migrations/add_task_scope.go
  • server/internal/orchestrator/swarm/pgbackrest_restore.go
  • server/internal/task/service.go
  • server/internal/task/service_scoped_test.go
  • server/internal/task/task.go
  • server/internal/task/task_log_store.go
  • server/internal/task/task_log_store_test.go
  • server/internal/task/task_log_writer.go
  • server/internal/task/task_scope_test.go
  • server/internal/task/task_store.go
  • server/internal/task/task_store_test.go
  • server/internal/workflows/activities/apply_event.go
  • server/internal/workflows/activities/create_pgbackrest_backup.go
  • server/internal/workflows/activities/log_task_event.go
  • server/internal/workflows/activities/update_task.go
  • server/internal/workflows/common.go
  • server/internal/workflows/create_pgbackrest_backup.go
  • server/internal/workflows/delete_database.go
  • server/internal/workflows/failover.go
  • server/internal/workflows/pgbackrest_restore.go
  • server/internal/workflows/refresh_current_state.go
  • server/internal/workflows/restart_instance.go
  • server/internal/workflows/service.go
  • server/internal/workflows/start_instance.go
  • server/internal/workflows/stop_instance.go
  • server/internal/workflows/switchover.go
  • server/internal/workflows/update_database.go
🧰 Additional context used
📓 Path-based instructions (5)
server/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

server/**/*.go: Use samber/do injector for dependency injection; each package should have a Provide() function that registers dependencies
Use structured JSON logging with zerolog throughout the codebase, with pretty-printing enabled in dev mode
Domain-specific errors should be defined in each package; API errors should be mapped to HTTP status codes via Goa

Files:

  • server/internal/task/task_log_store_test.go
  • server/internal/workflows/refresh_current_state.go
  • server/internal/task/task_scope_test.go
  • server/internal/task/service_scoped_test.go
  • server/internal/migrate/all_migrations.go
  • server/internal/workflows/activities/log_task_event.go
  • server/internal/orchestrator/swarm/pgbackrest_restore.go
  • server/internal/task/task_log_writer.go
  • server/internal/workflows/activities/create_pgbackrest_backup.go
  • server/internal/api/apiv1/post_init_handlers.go
  • server/internal/migrate/migrations/add_task_scope.go
  • server/internal/workflows/activities/update_task.go
  • server/internal/workflows/service.go
  • server/internal/workflows/update_database.go
  • server/internal/workflows/activities/apply_event.go
  • server/internal/workflows/start_instance.go
  • server/internal/workflows/create_pgbackrest_backup.go
  • server/internal/task/task_store_test.go
  • server/internal/workflows/restart_instance.go
  • server/internal/task/task_store.go
  • server/internal/workflows/delete_database.go
  • server/internal/workflows/switchover.go
  • server/internal/workflows/failover.go
  • server/internal/workflows/stop_instance.go
  • server/internal/api/apiv1/convert.go
  • server/internal/task/task.go
  • server/internal/task/task_log_store.go
  • server/internal/workflows/common.go
  • server/internal/workflows/pgbackrest_restore.go
  • server/internal/task/service.go
server/internal/workflows/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

server/internal/workflows/**/*.go: Workflows represent long-running operations and should persist state to etcd for durability and resumability using the cschleiden/go-workflows framework
Workflow definitions should be placed in server/internal/workflows/ and use activities from server/internal/workflows/activities/, registered in the Activities.Register method

Files:

  • server/internal/workflows/refresh_current_state.go
  • server/internal/workflows/activities/log_task_event.go
  • server/internal/workflows/activities/create_pgbackrest_backup.go
  • server/internal/workflows/activities/update_task.go
  • server/internal/workflows/service.go
  • server/internal/workflows/update_database.go
  • server/internal/workflows/activities/apply_event.go
  • server/internal/workflows/start_instance.go
  • server/internal/workflows/create_pgbackrest_backup.go
  • server/internal/workflows/restart_instance.go
  • server/internal/workflows/delete_database.go
  • server/internal/workflows/switchover.go
  • server/internal/workflows/failover.go
  • server/internal/workflows/stop_instance.go
  • server/internal/workflows/common.go
  • server/internal/workflows/pgbackrest_restore.go
server/internal/orchestrator/swarm/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Docker Swarm integration should use services for Postgres instances, overlay networks for database isolation, and bind mounts for configuration and data directories

Files:

  • server/internal/orchestrator/swarm/pgbackrest_restore.go
server/internal/api/apiv1/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Implement generated Goa service interface methods in server/internal/api/apiv1/ after regenerating code

Files:

  • server/internal/api/apiv1/post_init_handlers.go
  • server/internal/api/apiv1/convert.go
api/apiv1/design/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

API endpoints should be defined using Goa's DSL in api/apiv1/design/ with separate files for domain-specific types (database.go, host.go, cluster.go), then regenerated with make -C api generate

Files:

  • api/apiv1/design/task.go
🧠 Learnings (4)
📚 Learning: 2026-01-14T16:43:14.333Z
Learnt from: CR
Repo: pgEdge/control-plane PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T16:43:14.333Z
Learning: Applies to server/internal/api/apiv1/**/*.go : Implement generated Goa service interface methods in `server/internal/api/apiv1/` after regenerating code

Applied to files:

  • server/internal/task/service_scoped_test.go
📚 Learning: 2026-01-14T16:43:14.333Z
Learnt from: CR
Repo: pgEdge/control-plane PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T16:43:14.333Z
Learning: Applies to server/internal/orchestrator/swarm/**/*.go : Docker Swarm integration should use services for Postgres instances, overlay networks for database isolation, and bind mounts for configuration and data directories

Applied to files:

  • server/internal/orchestrator/swarm/pgbackrest_restore.go
📚 Learning: 2026-01-14T16:43:14.333Z
Learnt from: CR
Repo: pgEdge/control-plane PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T16:43:14.333Z
Learning: Applies to server/internal/resource/**/*.go : Resources should follow a standard lifecycle pattern with Refresh, Plan, and Apply phases, and declare their Executor, Dependencies, and lifecycle methods

Applied to files:

  • server/internal/workflows/activities/apply_event.go
📚 Learning: 2026-01-14T16:43:14.333Z
Learnt from: CR
Repo: pgEdge/control-plane PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T16:43:14.333Z
Learning: Applies to server/internal/workflows/**/*.go : Workflows represent long-running operations and should persist state to etcd for durability and resumability using the `cschleiden/go-workflows` framework

Applied to files:

  • server/internal/workflows/create_pgbackrest_backup.go
  • server/internal/workflows/switchover.go
  • server/internal/workflows/common.go
🧬 Code graph analysis (26)
server/internal/task/task_log_store_test.go (2)
server/internal/task/task_log_store.go (3)
  • NewTaskLogEntryStore (29-34)
  • StoredTaskLogEntry (12-22)
  • TaskLogOptions (73-76)
server/internal/storage/key.go (1)
  • Key (19-25)
server/internal/workflows/refresh_current_state.go (1)
server/internal/task/task.go (1)
  • ScopeDatabase (20-20)
server/internal/task/task_scope_test.go (1)
server/internal/task/task.go (7)
  • ScopeDatabase (20-20)
  • ScopeHost (21-21)
  • Task (67-83)
  • Scope (13-13)
  • Options (89-99)
  • Type (24-24)
  • NewTask (138-163)
server/internal/task/service_scoped_test.go (3)
server/internal/task/task.go (11)
  • Options (89-99)
  • Scope (13-13)
  • ScopeDatabase (20-20)
  • Type (24-24)
  • TypeCreate (31-31)
  • StatusPending (52-52)
  • Status (45-45)
  • ScopeHost (21-21)
  • TypeRemoveHost (42-42)
  • TypeUpdate (32-32)
  • StatusRunning (53-53)
server/internal/task/task_store.go (1)
  • TaskListOptions (55-64)
server/internal/task/task_log_store.go (1)
  • TaskLogOptions (73-76)
server/internal/migrate/all_migrations.go (2)
server/internal/migrate/migration.go (1)
  • Migration (10-16)
server/internal/migrate/migrations/add_task_scope.go (1)
  • AddTaskScope (18-18)
server/internal/workflows/activities/log_task_event.go (2)
server/internal/task/task.go (1)
  • Scope (13-13)
server/internal/task/service.go (1)
  • LogEntry (103-107)
server/internal/orchestrator/swarm/pgbackrest_restore.go (2)
server/internal/task/task.go (1)
  • ScopeDatabase (20-20)
server/internal/task/task_log_writer.go (1)
  • NewTaskLogWriter (18-32)
server/internal/task/task_log_writer.go (2)
server/internal/task/task.go (1)
  • Scope (13-13)
server/internal/utils/linewriter.go (1)
  • NewLineWriter (19-23)
server/internal/workflows/activities/create_pgbackrest_backup.go (2)
server/internal/task/task_log_writer.go (1)
  • NewTaskLogWriter (18-32)
server/internal/task/task.go (1)
  • ScopeDatabase (20-20)
server/internal/api/apiv1/post_init_handlers.go (1)
server/internal/task/task.go (1)
  • ScopeDatabase (20-20)
server/internal/migrate/migrations/add_task_scope.go (6)
server/internal/storage/key.go (1)
  • Prefix (10-16)
server/internal/storage/get.go (1)
  • NewGetPrefixOp (93-99)
server/internal/storage/errors.go (1)
  • ErrAlreadyExists (10-10)
server/internal/task/task_store.go (1)
  • StoredTask (10-13)
server/internal/task/task.go (2)
  • Scope (13-13)
  • ScopeDatabase (20-20)
server/internal/task/task_log_store.go (1)
  • StoredTaskLogEntry (12-22)
server/internal/workflows/activities/update_task.go (1)
server/internal/task/task.go (1)
  • Scope (13-13)
server/internal/workflows/update_database.go (1)
server/internal/task/task.go (2)
  • ScopeDatabase (20-20)
  • Scope (13-13)
server/internal/workflows/activities/apply_event.go (5)
api/apiv1/gen/control_plane/service.go (1)
  • Identifier (556-556)
server/internal/resource/resource.go (1)
  • Identifier (28-31)
server/internal/resource/state.go (2)
  • EventTypeUpdate (20-20)
  • EventTypeDelete (21-21)
server/internal/workflows/activities/activities.go (1)
  • Activities (14-19)
server/internal/task/task.go (1)
  • ScopeDatabase (20-20)
server/internal/workflows/start_instance.go (1)
server/internal/task/task.go (2)
  • Scope (13-13)
  • ScopeDatabase (20-20)
server/internal/workflows/create_pgbackrest_backup.go (1)
server/internal/task/task.go (2)
  • ScopeDatabase (20-20)
  • Scope (13-13)
server/internal/task/task_store_test.go (3)
server/internal/task/task_store.go (1)
  • NewTaskStore (20-25)
server/internal/task/task.go (8)
  • ScopeDatabase (20-20)
  • ScopeHost (21-21)
  • NewTask (138-163)
  • Options (89-99)
  • Scope (13-13)
  • Type (24-24)
  • Task (67-83)
  • Status (45-45)
server/internal/storage/key.go (1)
  • Key (19-25)
server/internal/workflows/restart_instance.go (2)
server/internal/task/task.go (2)
  • ScopeDatabase (20-20)
  • Scope (13-13)
server/internal/workflows/activities/update_task.go (1)
  • UpdateTaskInput (16-21)
server/internal/task/task_store.go (6)
server/internal/storage/key.go (2)
  • Prefix (10-16)
  • Key (19-25)
server/internal/task/task.go (2)
  • Scope (13-13)
  • Task (67-83)
server/internal/storage/interface.go (4)
  • GetOp (50-52)
  • GetMultipleOp (55-57)
  • PutOp (65-71)
  • DeleteOp (75-78)
api/apiv1/design/task.go (1)
  • Task (7-78)
server/internal/storage/put.go (2)
  • NewCreateOp (69-76)
  • NewUpdateOp (121-128)
server/internal/storage/delete.go (1)
  • NewDeleteKeyOp (17-23)
server/internal/workflows/switchover.go (2)
server/internal/task/task.go (2)
  • ScopeDatabase (20-20)
  • Scope (13-13)
server/internal/workflows/activities/update_task.go (1)
  • UpdateTaskInput (16-21)
server/internal/workflows/failover.go (2)
server/internal/task/task.go (2)
  • ScopeDatabase (20-20)
  • Scope (13-13)
server/internal/workflows/activities/update_task.go (1)
  • UpdateTaskInput (16-21)
server/internal/workflows/stop_instance.go (1)
server/internal/task/task.go (2)
  • Scope (13-13)
  • ScopeDatabase (20-20)
server/internal/api/apiv1/convert.go (2)
server/internal/task/task.go (1)
  • Scope (13-13)
server/internal/utils/utils.go (1)
  • NillablePointerTo (76-82)
server/internal/task/task_log_store.go (5)
server/internal/task/task.go (2)
  • Scope (13-13)
  • ScopeDatabase (20-20)
server/internal/storage/key.go (2)
  • Prefix (10-16)
  • Key (19-25)
server/internal/storage/interface.go (3)
  • GetMultipleOp (55-57)
  • PutOp (65-71)
  • DeleteOp (75-78)
server/internal/storage/put.go (1)
  • NewPutOp (24-31)
server/internal/storage/delete.go (1)
  • NewDeletePrefixOp (51-57)
server/internal/workflows/common.go (2)
server/internal/task/task.go (3)
  • Scope (13-13)
  • UpdateOptions (165-174)
  • UpdateCancel (197-202)
server/internal/workflows/activities/update_task.go (1)
  • UpdateTaskInput (16-21)
server/internal/workflows/pgbackrest_restore.go (2)
server/internal/task/task.go (2)
  • ScopeDatabase (20-20)
  • Scope (13-13)
server/internal/database/spec.go (1)
  • Spec (252-267)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (103)
changes/unreleased/Added-20260114-173755.yaml (1)

1-3: LGTM!

The changelog entry clearly documents the new scope and entity_id fields for tasks and task logs.

server/internal/task/task_log_store_test.go (4)

14-45: LGTM!

Comprehensive test coverage for key and prefix generation across both database and host scopes. The expected key formats are validated correctly.


47-104: LGTM!

Good test coverage for CRUD operations with both database and host scopes. The assertions properly verify that scope and entity information is persisted and retrieved correctly.


106-166: LGTM!

Delete operations are well-tested, including verification that deleting one entity's logs doesn't affect another entity's logs.


180-196: Good update to existing test for scope compatibility.

The PLAT-245 test is correctly updated to include Scope and EntityID fields alongside the existing DatabaseID, ensuring the test works with the new storage schema while maintaining the original test intent of verifying prefix-based isolation.

server/internal/workflows/activities/create_pgbackrest_backup.go (1)

85-86: LGTM!

The NewTaskLogWriter call is correctly updated with task.ScopeDatabase scope, which is appropriate for database backup operations.

server/internal/orchestrator/swarm/pgbackrest_restore.go (2)

138-140: LGTM!

The GetTask call is correctly updated with task.ScopeDatabase scope for database restore operations.


272-273: LGTM!

The NewTaskLogWriter call is correctly updated with task.ScopeDatabase scope for database restore log streaming.

server/internal/task/task.go (6)

13-22: LGTM!

The Scope type with its constants is well-defined. The String() method enables clean integration with storage key generation and logging.


67-83: LGTM!

The Task struct appropriately includes both the new Scope/EntityID fields for the new addressing model and retains DatabaseID/HostID for backward compatibility during migration.


101-110: LGTM!

The EntityID() method correctly derives the appropriate entity identifier based on the scope. The empty string default is safe since validation rejects invalid scopes before this method is called in NewTask.


112-136: LGTM!

The validation logic is thorough - requiring scope, enforcing scope-specific ID requirements, and properly rejecting invalid scopes with descriptive error messages.


148-162: LGTM!

NewTask correctly initializes Scope and EntityID from the validated options, ensuring the task is properly addressed.


246-254: LGTM!

The TaskLog struct appropriately mirrors the Task struct with scope and entity fields, maintaining consistency across the task model.

server/internal/task/task_scope_test.go (3)

11-14: LGTM!

Simple and effective test verifying the String() method for scope constants.


16-24: LGTM!

Validates that Task struct properly stores scope and entity fields.


37-114: LGTM!

Excellent table-driven test coverage for validation logic. All edge cases are covered: valid scopes, missing required fields, scope-specific ID requirements, and invalid scope values.

server/internal/task/task_log_store.go (8)

12-22: LGTM!

The StoredTaskLogEntry struct appropriately includes scope and entity fields while retaining DatabaseID for backward compatibility.


36-49: LGTM!

The prefix hierarchy is well-structured and the deprecation pattern provides a clean migration path from database-specific to entity-generic methods.


51-71: LGTM!

The key generation methods are well-structured with appropriate deprecated wrappers for migration compatibility.


78-99: LGTM!

The GetAllByTask method correctly uses scope and entity for key generation, with proper pagination support.


101-105: LGTM!

Deprecated wrapper correctly delegates to the new scope-aware method.


107-110: LGTM!

The Put method correctly derives the storage key from the item's scope and entity fields.


112-121: LGTM!

Delete methods follow the same scope-aware pattern with appropriate deprecated wrappers.


123-132: LGTM!

Entity-level deletion methods are consistent with the overall pattern and provide appropriate deprecation wrappers.

server/internal/task/task_store.go (5)

27-33: LGTM!

The tasks_v2 prefix enables non-destructive migration to the new keyspace. The EntityPrefix method provides consistent scope-aware key generation.


35-42: LGTM!

Key generation and retrieval methods correctly incorporate scope and entity into the storage path.


66-90: LGTM!

The GetAllByEntity method correctly implements scope-aware listing with proper pagination cursor handling for both ascending and descending sort orders.


92-100: LGTM!

Create and Update methods correctly derive storage keys from the task's scope and entity fields.


102-109: LGTM!

Delete methods correctly implement scope-aware deletion. The lack of deprecated wrappers here (unlike task_log_store.go) suggests the migration handles task store operations differently, which aligns with the PR's idempotent migration approach that copies to the new keyspace.

server/internal/task/task_store_test.go (5)

14-44: LGTM!

Excellent test coverage for key generation, validating the expected key structure for both database and host scopes.


46-117: LGTM!

Comprehensive CRUD test coverage including create, retrieve, and update operations for both database and host scopes. Good use of unique store roots for test isolation.


119-191: LGTM!

Thorough test coverage for GetAllByEntity, verifying correct entity-level isolation and accurate task counts for both scopes.


193-255: LGTM!

Good delete test coverage verifying that DeleteByEntity removes only the target entity's tasks while leaving other entities unaffected.


257-303: LGTM!

The PLAT-245 regression test is correctly updated to use the new scope-aware methods while preserving its original purpose of verifying that prefix collisions (e.g., "database" vs "database2") are properly handled.

server/internal/migrate/migrations/add_task_scope.go (2)

102-145: LGTM!

The conversion functions correctly map legacy task data to the new scoped model. Setting Scope to task.ScopeDatabase and EntityID to DatabaseID is appropriate since all existing tasks are database-scoped.


24-46: LGTM!

Dependency injection follows the samber/do pattern as per coding guidelines, and structured logging with zerolog is correctly implemented with appropriate context enrichment.

server/internal/migrate/all_migrations.go (1)

3-11: LGTM!

The migration is correctly registered and follows the documented pattern for adding new migrations in chronological order.

server/internal/task/service.go (2)

42-57: LGTM!

The UpdateTask method correctly retrieves the task using the scope and entity ID from the task object itself, maintaining consistency with the new scoped model.


136-178: LGTM!

The GetTaskLog method correctly handles backward compatibility by conditionally populating legacy fields (DatabaseID, HostID) based on the scope. The TODO comment appropriately flags future cleanup.

server/internal/workflows/activities/log_task_event.go (1)

15-20: LGTM!

The input struct correctly carries Scope and EntityID to support the new scoped task model.

server/internal/workflows/activities/apply_event.go (2)

149-166: LGTM!

The renamed logResourceEvent function correctly uses task.ScopeDatabase since this activity handles database resource events. The hardcoded scope is appropriate for this context.


104-127: LGTM!

The call sites correctly pass databaseID and taskID to logResourceEvent, maintaining the database-scoped context for resource lifecycle events.

server/internal/task/service_scoped_test.go (7)

14-41: LGTM! Good test coverage for database-scoped task creation and retrieval.

The test properly validates that:

  • Scope is set to ScopeDatabase
  • EntityID is populated from DatabaseID
  • Both fields are persisted and retrievable

43-65: LGTM! Good coverage for host-scoped tasks.

Validates the alternative scope path where EntityID is derived from HostID instead of DatabaseID.


67-118: LGTM! Comprehensive test for task isolation across scopes and entities.

Good coverage for verifying that tasks are correctly partitioned by scope and entity ID, ensuring no cross-contamination between different databases and hosts.


120-158: LGTM! Task log operations properly tested.

The test correctly handles JSON serialization behavior (int → float64 conversion) and validates multi-entry log scenarios.


160-182: LGTM! Update task flow validated.

Tests the status progression from StatusPending to StatusRunning via Start() within the scoped context.


184-227: LGTM! Delete operations properly tested.

Good coverage for both single task deletion (with ErrTaskNotFound verification) and bulk deletion via DeleteAllTasks.


229-290: LGTM! Task log deletion operations well covered.

Tests both single-task log deletion and bulk log deletion for all tasks under a scope/entity pair.

server/internal/workflows/service.go (13)

51-58: LGTM! Scope correctly applied to CreateDatabase workflow.

The DeleteAllTasks and CreateTask calls properly include the database scope.


77-80: LGTM! UpdateDatabase task creation includes scope.


100-103: LGTM! DeleteDatabase task creation includes scope.


128-131: LGTM! CreatePgBackRestBackup task creation includes scope.


160-163: LGTM! PgBackRestRestore properly scopes both parent and child tasks.

Both the main restore task and per-node restore tasks correctly include the database scope.

Also applies to: 173-178


225-235: LGTM! abortTasks correctly uses task's own Scope and EntityID.

This is the right approach - using the task's fields rather than hardcoding values allows flexibility if different scopes are used in the future.


272-276: LGTM! RestartInstance task creation includes scope.


291-296: LGTM! StopInstance task creation includes scope.


311-316: LGTM! StartInstance task creation includes scope.


329-356: LGTM! CancelDatabaseTask properly updated for scope-aware operations.

The function correctly:

  • Retrieves the task using ScopeDatabase and databaseID
  • Adds log entry with scope parameters
  • Uses the renamed workflowInstance variable (improved naming)

363-368: LGTM! SwitchoverDatabaseNode task creation includes scope.


384-389: LGTM! FailoverDatabaseNode task creation includes scope.


411-414: LGTM! RemoveHost creates database-scoped tasks for each affected database.

This is correct - the RemoveHost workflow creates update tasks scoped to each database being modified.

server/internal/workflows/refresh_current_state.go (2)

65-72: LGTM! logTaskEvent call updated with database scope.


80-90: LGTM! Second logTaskEvent call updated with database scope.

server/internal/workflows/start_instance.go (3)

32-37: LGTM! Error handler UpdateTaskInput includes scope and entity.


45-50: LGTM! Start task UpdateTaskInput includes scope and entity.


67-72: LGTM! Completion task UpdateTaskInput includes scope and entity.

server/internal/api/apiv1/convert.go (1)

539-541: LGTM! Task API mapping includes new scope and entity fields.

The conversion correctly:

  • Maps Scope as a string
  • Maps EntityID directly
  • Makes DatabaseID nullable for non-database scopes
server/internal/workflows/stop_instance.go (1)

32-37: LGTM! Scope migration looks correct.

All three UpdateTaskInput constructions correctly use task.ScopeDatabase and input.DatabaseID as the EntityID, aligning with the new scoped task model.

Minor observation: The error handler at line 38 uses ExecuteUpdateTask directly, while the completion path at line 73 uses the w.updateTask helper. This inconsistency exists in the original code and isn't introduced by this PR, but could be unified in a future refactor.

Also applies to: 45-50, 67-72

server/internal/workflows/create_pgbackrest_backup.go (2)

44-44: LGTM! Cancellation path correctly updated with scope.

The cancelTask call now properly passes task.ScopeDatabase as the scope parameter, maintaining consistency with the new scoped task model.


53-58: LGTM! All UpdateTaskInput constructions correctly migrated.

All three task update paths (error handling, start, and complete) consistently use Scope: task.ScopeDatabase and EntityID: input.DatabaseID, aligning with the broader scope-aware refactoring.

Also applies to: 74-79, 126-131

server/internal/workflows/delete_database.go (2)

41-41: LGTM! Cancellation cleanup correctly updated.

The cancelTask invocation in the deferred cleanup block correctly passes task.ScopeDatabase as the scope parameter.


60-65: LGTM! Scope-based task updates correctly implemented.

All UpdateTaskInput constructions in the error handler, start, and completion paths consistently use task.ScopeDatabase and input.DatabaseID as EntityID.

Also applies to: 71-76, 109-114

server/internal/workflows/restart_instance.go (2)

47-47: LGTM! Cancellation path updated with scope.

The cancelTask call correctly includes task.ScopeDatabase as the scope parameter in the deferred cleanup block.


54-59: LGTM! UpdateTaskInput constructions correctly migrated.

All task update paths consistently use the new Scope and EntityID fields with appropriate values (task.ScopeDatabase and input.DatabaseID).

Also applies to: 67-72, 87-92

server/internal/workflows/switchover.go (2)

57-57: LGTM! Cancellation cleanup correctly scoped.

The cancelTask call in the deferred cleanup block correctly passes task.ScopeDatabase along with in.DatabaseID and in.TaskID.


63-68: LGTM! All four UpdateTaskInput paths correctly updated.

All task update constructions—error handler, start, early completion (when candidate is already leader), and final completion—consistently use Scope: task.ScopeDatabase and EntityID: in.DatabaseID.

Also applies to: 74-79, 142-147, 166-171

server/internal/workflows/activities/update_task.go (2)

16-21: LGTM! Clean struct refactoring for scope-based task identification.

The UpdateTaskInput struct now correctly uses Scope and EntityID to identify task parents, replacing the previous DatabaseID field. This aligns with the PR objective to support non-database entities.


38-51: LGTM! Logger and service call updated consistently.

The logger context and GetTask call are correctly updated to use the new scope-based parameters.

server/internal/workflows/update_database.go (4)

45-45: LGTM! Cancellation path updated with scope parameter.

The cancelTask call now correctly passes task.ScopeDatabase as the scope parameter.


66-72: LGTM! Error handler uses scope-based task identification.

The UpdateTaskInput in handleError correctly uses task.ScopeDatabase and input.Spec.DatabaseID as EntityID.


77-82: LGTM! Task start update uses scope-based identification.

Consistent with the new scope model.


133-138: LGTM! Task completion update uses scope-based identification.

All UpdateTaskInput instances in this workflow are consistently updated.

server/internal/workflows/common.go (1)

151-174: LGTM! logTaskEvent correctly updated for scope-based task identification.

The function signature and LogTaskEventInput construction properly use scope and entityID parameters.

server/internal/api/apiv1/post_init_handlers.go (6)

607-607: LGTM! Scope parameter added to switchover task check.

The GetTasks call now correctly passes task.ScopeDatabase to ensure proper scope-based filtering.


700-700: LGTM! Scope parameter added to failover task check.

Consistent with the switchover handler pattern.


731-731: LGTM! ListDatabaseTasks uses scope-based retrieval.

Correctly scoped to database tasks.


751-751: LGTM! GetDatabaseTask uses scope-based retrieval.


769-769: LGTM! Task log retrieval uses scope-based lookups.

Both the task existence check and log retrieval correctly use task.ScopeDatabase.

Also applies to: 779-779


1033-1033: LGTM! CancelDatabaseTask uses scope-based task lookup.

Correctly scoped for database task cancellation.

server/internal/workflows/failover.go (5)

41-41: LGTM! Cancellation cleanup uses scope-based task cancellation.

The cancelTask call correctly passes task.ScopeDatabase as the scope.


47-52: LGTM! Error handler uses scope-based task update.

UpdateTaskInput correctly uses Scope and EntityID fields.


57-62: LGTM! Task start update uses scope-based identification.

Consistent with the new scope model.


142-148: LGTM! Early completion path (candidate is leader) uses scope-based update.

Correctly handles the skip-failover scenario with proper scope fields.


165-170: LGTM! Final completion update uses scope-based identification.

All UpdateTaskInput instances in the failover workflow are consistently updated.

server/internal/workflows/pgbackrest_restore.go (3)

30-36: LGTM!

The deferred cancel handler correctly passes the new scope and entityID parameters to cancelTask, consistent with the updated signature in common.go.


52-57: LGTM!

The UpdateTaskInput constructions for both task start and completion correctly populate Scope and EntityID fields, replacing the previous DatabaseID approach.

Also applies to: 106-111


138-156: LGTM!

The error handler correctly updates both node tasks and the main task with the new Scope and EntityID fields, maintaining consistency across all failure paths.

api/apiv1/design/task.go (4)

17-20: LGTM!

The entity_id field correctly omits a UUID format constraint since it can hold either a database ID (UUID) or host ID (string), as reflected in the examples.


113-116: Verify intent: newly added field marked as deprecated.

The host_id attribute appears to be newly introduced in this PR but is immediately marked as deprecated. If this is intentional for backward compatibility purposes, consider clarifying this in the description. Otherwise, the deprecation notice may have been added in error.


66-66: LGTM!

The required fields now properly mandate scope and entity_id, reflecting the shift from database_id-based to scope-based task identification. This is a breaking API change that aligns with the PR objectives.


266-313: LGTM!

The ListDatabaseTasksResponse examples properly demonstrate both the new scope/entity_id fields alongside the existing database_id field, which aids API consumers during the transition.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@jason-lynch jason-lynch force-pushed the feat/PLAT-347/add-scope-to-tasks branch 2 times, most recently from 8619be9 to baedea3 Compare January 15, 2026 21:56
@jason-lynch jason-lynch force-pushed the feat/PLAT-347/migrations branch from d35513b to b81f4bf Compare January 16, 2026 14:21
Adds a "scope" concept to tasks to support tasks that apply to non-
database entities. With this change, the task's parent is identified
by the `scope` and `entity_id` fields. This changes the way that tasks
and task logs are stored, so we'll add a migration to move existing
tasks in a subsequent commit.

PLAT-347
Adds a migration to copy logs from their old location to the new
keyspace that includes the task scope. This change is non-destructive
to allow the possibility of a rollback. Tasks are updated throughout
their lifetime, so this migration will not overwrite tasks that have
already been migrated.

PLAT-347
Some small bug fixes that came up during code review

- `handleError` returns wrong error variable in
  `swarm/pgbackrest_restore.go`
- empty `Statuses` slice filters out all tasks in `task/service.go`
- error logged unconditionally regardless of `logTaskEvent` outcome in
  `workflows/common.go`
@jason-lynch jason-lynch force-pushed the feat/PLAT-347/add-scope-to-tasks branch from baedea3 to 1ad0c8b Compare January 16, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants