Skip to content

[4.x] Worker QoL#1147

Merged
RichardAnderson merged 6 commits into
vitodeploy:4.xfrom
RichardAnderson:qol/workers
Jun 10, 2026
Merged

[4.x] Worker QoL#1147
RichardAnderson merged 6 commits into
vitodeploy:4.xfrom
RichardAnderson:qol/workers

Conversation

@RichardAnderson

@RichardAnderson RichardAnderson commented Jun 6, 2026

Copy link
Copy Markdown
Member
  • Restart all workers
  • Resync worker status
  • Worker Environment Variables

Summary by CodeRabbit

  • New Features

    • Worker environment management: view/edit masked secrets for site or worker, with UI dialog and validation; site-level pre-deploy variables are supported and copied to bootstrap workers.
    • Bulk "Restart All Workers" action and "Resync Worker Statuses" endpoint to restart/server-scope workers and synchronize live statuses.
    • Supervisor/process-manager improvements for safer restarts and status reporting.
  • Tests

    • Broad test coverage added for environment handling, migrations, resync, and restart flows.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fd28a9f-6647-4965-9669-cda2912b84d7

📥 Commits

Reviewing files that changed from the base of the PR and between a276ce0 and aca3040.

📒 Files selected for processing (5)
  • app/Actions/Worker/UpdateWorkerEnvironment.php
  • app/Http/Controllers/SiteSettingController.php
  • resources/js/pages/application/components/app-with-deployment.tsx
  • resources/js/pages/workers/components/env-dialog.tsx
  • resources/js/pages/workers/components/worker-row-actions.tsx
✅ Files skipped from review due to trivial changes (1)
  • resources/js/pages/application/components/app-with-deployment.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/Actions/Worker/UpdateWorkerEnvironment.php
  • resources/js/pages/workers/components/env-dialog.tsx
  • app/Http/Controllers/SiteSettingController.php

📝 Walkthrough

Walkthrough

Adds encrypted environment-variable storage and APIs for Sites and Workers, validation/merge logic, supervisor formatting and site-scoped restarts, a WorkerEnvDialog UI, migration/backfill, worker status sync and restart orchestration, and comprehensive tests across models, controllers, services, and frontend.

Changes

Worker Environment Management

Layer / File(s) Summary
Worker environment data model and schema
app/Models/Worker.php, app/Models/Site.php, database/factories/WorkerFactory.php, database/migrations/2026_06_06_120237_add_worker_environment_support.php
Worker and Site models use encrypted array entry format {key,value,is_secret}; factory normalizes test data; migration adds sites.worker_environment and backfills workers.environment to the new encrypted list shape.
Environment validation and processing
app/Actions/Worker/UpdateWorkerEnvironment.php, app/Actions/Site/UpdateSiteWorkerEnvironment.php, app/Actions/Worker/CreateWorker.php
Validation rules and nested rules for variables, normalization via processVariables(), merging with stored values, UpdateSiteWorkerEnvironment delegates to worker when bootstrap exists or persists site pre-deploy variables. CreateWorker applies processVariables for initial environment.
Worker status sync and restart operations
app/Actions/Worker/SyncWorkerStatuses.php, app/Actions/Worker/RestartAllWorkers.php, app/Jobs/Worker/RestartAllJob.php
SyncWorkerStatuses maps supervisor process states to Worker status/error, persists changes and broadcasts site updates. RestartAllWorkers marks selected workers RESTARTING and dispatches RestartAllJob, which executes restartAll and recovers failed restarts by marking workers failed and broadcasting updates.
ProcessManager / Supervisor
app/Services/ProcessManager/ProcessManager.php, app/Services/ProcessManager/Supervisor.php
ProcessManager adds statuses() contract. Supervisor::formatEnvironment() sanitizes/escapes values for config, writeConfig uses formatted environment, restartAll supports site-scoped restartMany, and statuses() parsing is more defensive.
HTTP controllers and routes
app/Http/Controllers/API/WorkerController.php, app/Http/Controllers/WorkerController.php, app/Http/Controllers/SiteSettingController.php
API adds POST /workers/resync and POST /workers/restart-all (optional site scope). Web controllers add worker env GET/PATCH endpoints and site settings GET/PATCH for proxied sites, with masking and result-based flash messages.
Authorization policy
app/Policies/WorkerPolicy.php
New manage() method enforces write access, server readiness, and process manager availability for worker operations.
Site deployment propagation
app/SiteTypes/AbstractProxiedSiteType.php
afterDeploy() now passes site.worker_environment into bootstrap worker creation payload when present.
Supervisor templates
resources/views/ssh/services/process-manager/supervisor/*.blade.php
Status template captures supervisorctl status output; restart-all template runs supervisorctl update then restart; worker template emits preformatted environment (from Supervisor::formatEnvironment()).
Frontend WorkerEnv dialog & registry
resources/js/pages/workers/components/env-dialog.tsx, resources/js/components/dialogs/registry.ts
WorkerEnvDialog fetches/masks/edits variables, supports add/edit/delete, duplicate detection, and conditional restart choice; registered as workerEnv in dialog registry.
Frontend components and pages
resources/js/pages/workers/components/worker-row-actions.tsx, resources/js/components/radio-card.tsx, resources/js/pages/workers/components/columns.tsx, resources/js/pages/workers/index.tsx, resources/js/pages/application/components/proxied-app-card.tsx, resources/js/pages/site-settings/components/start-command.tsx
Adds WorkerEnvironment action and menu items, shared RadioCard component, Workers index dropdown with resync/restart-all actions, proxied-app-card environment shortcut, and start-command uses shared RadioCard.
Utilities, editor, build & docs
resources/js/lib/env.ts, resources/js/lib/editor.ts, vite.config.ts, public/api-docs/openapi/workers.yaml
Introduces generateUniqueKey, refactors Bash tokenizer, adds Vite alias for decimal.mjs, and updates OpenAPI spec to include environment schema and resync/restart-all endpoints.
Tests: migration, unit, feature
tests/Feature/WorkerEnvironmentMigrationTest.php, tests/Unit/AbstractProxiedSiteTypeTest.php, tests/Feature/AfterDeployHookTest.php, tests/Feature/WorkerEnvironmentTest.php, tests/Feature/SiteSettingsProxiedSiteTest.php, tests/Feature/API/WorkersTest.php, tests/Feature/WorkersTest.php
Adds migration tests, unit tests for Supervisor::formatEnvironment, feature tests for env read/update/restart, site settings endpoints, API resync/restart-all, restart/resync flows, and authorization/validation coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • vitodeploy/vito#1140: Introduces the centralized dialog registry and infrastructure used by the new workerEnv dialog.

Suggested reviewers

  • saeedvaziry

Poem

🐰 I hopped through envs with a tiny pen,
Secrets encrypted — safe in my den,
Dialogs pop, supervisors hum,
Restarts dance and statuses come,
A bunny cheers: deploys made zen!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "[4.x] Worker QoL" is vague and generic, using non-descriptive terminology ("QoL") that doesn't convey specific information about the changeset's primary features. Revise the title to be more specific and descriptive, such as: "Add worker environment variables, restart-all, and status sync functionality" or similar phrasing that explicitly names the key features being introduced.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (4)
tests/Feature/WorkerEnvironmentMigrationTest.php (1)

73-77: ⚡ Quick win

Make migration resolution deterministic in runMigration().

Line 73 + Line 76 currently rely on the first glob() match. If more than one file matches, this test can run the wrong migration and produce misleading results. Prefer asserting a single match (or selecting by exact basename) before require.

Proposed fix
 private function runMigration(): void
 {
     $paths = glob(database_path('migrations/*_add_worker_environment_support.php')) ?: [];
-    $this->assertNotEmpty($paths, 'Worker environment migration not found.');
+    sort($paths);
+    $this->assertCount(1, $paths, 'Expected exactly one worker environment migration file.');
 
     $migration = require $paths[0];
     $migration->up();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Feature/WorkerEnvironmentMigrationTest.php` around lines 73 - 77, The
test currently grabs migrations with $paths =
glob(database_path('migrations/*_add_worker_environment_support.php')) and
requires $paths[0], which is non-deterministic if multiple files match; update
the test in WorkerEnvironmentMigrationTest (the runMigration() flow) to
deterministically resolve the migration by either asserting exactly one match
with $this->assertCount(1, $paths) before requiring, or locate the exact
basename (e.g. array_filter by basename ===
'YYYY_add_worker_environment_support.php') and then require that chosen path
instead of blindly using $paths[0], then call up() on the resolved migration.

Source: Linters/SAST tools

app/Models/Worker.php (1)

57-57: ⚡ Quick win

Use the model secret cast format required by the repo standard.

Line 57 uses 'encrypted:array', but this field stores sensitive environment values and should follow the project’s 'encrypted:json' convention for secret-bearing model attributes.

Suggested change
-        'environment' => 'encrypted:array',
+        'environment' => 'encrypted:json',

As per coding guidelines, “Use 'encrypted:json' cast on model properties that store credentials or secrets.” Based on learnings, the same model-secret cast rule is explicitly reinforced for app/Models/**/*.php.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/Worker.php` at line 57, The model cast for the sensitive
"environment" attribute in the Worker model is using 'encrypted:array' but must
follow the repo secret convention; update the cast for the 'environment'
property in app/Models/Worker.php (class Worker) from 'encrypted:array' to
'encrypted:json' so secrets/credentials are stored with the JSON secret cast.

Sources: Coding guidelines, Learnings

app/Models/Site.php (1)

126-126: ⚡ Quick win

Align worker_environment cast with the repository’s secret-storage standard.

Line 126 should use the secret-field cast convention used by this codebase for credentials/secrets.

Suggested change
-        'worker_environment' => 'encrypted:array',
+        'worker_environment' => 'encrypted:json',

As per coding guidelines, “Use 'encrypted:json' cast on model properties that store credentials or secrets.” Based on learnings, this same rule is explicitly tracked for model files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/Site.php` at line 126, The Site model's cast for the
worker_environment attribute uses 'encrypted:array' but the repository standard
requires secret fields to use the 'encrypted:json' cast; update the Site model's
$casts entry for 'worker_environment' to 'encrypted:json' (locate the $casts
array in the Site class where 'worker_environment' is defined) so the field
follows the secret-storage convention used across the codebase.

Sources: Coding guidelines, Learnings

app/Jobs/Worker/RestartAllJob.php (1)

43-43: ⚡ Quick win

Align failed() signature with the jobs convention.

Use Exception in failed() to match the project’s job contract and keep failure handlers consistent.

As per coding guidelines, app/Jobs/**/*.php should implement failed(Exception $e) for error handling/status updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/Worker/RestartAllJob.php` at line 43, Change the
RestartAllJob::failed method signature from failed(Throwable $e): void to
failed(Exception $e): void to match the project's job contract; update the type
hint and any usages inside failed() accordingly (ensure imports/use statements
reference Exception if necessary) so the method implements the expected
failed(Exception $e) handler for app/Jobs.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Actions/Worker/RestartAllWorkers.php`:
- Around line 15-25: The worker status updates in RestartAllWorkers (the block
calling $server->workers()->when(...)->whereNotIn(...)->get()->each(...)) must
be wrapped in a database transaction to avoid partial state if a save fails;
change the logic to perform the fetch and the per-Worker status/error mutation
inside a DB::transaction (or use an atomic bulk update on the query) so all
matching rows are set to WorkerStatus::RESTARTING with error=null together, then
after the transaction dispatch the RestartAllJob to the 'ssh' queue; ensure you
reference WorkerStatus::RESTARTING, the same query used
($server->workers()->when(...)->whereNotIn(...)) and dispatch(new
RestartAllJob($server, $site)) when making the change.

In `@app/Actions/Worker/UpdateWorkerEnvironment.php`:
- Around line 13-18: The PHPDoc for the update(Worker $worker, array $input):
WorkerEnvironmentUpdateResult method currently only documents `@throws` SSHError
but Validator::make(...)->validate() can throw ValidationException; update the
PHPDoc block for the update method to include `@throws` ValidationException (or
the fully-qualified Illuminate\Validation\ValidationException) alongside
SSHError so the annotations accurately reflect possible exceptions thrown by
update().

In `@app/Jobs/Worker/RestartAllJob.php`:
- Line 32: The unique lock key passed to run(...) only uses
"server-{$this->server->id}", which can collide for different site-scoped
restart jobs; update the key to include the site scope (e.g., append the site
identifier such as $this->site->id or $this->site->uuid) so each site/server
combo is unique. Locate the run(...) call in RestartAllJob.php (the line with
$this->run("server-{$this->server->id}", ...)) and modify the string to include
the site identifier (using $this->site or the appropriate site property) so the
lock distinguishes site-scoped jobs from each other.

In `@app/Policies/WorkerPolicy.php`:
- Around line 39-44: manage() accepts a $site but never verifies it belongs to
the given $server, allowing scoped operations against mismatched Site objects;
update the method to also check that when $site is provided it is tied to the
same server (e.g. add a condition like $site === null || $site->server_id ===
$server->id or $site->server->id === $server->id) in the return expression
alongside the existing hasWriteAccess($user, $server->project),
$server->isReady() and $server->processManager() checks so the Site-to-Server
boundary is enforced.

In `@app/Services/ProcessManager/Supervisor.php`:
- Around line 228-248: The parsing loop in Supervisor.php silently ignores lines
it cannot parse (using $output and building $statuses) which leads to empty maps
for non-empty, unparsable output; update the method to detect when $output is
non-empty but no valid status entries were produced (e.g., $output !== '' and
empty($statuses) after the loop) and fail fast by throwing an exception
(RuntimeException or the existing ProcessManagerException) with a clear message
including the raw $output so callers can handle command/parsing failures instead
of treating them as “Process not found”; implement this check immediately after
the loop that iterates explode("\n", $output) and before returning $statuses.

In `@database/migrations/2026_06_06_120237_add_worker_environment_support.php`:
- Around line 50-52: The down() migration always drops the
sites.worker_environment column which can delete preexisting data; update down()
to mirror up() by checking existence before dropping (e.g., use
Schema::hasColumn('sites', 'worker_environment') or the same conditional used in
up()) and only call $table->dropColumn('worker_environment') when that check
passes so rollback doesn't remove unrelated columns.

In `@public/api-docs/openapi/workers.yaml`:
- Around line 145-160: The OpenAPI schema for environment.value is declared as a
non-null string but the backend permits null; update the environment.*.value
property in public/api-docs/openapi/workers.yaml (the "value" property inside
the environment variable object) to allow null by adding nullable: true (or
change the type to allow "null", e.g., type: [ "string", "null" ]) while keeping
the property listed in required if the field must be present; ensure the
description reflects that null is allowed.

In `@resources/js/pages/workers/components/env-dialog.tsx`:
- Around line 157-167: The Save button is still enabled when fetching
environment variables fails (query.isError), allowing submission of an empty
`variables` payload and accidentally clearing existing variables; update the
Save button (the Button that triggers the save handler — e.g.,
handleSave/onSave) to be disabled when query.isError or when the fetch hasn't
succeeded (use !query.isSuccess || query.isFetching || query.isError), and add a
guard in the save handler (handleSave/onSave) to no-op or show an error if
`variables` is empty or the query is not successful to prevent sending an empty
payload.

In `@tests/Feature/API/WorkersTest.php`:
- Around line 472-474: The test currently hardcodes the foreign key when
creating the Server fixture (Server::factory()->create(['user_id' => 1])), which
makes the test order-dependent; change it to reference an explicit user instance
instead—either use $this->user->id if the test sets up $this->user, or create a
User via User::factory()->create() and pass that user's id into
Server::factory()->create(['user_id' => $user->id']) so the fixture is
deterministic.

---

Nitpick comments:
In `@app/Jobs/Worker/RestartAllJob.php`:
- Line 43: Change the RestartAllJob::failed method signature from
failed(Throwable $e): void to failed(Exception $e): void to match the project's
job contract; update the type hint and any usages inside failed() accordingly
(ensure imports/use statements reference Exception if necessary) so the method
implements the expected failed(Exception $e) handler for app/Jobs.

In `@app/Models/Site.php`:
- Line 126: The Site model's cast for the worker_environment attribute uses
'encrypted:array' but the repository standard requires secret fields to use the
'encrypted:json' cast; update the Site model's $casts entry for
'worker_environment' to 'encrypted:json' (locate the $casts array in the Site
class where 'worker_environment' is defined) so the field follows the
secret-storage convention used across the codebase.

In `@app/Models/Worker.php`:
- Line 57: The model cast for the sensitive "environment" attribute in the
Worker model is using 'encrypted:array' but must follow the repo secret
convention; update the cast for the 'environment' property in
app/Models/Worker.php (class Worker) from 'encrypted:array' to 'encrypted:json'
so secrets/credentials are stored with the JSON secret cast.

In `@tests/Feature/WorkerEnvironmentMigrationTest.php`:
- Around line 73-77: The test currently grabs migrations with $paths =
glob(database_path('migrations/*_add_worker_environment_support.php')) and
requires $paths[0], which is non-deterministic if multiple files match; update
the test in WorkerEnvironmentMigrationTest (the runMigration() flow) to
deterministically resolve the migration by either asserting exactly one match
with $this->assertCount(1, $paths) before requiring, or locate the exact
basename (e.g. array_filter by basename ===
'YYYY_add_worker_environment_support.php') and then require that chosen path
instead of blindly using $paths[0], then call up() on the resolved migration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cafe3fa8-8bd9-4c12-87f2-df1cde91da14

📥 Commits

Reviewing files that changed from the base of the PR and between 13b59f3 and a276ce0.

📒 Files selected for processing (41)
  • app/Actions/Site/UpdateSiteWorkerEnvironment.php
  • app/Actions/Worker/CreateWorker.php
  • app/Actions/Worker/RestartAllWorkers.php
  • app/Actions/Worker/SyncWorkerStatuses.php
  • app/Actions/Worker/UpdateWorkerEnvironment.php
  • app/Http/Controllers/API/WorkerController.php
  • app/Http/Controllers/SiteSettingController.php
  • app/Http/Controllers/WorkerController.php
  • app/Jobs/Worker/RestartAllJob.php
  • app/Models/Site.php
  • app/Models/Worker.php
  • app/Policies/WorkerPolicy.php
  • app/Services/ProcessManager/ProcessManager.php
  • app/Services/ProcessManager/Supervisor.php
  • app/SiteTypes/AbstractProxiedSiteType.php
  • app/Support/Testing/SSHFake.php
  • database/factories/WorkerFactory.php
  • database/migrations/2026_06_06_120237_add_worker_environment_support.php
  • public/api-docs/openapi/workers.yaml
  • resources/js/components/dialogs/registry.ts
  • resources/js/components/radio-card.tsx
  • resources/js/lib/editor.ts
  • resources/js/lib/env.ts
  • resources/js/pages/application/components/env.tsx
  • resources/js/pages/application/components/proxied-app-card.tsx
  • resources/js/pages/site-settings/components/start-command.tsx
  • resources/js/pages/workers/components/columns.tsx
  • resources/js/pages/workers/components/env-dialog.tsx
  • resources/js/pages/workers/components/worker-row-actions.tsx
  • resources/js/pages/workers/index.tsx
  • resources/views/ssh/services/process-manager/supervisor/restart-all-workers.blade.php
  • resources/views/ssh/services/process-manager/supervisor/worker-statuses.blade.php
  • resources/views/ssh/services/process-manager/supervisor/worker.blade.php
  • tests/Feature/API/WorkersTest.php
  • tests/Feature/AfterDeployHookTest.php
  • tests/Feature/SiteSettingsProxiedSiteTest.php
  • tests/Feature/WorkerEnvironmentMigrationTest.php
  • tests/Feature/WorkerEnvironmentTest.php
  • tests/Feature/WorkersTest.php
  • tests/Unit/AbstractProxiedSiteTypeTest.php
  • vite.config.ts

Comment thread app/Actions/Worker/RestartAllWorkers.php
Comment thread app/Actions/Worker/UpdateWorkerEnvironment.php
Comment thread app/Jobs/Worker/RestartAllJob.php
Comment thread app/Policies/WorkerPolicy.php
Comment thread app/Services/ProcessManager/Supervisor.php
Comment thread public/api-docs/openapi/workers.yaml
Comment thread resources/js/pages/workers/components/env-dialog.tsx
Comment thread tests/Feature/API/WorkersTest.php
@RichardAnderson RichardAnderson merged commit aa266e1 into vitodeploy:4.x Jun 10, 2026
4 checks passed
@RichardAnderson RichardAnderson deleted the qol/workers branch June 10, 2026 11:56
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