From fb329cd29e301569a21278d119e26faa21e2d971 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Mon, 14 Sep 2026 12:39:38 -0700 Subject: [PATCH 1/8] Update workflow reference to enable-octane-cicd --- .github/workflows/deploy-pm4.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-pm4.yml b/.github/workflows/deploy-pm4.yml index 4b05f4736b..176a237412 100644 --- a/.github/workflows/deploy-pm4.yml +++ b/.github/workflows/deploy-pm4.yml @@ -15,5 +15,5 @@ concurrency: jobs: run: name: Run PM4-workflow - uses: processmaker/.github/.github/workflows/deploy-pm4.yml@main + uses: processmaker/.github/.github/workflows/deploy-pm4.yml@enable-octane-cicd secrets: inherit From 0c649e9a285e76f0e14935ac4fafb784bf949420 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 15 Sep 2026 15:25:35 -0700 Subject: [PATCH 2/8] Skip warming AnonymouUser in multitenancy --- config/octane.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/config/octane.php b/config/octane.php index 13cddfe169..45ee55519d 100644 --- a/config/octane.php +++ b/config/octane.php @@ -22,6 +22,9 @@ use Laravel\Octane\Listeners\ReportException; use Laravel\Octane\Listeners\StopWorkerIfNecessary; use Laravel\Octane\Octane; +use ProcessMaker\Models\AnonymousUser; + +$multitenancyEnabled = filter_var(env('MULTITENANCY', false), FILTER_VALIDATE_BOOL); return [ @@ -132,7 +135,11 @@ 'warm' => [ ...Octane::defaultServicesToWarm(), - ProcessMaker\Models\AnonymousUser::class, + // AnonymousUser is tenant-scoped; warming it at worker boot queries the + // default connection before any tenant is resolved (breaks multitenancy). + ...($multitenancyEnabled ? [] : [ + AnonymousUser::class, + ]), ProcessMaker\ImportExport\Extension::class, ProcessMaker\ImportExport\SignalHelper::class, ProcessMaker\Managers\MenuManager::class, @@ -157,6 +164,9 @@ ProcessMaker\Managers\ModelerManager::class, ProcessMaker\Managers\ScreenBuilderManager::class, Lavary\Menu\Menu::class, + ...($multitenancyEnabled ? [ + AnonymousUser::class, + ] : []), ], /* From 93bfb7ccf110ba14382c996248e1fb387587c206 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Mon, 21 Sep 2026 06:23:11 -0700 Subject: [PATCH 3/8] Rebuild token guard on tenant switch --- .../Auth/PassportTokenGuardFactory.php | 36 +++++++++ ProcessMaker/Multitenancy/SwitchTenant.php | 31 +++++++- .../Providers/AuthServiceProvider.php | 19 +++++ .../Auth/PassportTokenGuardFactoryTest.php | 75 +++++++++++++++++++ .../Multitenancy/SwitchTenantTest.php | 70 +++++++++++++++++ 5 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 ProcessMaker/Auth/PassportTokenGuardFactory.php create mode 100644 tests/unit/ProcessMaker/Auth/PassportTokenGuardFactoryTest.php create mode 100644 tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php diff --git a/ProcessMaker/Auth/PassportTokenGuardFactory.php b/ProcessMaker/Auth/PassportTokenGuardFactory.php new file mode 100644 index 0000000000..3e73bd67e6 --- /dev/null +++ b/ProcessMaker/Auth/PassportTokenGuardFactory.php @@ -0,0 +1,36 @@ +make(ResourceServer::class), + new PassportUserProvider(Auth::createUserProvider($config['provider']), $config['provider']), + $app->make(ClientRepository::class), + $app->make('encrypter'), + $app->make('request') + ), function (TokenGuard $guard) use ($app): void { + $app->refresh('request', $guard, 'setRequest'); + }); + } +} diff --git a/ProcessMaker/Multitenancy/SwitchTenant.php b/ProcessMaker/Multitenancy/SwitchTenant.php index e85c1421f7..ab08830519 100644 --- a/ProcessMaker/Multitenancy/SwitchTenant.php +++ b/ProcessMaker/Multitenancy/SwitchTenant.php @@ -6,7 +6,10 @@ use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Support\Arr; use Illuminate\Support\Env; -use Monolog\Handler\RotatingFileHandler; +use Laravel\Passport\ApiTokenCookieFactory; +use Laravel\Passport\ClientRepository; +use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\ResourceServer; use ProcessMaker\Application; use ProcessMaker\Multitenancy\Broadcasting\TenantAwareBroadcastManager; use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig; @@ -65,7 +68,31 @@ public function forgetCurrent(): void // app key / encrypter $this->setConfig('app.key', $this->landlordConfig('app.key')); + $this->flushTenantSensitiveSingletons($app); + } + + /** + * Drop container instances that captured the previous tenant's APP_KEY or oauth keys. + * + * Passport's ResourceServer/AuthorizationServer and Encrypter are singletons. + * Under Octane they can outlive a tenant switch and keep signing or verifying + * laravel_token cookies with the wrong key (API 401s). + */ + private function flushTenantSensitiveSingletons(Application $app): void + { $app->forgetInstance('encrypter'); + $app->forgetInstance(ResourceServer::class); + $app->forgetInstance(AuthorizationServer::class); + $app->forgetInstance(ClientRepository::class); + $app->forgetInstance(ApiTokenCookieFactory::class); + + if ($app->resolved('auth.driver')) { + $app->forgetInstance('auth.driver'); + } + + if ($app->resolved('auth')) { + $app->make('auth')->forgetGuards(); + } } private function landlordConfig($key) @@ -130,7 +157,7 @@ private function overrideConfigs(Application $app, IsTenant $tenant) // app key / encrypter $landlordEncrypter = $app->make('encrypter'); $this->setConfig('app.key', $landlordEncrypter->decryptString($tenant->config['app.key'])); - $app->forgetInstance('encrypter'); + $this->flushTenantSensitiveSingletons($app); // Logging $this->setConfig('logging.channels.daily.path', storage_path('logs/processmaker.log')); diff --git a/ProcessMaker/Providers/AuthServiceProvider.php b/ProcessMaker/Providers/AuthServiceProvider.php index 36858333c5..6c8f110060 100644 --- a/ProcessMaker/Providers/AuthServiceProvider.php +++ b/ProcessMaker/Providers/AuthServiceProvider.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Log; use Laravel\Passport\Passport; +use ProcessMaker\Auth\PassportTokenGuardFactory; use ProcessMaker\Events\TenantResolved; use ProcessMaker\Models\AnonymousUser; use ProcessMaker\Models\Media; @@ -64,6 +65,8 @@ public function boot() Passport::authorizationView('auth.oauth2.authorize'); + $this->registerPassportGuard(); + Gate::before(function ($user) { if ($user->is_administrator) { return true; @@ -113,4 +116,20 @@ public function register() $this->defineGates(); }); } + + /** + * Replace Passport's guard so TokenGuard is resolved from the current app. + * + * Passport binds the guard with the service provider's root container. + * Octane clones a sandbox per request and SwitchTenant swaps APP_KEY on + * that sandbox; the root worker still holds the landlord Encrypter. + */ + private function registerPassportGuard(): void + { + Auth::resolved(function ($auth): void { + $auth->extend('passport', function ($app, $name, array $config) { + return $app->make(PassportTokenGuardFactory::class)->make($app, $config); + }); + }); + } } diff --git a/tests/unit/ProcessMaker/Auth/PassportTokenGuardFactoryTest.php b/tests/unit/ProcessMaker/Auth/PassportTokenGuardFactoryTest.php new file mode 100644 index 0000000000..ef3448648b --- /dev/null +++ b/tests/unit/ProcessMaker/Auth/PassportTokenGuardFactoryTest.php @@ -0,0 +1,75 @@ +instance('encrypter', $encrypter); + $app->instance('request', Request::create('/')); + $app->instance(ResourceServer::class, Mockery::mock(ResourceServer::class)); + $app->instance(ClientRepository::class, Mockery::mock(ClientRepository::class)); + + $guard = (new PassportTokenGuardFactory())->make($app, [ + 'provider' => 'users', + ]); + + $this->assertInstanceOf(TokenGuard::class, $guard); + $this->assertSame($encrypter, $this->guardEncrypter($guard)); + } + + public function test_passport_guard_uses_the_auth_manager_application_encrypter(): void + { + $root = app(); + $auth = $root->make('auth'); + + $sandbox = clone $root; + $sandboxEncrypter = new Encrypter( + Encrypter::generateKey(config('app.cipher')), + config('app.cipher') + ); + $sandbox->instance('encrypter', $sandboxEncrypter); + $sandbox->instance('request', Request::create('/')); + $sandbox->instance(ResourceServer::class, Mockery::mock(ResourceServer::class)); + $sandbox->instance(ClientRepository::class, Mockery::mock(ClientRepository::class)); + + $auth->setApplication($sandbox); + $auth->forgetGuards(); + + try { + $guard = $auth->guard('api'); + + $this->assertInstanceOf(TokenGuard::class, $guard); + $this->assertSame($sandboxEncrypter, $this->guardEncrypter($guard)); + } finally { + $auth->setApplication($root); + $auth->forgetGuards(); + } + } + + private function guardEncrypter(TokenGuard $guard): object + { + $property = (new ReflectionClass($guard))->getProperty('encrypter'); + + return $property->getValue($guard); + } +} diff --git a/tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php b/tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php new file mode 100644 index 0000000000..db229a0af4 --- /dev/null +++ b/tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php @@ -0,0 +1,70 @@ +instance(ResourceServer::class, Mockery::mock(ResourceServer::class)); + $app->instance(AuthorizationServer::class, Mockery::mock(AuthorizationServer::class)); + $app->instance(ClientRepository::class, Mockery::mock(ClientRepository::class)); + + $auth = $app->make('auth'); + $auth->guard('web'); + + $previousEncrypter = $app->make('encrypter'); + $tenantKey = 'base64:' . base64_encode(Encrypter::generateKey(config('app.cipher'))); + + $tenant = new Tenant(); + $tenant->id = 999001; + $tenant->domain = 'tenant-999001.test'; + $tenant->database = config('database.connections.processmaker.database'); + $tenant->config = [ + 'app.url' => config('app.url'), + 'app.key' => Crypt::encryptString($tenantKey), + ]; + + $switch = new SwitchTenant(); + + try { + $switch->makeCurrent($tenant); + + $this->assertArrayNotHasKey(ResourceServer::class, $this->containerInstances($app)); + $this->assertArrayNotHasKey(AuthorizationServer::class, $this->containerInstances($app)); + $this->assertArrayNotHasKey(ClientRepository::class, $this->containerInstances($app)); + $this->assertSame([], $this->authGuards($auth)); + $this->assertNotSame($previousEncrypter, $app->make('encrypter')); + } finally { + $switch->forgetCurrent(); + } + } + + private function containerInstances($app): array + { + $property = (new ReflectionObject($app))->getProperty('instances'); + + return $property->getValue($app); + } + + private function authGuards($auth): array + { + $property = (new ReflectionObject($auth))->getProperty('guards'); + + return $property->getValue($auth); + } +} From c1ebd2de42d5ef8cdc326ffedf1d00d359368805 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Mon, 21 Sep 2026 06:45:51 -0700 Subject: [PATCH 4/8] Disable realtime output by default to speed things up --- phpunit.xml | 1 + tests/Extensions/RealTimeOutputExtension.php | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/phpunit.xml b/phpunit.xml index 6d8310e71c..218d88983a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -34,6 +34,7 @@ + diff --git a/tests/Extensions/RealTimeOutputExtension.php b/tests/Extensions/RealTimeOutputExtension.php index 8a317305b7..df9d13316c 100644 --- a/tests/Extensions/RealTimeOutputExtension.php +++ b/tests/Extensions/RealTimeOutputExtension.php @@ -29,6 +29,11 @@ class RealTimeOutputExtension implements Extension public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void { + $enabled = getenv('PHPUNIT_REALTIME_OUTPUT'); + if (!filter_var($enabled === false ? 'false' : $enabled, FILTER_VALIDATE_BOOLEAN)) { + return; + } + $facade->registerSubscriber(new class implements PreparationStartedSubscriber { public function notify(PreparationStarted $event): void { From e75feae46e5ed21b129eca4d963ee8e567c6cad5 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Mon, 21 Sep 2026 11:58:45 -0700 Subject: [PATCH 5/8] Fix broadcasting auth for multitenant octane --- .../TenantAwareBroadcastManager.php | 18 ++- .../TenantAwarePusherBroadcaster.php | 94 +++++++++--- ProcessMaker/Multitenancy/SwitchTenant.php | 7 - .../Providers/BroadcastServiceProvider.php | 28 +++- .../settings/components/SettingsListing.vue | 2 +- .../TenantAwarePusherBroadcasterTest.php | 135 ++++++++++++++++++ .../Multitenancy/SwitchTenantTest.php | 50 +++++-- 7 files changed, 287 insertions(+), 47 deletions(-) create mode 100644 tests/unit/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcasterTest.php diff --git a/ProcessMaker/Multitenancy/Broadcasting/TenantAwareBroadcastManager.php b/ProcessMaker/Multitenancy/Broadcasting/TenantAwareBroadcastManager.php index c2570bc129..d827a8ddff 100644 --- a/ProcessMaker/Multitenancy/Broadcasting/TenantAwareBroadcastManager.php +++ b/ProcessMaker/Multitenancy/Broadcasting/TenantAwareBroadcastManager.php @@ -6,16 +6,14 @@ class TenantAwareBroadcastManager extends BroadcastManager { - private int $tenantId; - - public function __construct($app, int $tenantId) - { - parent::__construct($app); - $this->tenantId = $tenantId; - } - - public function createPusherDriver($config) + /** + * Create an instance of the driver. + * + * @param array $config + * @return \Illuminate\Contracts\Broadcasting\Broadcaster + */ + protected function createPusherDriver(array $config) { - return new TenantAwarePusherBroadcaster($this->pusher($config), $this->tenantId); + return new TenantAwarePusherBroadcaster($this->pusher($config), $config['jsonp'] ?? false); } } diff --git a/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcaster.php b/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcaster.php index bb5fafcc63..aa78b073e6 100644 --- a/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcaster.php +++ b/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcaster.php @@ -3,41 +3,99 @@ namespace ProcessMaker\Multitenancy\Broadcasting; use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster; -use Pusher\Pusher; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class TenantAwarePusherBroadcaster extends PusherBroadcaster { - private int $tenantId; + /** + * Authenticate the incoming request for a given channel. + * + * Channel callbacks are registered without a tenant prefix (once per Octane + * worker). Incoming Echo channels are prefixed, so strip the current + * tenant's prefix before matching. Pusher still signs the original name. + * + * @param \Illuminate\Http\Request $request + * @return mixed + * + * @throws AccessDeniedHttpException + */ + public function auth($request) + { + $channelName = $this->normalizeChannelName($request->channel_name); + $channelName = $this->unprefixTenantChannel($channelName); + + if (empty($request->channel_name) || + ($this->isGuardedChannel($request->channel_name) && + !$this->retrieveUser($request, $channelName))) { + throw new AccessDeniedHttpException; + } - public function __construct(Pusher $pusher, int $tenantId) + return parent::verifyUserCanAccessChannel( + $request, $channelName + ); + } + + /** + * @param array $channels + * @return array + */ + protected function formatChannels(array $channels) { - parent::__construct($pusher); - $this->tenantId = $tenantId; + return array_map(function ($channel) { + return $this->prefixTenantChannel((string) $channel); + }, $channels); } - public function channel($channel, $callback, $options = []) + private function currentTenantId(): ?int { - $channel = "tenant_{$this->tenantId}.{$channel}"; + $tenant = app()->bound('currentTenant') ? app('currentTenant') : null; - return parent::channel($channel, $callback, $options); + return $tenant?->id ? (int) $tenant->id : null; } - protected function formatChannels(array $channels) + private function tenantPrefix(): ?string { - $channels = array_map(function ($channel) { - $channel = (string) $channel; - if ($this->tenantId) { - // Check if channel starts with "private-" - if (str_starts_with($channel, 'private-')) { - return "private-tenant_{$this->tenantId}." . substr($channel, 8); // Remove "private-" prefix and add tenant before the rest + $tenantId = $this->currentTenantId(); + + return $tenantId ? "tenant_{$tenantId}." : null; + } + + private function prefixTenantChannel(string $channel): string + { + $prefix = $this->tenantPrefix(); + if ($prefix === null) { + return $channel; + } + + foreach (['private-encrypted-', 'private-', 'presence-'] as $guardPrefix) { + if (str_starts_with($channel, $guardPrefix)) { + $name = substr($channel, strlen($guardPrefix)); + if (str_starts_with($name, $prefix)) { + return $channel; } - return "tenant_{$this->tenantId}.{$channel}"; + return $guardPrefix . $prefix . $name; } + } + if (str_starts_with($channel, $prefix)) { return $channel; - }, $channels); + } + + return $prefix . $channel; + } + + private function unprefixTenantChannel(string $channelName): string + { + $prefix = $this->tenantPrefix(); + if ($prefix === null) { + return $channelName; + } + + if (!str_starts_with($channelName, $prefix)) { + throw new AccessDeniedHttpException; + } - return $channels; + return substr($channelName, strlen($prefix)); } } diff --git a/ProcessMaker/Multitenancy/SwitchTenant.php b/ProcessMaker/Multitenancy/SwitchTenant.php index ab08830519..cd35cd353c 100644 --- a/ProcessMaker/Multitenancy/SwitchTenant.php +++ b/ProcessMaker/Multitenancy/SwitchTenant.php @@ -2,7 +2,6 @@ namespace ProcessMaker\Multitenancy; -use Illuminate\Broadcasting\BroadcastManager; use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Support\Arr; use Illuminate\Support\Env; @@ -11,7 +10,6 @@ use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\ResourceServer; use ProcessMaker\Application; -use ProcessMaker\Multitenancy\Broadcasting\TenantAwareBroadcastManager; use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig; use Spatie\Multitenancy\Contracts\IsTenant; use Spatie\Multitenancy\Tasks\SwitchTenantTask; @@ -43,11 +41,6 @@ public function makeCurrent(IsTenant $tenant): void request()->headers->set('host', $tenant->domain); $this->overrideConfigs($app, $tenant); - - // Extend BroadcastManager to our custom implementation that prefixes the channel names with the tenant id. - $app->extend(BroadcastManager::class, function ($manager, $app) use ($tenant) { - return new TenantAwareBroadcastManager($app, $tenant->id); - }); } /** diff --git a/ProcessMaker/Providers/BroadcastServiceProvider.php b/ProcessMaker/Providers/BroadcastServiceProvider.php index 233252f282..17b5176608 100644 --- a/ProcessMaker/Providers/BroadcastServiceProvider.php +++ b/ProcessMaker/Providers/BroadcastServiceProvider.php @@ -2,8 +2,12 @@ namespace ProcessMaker\Providers; +use Illuminate\Broadcasting\BroadcastManager; +use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract; +use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory; use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\ServiceProvider; +use ProcessMaker\Multitenancy\Broadcasting\TenantAwareBroadcastManager; class BroadcastServiceProvider extends ServiceProvider { @@ -14,7 +18,29 @@ class BroadcastServiceProvider extends ServiceProvider */ public function boot() { - Broadcast::routes(['middleware'=>['web', 'auth:anon']]); + if (config('app.multitenancy')) { + $this->useTenantAwareBroadcastManager(); + } + + Broadcast::routes(['middleware' => ['web', 'auth:anon']]); require base_path('routes/channels.php'); } + + /** + * Replace Laravel's deferred BroadcastManager after it has registered, so + * channel callbacks stay on one driver instance for the life of the Octane + * worker. Tenant prefixes are applied at auth/broadcast time instead. + */ + private function useTenantAwareBroadcastManager(): void + { + $this->app->make(BroadcastManager::class); + + $manager = new TenantAwareBroadcastManager($this->app); + $this->app->instance(BroadcastManager::class, $manager); + $this->app->instance(BroadcastingFactory::class, $manager); + $this->app->forgetInstance(BroadcasterContract::class); + + Broadcast::clearResolvedInstance(BroadcastManager::class); + Broadcast::clearResolvedInstance(BroadcastingFactory::class); + } } diff --git a/resources/js/admin/settings/components/SettingsListing.vue b/resources/js/admin/settings/components/SettingsListing.vue index fd2ae5c596..1fbaeb1f57 100644 --- a/resources/js/admin/settings/components/SettingsListing.vue +++ b/resources/js/admin/settings/components/SettingsListing.vue @@ -374,7 +374,7 @@ export default { dataProvider(context, callback) { this.filter = ''; this.pmql = ''; - if (this.searchQuery.isPMQL()) { + if (isPMQL.call(this.searchQuery || "")) { this.pmql = this.searchQuery; } else { this.filter = this.searchQuery; diff --git a/tests/unit/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcasterTest.php b/tests/unit/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcasterTest.php new file mode 100644 index 0000000000..89c6b9a2a5 --- /dev/null +++ b/tests/unit/ProcessMaker/Multitenancy/Broadcasting/TenantAwarePusherBroadcasterTest.php @@ -0,0 +1,135 @@ +create(); + $this->setCurrentTenantId(4); + + $pusher = Mockery::mock(Pusher::class); + $pusher->shouldReceive('authorizeChannel') + ->once() + ->with('private-tenant_4.ProcessMaker.Models.User.' . $user->id, '1.234') + ->andReturn(json_encode(['auth' => 'app-key:signature'])); + + $broadcaster = $this->broadcasterWithUserChannel($pusher); + + $response = $broadcaster->auth($this->authRequest( + 'private-tenant_4.ProcessMaker.Models.User.' . $user->id, + $user + )); + + $this->assertSame(['auth' => 'app-key:signature'], $response); + } + + public function test_auth_rejects_channel_for_a_different_tenant(): void + { + $user = User::factory()->create(); + $this->setCurrentTenantId(4); + + $broadcaster = $this->broadcasterWithUserChannel(Mockery::mock(Pusher::class)); + + $this->expectException(AccessDeniedHttpException::class); + + $broadcaster->auth($this->authRequest( + 'private-tenant_9.ProcessMaker.Models.User.' . $user->id, + $user + )); + } + + public function test_auth_rejects_unprefixed_channel_when_tenant_is_current(): void + { + $user = User::factory()->create(); + $this->setCurrentTenantId(4); + + $broadcaster = $this->broadcasterWithUserChannel(Mockery::mock(Pusher::class)); + + $this->expectException(AccessDeniedHttpException::class); + + $broadcaster->auth($this->authRequest( + 'private-ProcessMaker.Models.User.' . $user->id, + $user + )); + } + + public function test_format_channels_prefixes_private_and_presence_names(): void + { + $this->setCurrentTenantId(4); + $broadcaster = new TenantAwarePusherBroadcaster(Mockery::mock(Pusher::class)); + + $this->assertSame( + [ + 'private-tenant_4.ProcessMaker.Models.User.1', + 'presence-tenant_4.room', + 'tenant_4.open-channel', + ], + $this->formatChannels($broadcaster, [ + 'private-ProcessMaker.Models.User.1', + 'presence-room', + 'open-channel', + ]) + ); + } + + public function test_format_channels_does_not_double_prefix(): void + { + $this->setCurrentTenantId(4); + $broadcaster = new TenantAwarePusherBroadcaster(Mockery::mock(Pusher::class)); + + $this->assertSame( + ['private-tenant_4.ProcessMaker.Models.User.1'], + $this->formatChannels($broadcaster, ['private-tenant_4.ProcessMaker.Models.User.1']) + ); + } + + private function broadcasterWithUserChannel(Pusher $pusher): TenantAwarePusherBroadcaster + { + $broadcaster = new TenantAwarePusherBroadcaster($pusher); + $broadcaster->channel('ProcessMaker.Models.User.{id}', function ($user, $id) { + return (int) $user->id === (int) $id; + }); + + return $broadcaster; + } + + private function authRequest(string $channelName, User $user): Request + { + $request = Request::create('/broadcasting/auth', 'POST', [ + 'socket_id' => '1.234', + 'channel_name' => $channelName, + ]); + $request->setUserResolver(fn () => $user); + + return $request; + } + + private function setCurrentTenantId(int $id): void + { + app()->instance('currentTenant', (object) ['id' => $id]); + } + + /** + * @param array $channels + * @return array + */ + private function formatChannels(TenantAwarePusherBroadcaster $broadcaster, array $channels): array + { + $method = new ReflectionMethod($broadcaster, 'formatChannels'); + + return $method->invoke($broadcaster, $channels); + } +} diff --git a/tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php b/tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php index db229a0af4..22a33b5ed4 100644 --- a/tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php +++ b/tests/unit/ProcessMaker/Multitenancy/SwitchTenantTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\ProcessMaker\Multitenancy; +use Illuminate\Broadcasting\BroadcastManager; use Illuminate\Encryption\Encrypter; use Illuminate\Support\Facades\Crypt; use Laravel\Passport\ClientRepository; @@ -17,6 +18,28 @@ class SwitchTenantTest extends TestCase { + public function test_make_current_keeps_broadcast_manager_and_channel_callbacks(): void + { + $app = app(); + $manager = $app->make(BroadcastManager::class); + $manager->connection()->channel('ProcessMaker.Models.User.{id}', function ($user, $id) { + return (int) $user->id === (int) $id; + }); + + $switch = new SwitchTenant(); + + try { + $switch->makeCurrent($this->fakeTenant()); + + $this->assertSame($manager, $app->make(BroadcastManager::class)); + $this->assertTrue( + $app->make(BroadcastManager::class)->connection()->getChannels()->has('ProcessMaker.Models.User.{id}') + ); + } finally { + $switch->forgetCurrent(); + } + } + public function test_make_current_flushes_passport_singletons_and_auth_guards(): void { $app = app(); @@ -28,16 +51,7 @@ public function test_make_current_flushes_passport_singletons_and_auth_guards(): $auth->guard('web'); $previousEncrypter = $app->make('encrypter'); - $tenantKey = 'base64:' . base64_encode(Encrypter::generateKey(config('app.cipher'))); - - $tenant = new Tenant(); - $tenant->id = 999001; - $tenant->domain = 'tenant-999001.test'; - $tenant->database = config('database.connections.processmaker.database'); - $tenant->config = [ - 'app.url' => config('app.url'), - 'app.key' => Crypt::encryptString($tenantKey), - ]; + $tenant = $this->fakeTenant(); $switch = new SwitchTenant(); @@ -54,6 +68,22 @@ public function test_make_current_flushes_passport_singletons_and_auth_guards(): } } + private function fakeTenant(): Tenant + { + $tenantKey = 'base64:' . base64_encode(Encrypter::generateKey(config('app.cipher'))); + + $tenant = new Tenant(); + $tenant->id = 999001; + $tenant->domain = 'tenant-999001.test'; + $tenant->database = config('database.connections.processmaker.database'); + $tenant->config = [ + 'app.url' => config('app.url'), + 'app.key' => Crypt::encryptString($tenantKey), + ]; + + return $tenant; + } + private function containerInstances($app): array { $property = (new ReflectionObject($app))->getProperty('instances'); From fe5e57f864272509e0a1a7e27142667cf8207c12 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 22 Sep 2026 13:44:07 -0700 Subject: [PATCH 6/8] Update readme with ci changes --- README.md | 308 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 214 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 889bb63882..5592643040 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # ProcessMaker 4 Documentation # Overview @@ -7,113 +6,173 @@ ProcessMaker is an open source, workflow management software suite, which includ ## Getting Started -If you are new to ProcessMaker 4 and would like to load the software locally, we recommend you download the Dockerized version from https://github.com/ProcessMaker/pm4core-docker +If you are new to ProcessMaker 4 and would like to load the software locally, we recommend you download the Dockerized version from [https://github.com/ProcessMaker/pm4core-docker](https://github.com/ProcessMaker/pm4core-docker) ## System Requirements -* [Composer 2](https://getcomposer.org/) -* [Node.js 16.18.1](https://nodejs.org/en/) -* [NPM 8.9](https://www.npmjs.com/package/npm) -* [PHP 8.1](https://php.net) -* [PHP-FPM](https://www.php.net/manual/en/install.fpm.php) -* [PHP GD Extension](https://www.php.net/manual/en/image.installation.php) -* [PHP ImageMagick Extension](https://www.php.net/manual/en/book.imagick.php) -* [PHP IMAP Extension](https://www.php.net/manual/en/imap.setup.php) -* [Nginx](https://nginx.org/) -* [MySql 8.0](https://dev.mysql.com/downloads/mysql/8.0.html) -* [Redis](https://redis.io/) -* [Docker](https://docs.docker.com/get-docker/) +- [Composer 2](https://getcomposer.org/) +- [Node.js 16.18.1](https://nodejs.org/en/) +- [NPM 8.9](https://www.npmjs.com/package/npm) +- [PHP 8.1](https://php.net) +- [PHP-FPM](https://www.php.net/manual/en/install.fpm.php) +- [PHP GD Extension](https://www.php.net/manual/en/image.installation.php) +- [PHP ImageMagick Extension](https://www.php.net/manual/en/book.imagick.php) +- [PHP IMAP Extension](https://www.php.net/manual/en/imap.setup.php) +- [Nginx](https://nginx.org/) +- [MySql 8.0](https://dev.mysql.com/downloads/mysql/8.0.html) +- [Redis](https://redis.io/) +- [Docker](https://docs.docker.com/get-docker/) + ## Install Before installing, Nginx needs to be configured to use php-fpm and point to the public folder -1. Download and unzip a version from the releases page https://github.com/ProcessMaker/processmaker/releases -1. Configure Nginx to use php-fpm and point to the public folder in the unzipped code. See https://laravel.com/docs/8.x/deployment#nginx -1. CD into the folder and run `composer install` -1. Run the installer `php artisan processmaker:install` and follow the instructions -1. Edit the .env file to update any server specific settings -1. Install javascript assets `npm install` -1. Compile javascript assets `npm run dev` -1. Configure [laravel echo server](https://github.com/tlaverdure/Laravel-Echo-Server) in a separate shell `npx laravel-echo-server init` using the following settings: - 1. Do you want to run this server in development mode? **Yes** - 1. Which port would you like to serve from? **6001** - 1. Which database would you like to use to store presence channel members? **redis** - 1. Enter the host of your Laravel authentication server. *Enter your instance's url* - 1. Will you be serving on http or https? **http** - 1. Do you want to generate a client ID/Key for HTTP API? **No** - 1. Do you want to setup cross domain access to the API? **No** - 1. What do you want this config to be saved as? **laravel-echo-server.json** -1. Then run [laravel echo server](https://github.com/tlaverdure/Laravel-Echo-Server) `npx laravel-echo-server start` -1. Run horizon in a separate shell `php artisan horizon` -1. If you change any settings, make sure to run `php artisan optimize:clear` and restart horizon +1. Download and unzip a version from the releases page [https://github.com/ProcessMaker/processmaker/releases](https://github.com/ProcessMaker/processmaker/releases) +2. Configure Nginx to use php-fpm and point to the public folder in the unzipped code. See [https://laravel.com/docs/8.x/deployment#nginx](https://laravel.com/docs/8.x/deployment#nginx) +3. CD into the folder and run `composer install` +4. Run the installer `php artisan processmaker:install` and follow the instructions +5. Edit the .env file to update any server specific settings +6. Install javascript assets `npm install` +7. Compile javascript assets `npm run dev` +8. Configure [laravel echo server](https://github.com/tlaverdure/Laravel-Echo-Server) in a separate shell `npx laravel-echo-server init` using the following settings: + 1. Do you want to run this server in development mode? **Yes** + 2. Which port would you like to serve from? **6001** + 3. Which database would you like to use to store presence channel members? **redis** + 4. Enter the host of your Laravel authentication server. *Enter your instance's url* + 5. Will you be serving on http or https? **http** + 6. Do you want to generate a client ID/Key for HTTP API? **No** + 7. Do you want to setup cross domain access to the API? **No** + 8. What do you want this config to be saved as? **laravel-echo-server.json** +9. Then run [laravel echo server](https://github.com/tlaverdure/Laravel-Echo-Server) `npx laravel-echo-server start` +10. Run horizon in a separate shell `php artisan horizon` +11. If you change any settings, make sure to run `php artisan optimize:clear` and restart horizon + + ## Installing and upgrading an enterprise instance hosted on AWS -https://processmaker.atlassian.net/wiki/spaces/PM4/pages/480149598/Server+Deployment +[https://processmaker.atlassian.net/wiki/spaces/PM4/pages/480149598/Server+Deployment](https://processmaker.atlassian.net/wiki/spaces/PM4/pages/480149598/Server+Deployment) ## Using ProcessMaker 4 The online documentation for usage of ProcessMaker 4 can be found by clicking the link below. -https://docs.processmaker.com/ +[https://docs.processmaker.com/](https://docs.processmaker.com/) ## Testing + All PRs for PM4 and it's packages should be accompanied by a test. ## CI/CD +Put `ci:` tags anywhere in the pull request body. Separate tags with whitespace; one tag per line is the easiest. Editing the PR body re-runs CI. + ### Automated Tests -When ever you open or update a PR, the test suite is run with all packages installed. -If your PR requires branches in other packges or core, you can specify the branch anywhere in the PR body with this tag: +Opening or updating a PR builds an image with enterprise packages installed and runs the PHPUnit suite. Tests run whether or not you deploy an instance. + +### Package and Core Branches -`ci:< package name >:< branch name>` +Point CI at a branch of another package, or of core, with: -For example, if you open a PR in core that requires a bugfix branch in connector-send-email, put this in your core PR body text: +`ci::` + +For example, a core PR that needs a connector-send-email branch: `ci:connector-send-email:bugfix/FOUR-5059` -This works in package PRs as well. To specify a branch in core, use: +From a package PR, pin core the same way: `ci:processmaker:my-branch-in-core` -If no branches are specified in the PR body, the develop branch of each package will be used. +The repository that owns the PR always builds from that PR's head branch. + +When many packages share one branch name, set a wildcard. Each package that has the branch uses it. Packages that do not have it fall back to the release branch: + +`ci:*:epic/FOUR-12345` + +An explicit `ci::` tag wins over the wildcard. + +`@processmaker` JavaScript dependencies are built from the same branch rules. Name one to build a feature branch, for example `ci:modeler:my-branch`. Packages with their own bundle step are `modeler`, `screen-builder`, `vue-form-elements`, and `vue-multiselect`. `processmaker-bpmn-moddle` is linked into `modeler` when its branch is set. + +A `ci::` tag for a normal `processmaker/*` dependency that is not in the enterprise list still installs that package. -### Release Branches and Packages +Branch selection order: -If your PR is based on a release branch (for example `release-2024-fall <-- feature/123`), then all packages will be installed using the corresponding base branch (`release-2024-fall`). No need to add additional `ci:` tags. +1. `ci::` +2. `ci:*:`, when that branch exists on the package +3. The release branch (develop) -However, if your PR has an intermediate branch, for example (`epic/abc <-- feature/123`), where `epic/abc` is branched off the `release-2024-fall` branch, you will need to add `ci:release-branch:release-2024-fall` to your PR body so the CI builder knows what branch to use for packages. +### Package Source +PHP packages and script executors are installed from private Packagist as `dev-`. + +`ci:use-github-branches` clones those branches from GitHub instead. + +`ci:use-packagist-versions` keeps the versions already pinned in core `composer.json`. When both source tags are present, this one is used. ### CI Server -A full working instance can be built by adding the tag `ci:deploy` to your PR description. A link will be posted in the PR comments when it's ready. Note that this currently takes 10 to 30 minutes before the instance is ready. +`ci:deploy` builds a full instance and comments the URL on the PR when it is ready. The Helm install allows up to 75 minutes. + +The hostname comes from the repository and branch: + +- Multitenancy, the default: `https://tenant-1.ci-.engk8s.processmaker.net` +- `ci:single-tenant`: `https://ci-.engk8s.processmaker.net` + +The instance is removed when the PR is closed. `ci:redeploy` deletes the current instance, builds, and deploys a new one. CI then removes `ci:redeploy` from the PR body so later edits leave the new instance in place. The Harbor image is kept across a redeploy and deleted when the PR is closed. + +`ci:skip-redeploy` skips the Helm install or upgrade. The workflow still comments the instance URL. Use it when an instance already exists and should stay as it is. + +`ci:skip-build` skips the Docker image build and push. Later jobs use the image already stored for this PR. + +`ci:db:clean` wipes and re-seeds the database every time the instance starts. Remove the tag when data should persist. Leaving it in the body wipes the database on later updates and pod restarts. + +`ci:no-octane` serves the instance with PHP-FPM. Octane (FrankenPHP) is the default. + +`ci:pmai_dev` points the instance at the dev PMAI service. + +`ci:api-test` runs the API test suite after a successful deploy and comments the results. It requires `ci:deploy` or `ci:redeploy`. With no extra tags, every suite runs. Limit the run with one or more of: + +- `ci:api-test-saved-searches-results` +- `ci:api-test-saved-searches-charts` +- `ci:api-test-tasks-closed` +- `ci:api-test-requests-show` +- `ci:api-test-cases-started` +- `ci:api-test-cases-participated` -The instance will stay active until the PR is merged. +`ci:api-test-iterations:` sets the iteration count. The default is 5. -You can wipe the database on the CI Server by adding the tag `ci:db:clean`. Remember to remove the tag from your PR description or the DB will be wiped clean every time the PR is updated. +`ci:run-testbench` runs testbench against the deployed instance after a successful deploy. It also requires `ci:deploy` or `ci:redeploy`. ### Environment Variables -You can add or overwrite environment variables on the deployed server using this syntax in your PR body + +Add or overwrite environment variables on the deployed server from the PR body: + ``` ci:MY_ENVIRONMENT_VARIABLE=value ``` -Or with double quotes if the value has spaces + +Use double quotes when the value has spaces: + ``` ci:MY_ENVIRONMENT_VARIABLE="custom value" ``` + + ### Specify the K8S Distribution Branch -The CI Builder uses the `pm4-k8s-distribution` repository for building and deploying your PR branch in a CI server. -By default, the branch of `pm4-k8s-distribution` used for the build will be the same as the release branch (see "Release Branches and Packages" above). +The CI builder uses `pm4-k8s-distribution` to build and deploy the PR. That checkout uses the release branch described above. + +To test a different k8s distribution branch: -If you are testing updates to `pm4-k8s-distribution`, you can specify a branch in your PR body with `ci:k8s-branch:some-other-branch` +`ci:k8s-branch:some-other-branch` ### PHPUnit Tests + We use PHPUnit for both integration and unit testing. Most of our PHPUnit tests are integration tests that use the framework and database. Run the entire testsuite with `phpunit` @@ -121,12 +180,15 @@ Run the entire testsuite with `phpunit` If phpunit is not in your $PATH, you can use `vendor/bin/phpunit ...` To run the entire suite faster using parallel tests, run + ``` PARALLEL_TEST_PROCESSES=6 vendor/bin/paratest -p 6 ``` + - The environment variable and the -p argument must be the same number of parallel processes. To run an individual test, run + ``` phpunit tests/path/to/testTest.php ``` @@ -136,6 +198,7 @@ skip populating the database with `POPULATE_DATABASE=0 phpunit ...` to run tests - All test file names must end in Test.php Package tests should be saved in the package repository but must be run from processmaker core: + ``` phpunit vendor/processmaker/package-name/tests/... ``` @@ -145,37 +208,39 @@ Then, modify the code until the test passes* ## Development + + #### System Requirements You can develop ProcessMaker as well as ProcessMaker packages locally. In order to do so, you must have the following: -* [Virtualbox 5.2](https://www.virtualbox.org/) or above -* [Vagrant 2.2.0](https://www.vagrantup.com/) or above -* [PHP 8.1](https://php.net) or above - * Windows users can install [XAMPP](https://www.apachefriends.org/index.html) -* [Composer 2](https://getcomposer.org/) -* [Node.js 16.18.1](https://nodejs.org/en/) or above +- [Virtualbox 5.2](https://www.virtualbox.org/) or above +- [Vagrant 2.2.0](https://www.vagrantup.com/) or above +- [PHP 8.1](https://php.net) or above + - Windows users can install [XAMPP](https://www.apachefriends.org/index.html) +- [Composer 2](https://getcomposer.org/) +- [Node.js 16.18.1](https://nodejs.org/en/) or above **Steps for Development Installation** -* Clone the repository into a directory -* Perform `composer install` to install required libraries. If you are on windows, you may need to run `composer install --ignore-platform-reqs` due to Horizon requiring the pcntl extension. You can safely ignore this as the application runs in the virtual machine which has the appropriate extensions installed. -* Perform `npm install` in the project directory -* Perform `npm run dev` to build the front-end assets -* Modify your local `/etc/hosts` add `192.168.10.10 processmaker.local.processmaker.com`. On Windows, this file is located at `C:\Windows\System32\Drivers\etc\hosts`. - * If you need to change the ip address to something else to avoid conflicts on your network, modify the `Homestead.yaml` file accordingly. Do not commit this change to the repository. -* Execute `vagrant up` in the project directory to bring up the laravel homestead virtual machine -* Execute `vagrant ssh` to ssh into the newly created virtual machine -* Execute `php artisan processmaker:install` in `/home/vagrant/processmaker` to start the ProcessMaker Installation - * Specify `localhost` as your local database server - * Specify `3306` as your local database port - * Specify `processmaker` as your local database name - * Specify `homestead` as your local database username - * Specify `secret` as your local database password - * Specify `https://processmaker.local.processmaker.com` as your application url -* Check your .env file to ensure the `PROCESSMAKER_SCRIPTS_DOCKER` variable has the right Docker installation path, especially if you are under macOS (Docker on macOS installs under /usr/local/bin/docker). -* Visit `https://processmaker.local.processmaker.com` in your browser to access the application - * Login with the username of `admin` and password of `admin` +- Clone the repository into a directory +- Perform `composer install` to install required libraries. If you are on windows, you may need to run `composer install --ignore-platform-reqs` due to Horizon requiring the pcntl extension. You can safely ignore this as the application runs in the virtual machine which has the appropriate extensions installed. +- Perform `npm install` in the project directory +- Perform `npm run dev` to build the front-end assets +- Modify your local `/etc/hosts` add `192.168.10.10 processmaker.local.processmaker.com`. On Windows, this file is located at `C:\Windows\System32\Drivers\etc\hosts`. + - If you need to change the ip address to something else to avoid conflicts on your network, modify the `Homestead.yaml` file accordingly. Do not commit this change to the repository. +- Execute `vagrant up` in the project directory to bring up the laravel homestead virtual machine +- Execute `vagrant ssh` to ssh into the newly created virtual machine +- Execute `php artisan processmaker:install` in `/home/vagrant/processmaker` to start the ProcessMaker Installation + - Specify `localhost` as your local database server + - Specify `3306` as your local database port + - Specify `processmaker` as your local database name + - Specify `homestead` as your local database username + - Specify `secret` as your local database password + - Specify `https://processmaker.local.processmaker.com` as your application url +- Check your .env file to ensure the `PROCESSMAKER_SCRIPTS_DOCKER` variable has the right Docker installation path, especially if you are under macOS (Docker on macOS installs under /usr/local/bin/docker). +- Visit `https://processmaker.local.processmaker.com` in your browser to access the application + - Login with the username of `admin` and password of `admin` When developing, make sure to turn on debugging in your `.env` so you can see the actual error instead of the Whoops page. @@ -196,7 +261,9 @@ For macOS: If you choose not to install the certificate, you should access the socket.io js file in your browser to allow unsafe connections from it. Otherwise, real-time notifications may not work in your development environment. -* [https://processmaker.local.processmaker.com:6001/socket.io/socket.io.js](https://processmaker.local.processmaker.com:6001/socket.io/socket.io.js) +- [https://processmaker.local.processmaker.com:6001/socket.io/socket.io.js](https://processmaker.local.processmaker.com:6001/socket.io/socket.io.js) + + #### Customize Logos @@ -211,16 +278,21 @@ LOGIN_LOGO_PATH={{LOGIN PAGE LOGO PATH HERE}} 1. Run npm run dev + + #### Scheduled tasks/events To run time based BPMN events like Timer Start Events or Intermediate Timer Events, the laravel scheduler should be enabled. To do this open a console and: + 1. Execute crontab -e -2. Add to the cron tab the following line \(replacing the upper cased text with the directory where your proyecto is located \): +2. Add to the cron tab the following line replacing the upper cased text with the directory where your proyecto is located : ```text * * * * * cd YOUR_BPM_PROJECT && php artisan schedule:run >> /dev/null 2>&1 ``` + + #### API The ProcessMaker API is documented using OpenAPI 3.0 documentation and can be viewed at `/api/documentation`. The documention is generated by adding annotations to Models and Controllers. @@ -336,20 +408,25 @@ And for a show method ... ``` + + #### NAYRA Please add/change the next configuration in .env file to define the message broker driver that is used for Nayra # Message broker driver, possible values: rabbitmq, kafka, this is optional, if not exists or is empty, the Nayra will be work as normally with local execution + MESSAGE_BROKER_DRIVER=rabbitmq # Rabbit MQ connection, only when you use RabbitMQ + RABBITMQ_HOST=127.0.0.1 RABBITMQ_PORT=30672 RABBITMQ_LOGIN=guest RABBITMQ_PASSWORD=guest # Kafka connection, only when you use Kafka + KAFKA_BROKERS=127.0.0.1:30092 **Notes** @@ -378,7 +455,7 @@ Full OpenAPI 3.0 specification at [https://github.com/OAI/OpenAPI-Specification/ **Testing with Laravel Dusk** -When testing in [Laravel Dusk](https://laravel.com/docs/6.x/dusk), make sure to turn off debugging mode in your `.env` so you can use the whole page and screens executing functional tests. Then, change app\_env value to `develop` in the same file: +When testing in [Laravel Dusk](https://laravel.com/docs/6.x/dusk), make sure to turn off debugging mode in your `.env` so you can use the whole page and screens executing functional tests. Then, change appenv value to `develop` in the same file: ```text APP_DEBUG=FALSE @@ -399,74 +476,100 @@ To interact with web elements [https://laravel.com/docs/6.x/dusk#interacting-wit List of available assertions [https://laravel.com/docs/6.x/dusk#available-assertions](https://laravel.com/docs/6.x/dusk#available-assertions) - # ICONS Please follow the steps: + 1. Execute the command in root processmaker + ```text npm install ``` -2. Add the new svg icon file in the /processmaker/resources/devhub/pm-font/svg + +1. Add the new svg icon file in the /processmaker/resources/devhub/pm-font/svg + ```text /processmaker/resources/devhub/pm-font/svg/my-new-icon.svg ``` + 3.Run the follow command + ```text npm run font ``` + 4.Run the follow command + ```text npm run dev ``` + 5.To use your new icon, in any template or component, add the icon as follows: + ```text ``` + + ### RECOMMENDATIONS ABOUT ICONS -1. We recommend using the file name with '-' for example: + +1. We recommend using the file name with '-' for example: + ```text "left-arrow.svg" ``` -2. To use the icon, we should use the same name of the file, for example: + +1. To use the icon, we should use the same name of the file, for example: + ```text File name: "my-jonas-custom-icon.svg" How to use icon: ``` -3. To check all the icons + +1. To check all the icons + ```text npm run dev-font ``` + + # Case Retention Tier (CASE_RETENTION_TIER) The case retention policy controls how long cases are stored before they are automatically and permanently deleted. The **CASE_RETENTION_TIER** environment variable determines which retention periods customers can select when configuring a process. Each tier exposes a different set of options in the UI; options for higher tiers are visible but disabled so users see what is available at higher tiers. ### Supported tiers -| Tier | Retention options available | -|------|----------------------------| -| **1** | Six months, One year | -| **2** | Six months, One year, Three years | + +| Tier | Retention options available | +| ----- | --------------------------------------------- | +| **1** | Six months, One year | +| **2** | Six months, One year, Three years | | **3** | Six months, One year, Three years, Five years | + Set the variable in your `.env` file: + ```env CASE_RETENTION_POLICY_ENABLED=true CASE_RETENTION_TIER=1 ``` -Use `1`, `2`, or `3`. The default is `1` if not set. The default retention period shown in the UI for Tier 1 is one year. - +Use `1`, `2`, or `3`. The default is `1` if not set. The default retention period shown in the UI for Tier 1 is one year. # Prometheus and Grafana This guide explains how to install and run **Prometheus** and **Grafana** using Docker. Both tools complement each other: Prometheus collects and monitors metrics, while Grafana visualizes them with interactive dashboards. ## Local Development with docker compose + + + ### Prometheus & Grafana + Go to the metrics directory + ```text cd metrics ``` @@ -477,9 +580,9 @@ Edit `prometheus.yml` and update the target hostname with your local processmake Run `docker compose up -d` -Check that prometheus can connect to your local instance at http://localhost:9090/targets +Check that prometheus can connect to your local instance at [http://localhost:9090/targets](http://localhost:9090/targets) -Go to Grafana at http://localhost:3000/ +Go to Grafana at [http://localhost:3000/](http://localhost:3000/) When you are finished, run `docker compose down`. To delete all data, run `docker compose down -v` @@ -488,6 +591,7 @@ When you are finished, run `docker compose down`. To delete all data, run `docke Now you can use the `Metrics` Facade anywhere in your application to manage metrics. ### **1. Counter** + A **Counter** only **increases** over time or resets to zero. It is used for cumulative events. - Total number of HTTP requests: @@ -498,7 +602,10 @@ A **Counter** only **increases** over time or resets to zero. It is used for cum ``` - Number of system errors (e.g., HTTP 5xx). + + ### **2. Gauge** + A **Gauge** can **increase or decrease**. It is used for values that fluctuate over time. - Current number of active jobs in a queue: @@ -508,7 +615,10 @@ A **Gauge** can **increase or decrease**. It is used for values that fluctuate o ``` - Memory or CPU usage. + + ### **3. Histogram** + A **Histogram** measures **value distributions** by organizing them into buckets. It is ideal for latency or size measurements. - Duration of HTTP requests: @@ -558,19 +668,26 @@ ProcessMaker can now be set up as a multitenant application. - Create an empty datbase named `landlord`. Your `DB_USERNAME` should have permission to write to this table. + + ## Transition your dev instnace to multitenancy Run the following command to enable multitenancy + ``` php artisan tenants:enable --migrate ``` + This command will + - Setup the landlord database. Make sure you create the empty landlord database first. - Set your existing database as the tenant database - Copy your existing `storage` folder to `storage/tenant_1` - Copy your existing `resources/lang` folder to `resources/lang/tenant_1` - Enable multitenancy in your .env + + ## Using `valet share` for the script microservice In the landlord tenant's table you will need to set the domain to the ngroc domain (without https://) and, in the config, column @@ -585,11 +702,13 @@ valet link another-tenant.test ``` Run the following command to create another tenant: + ``` php artisan tenants:create --domain="another-tenant.test" --name="Another Tenant" --database="another_tenant" ``` This command will + - Create the required folder structure - Create the tenant database @@ -608,6 +727,7 @@ copy the `.env` file into the transitions folder and add the instnace name to th For example `.env.my-instance` Run the following command to migrate the instance(s) to a tenant: + ``` php artisan tenants:transition ``` @@ -620,6 +740,6 @@ You must move the tenants storage folder and the resources/lang folder manually Distributed under the [AGPL Version 3](https://www.gnu.org/licenses/agpl-3.0.en.html) -ProcessMaker \(C\) 2002 - 2020 ProcessMaker Inc. +ProcessMaker C 2002 - 2020 ProcessMaker Inc. -For further information visit: [http://www.processmaker.com/](http://www.processmaker.com/) +For further information visit: [http://www.processmaker.com/](http://www.processmaker.com/) \ No newline at end of file From 5866da9369f77ac97fd7fa875f24aa8bbb393f57 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 22 Sep 2026 13:51:31 -0700 Subject: [PATCH 7/8] Update workflow reference to use 'main' branch --- .github/workflows/deploy-pm4.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-pm4.yml b/.github/workflows/deploy-pm4.yml index 176a237412..4b05f4736b 100644 --- a/.github/workflows/deploy-pm4.yml +++ b/.github/workflows/deploy-pm4.yml @@ -15,5 +15,5 @@ concurrency: jobs: run: name: Run PM4-workflow - uses: processmaker/.github/.github/workflows/deploy-pm4.yml@enable-octane-cicd + uses: processmaker/.github/.github/workflows/deploy-pm4.yml@main secrets: inherit From f18d35df88964fe1bfa9dbff5007521d9aa3d557 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 22 Sep 2026 14:02:17 -0700 Subject: [PATCH 8/8] Refactor PMQL check in SettingsListing component --- resources/js/admin/settings/components/SettingsListing.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/admin/settings/components/SettingsListing.vue b/resources/js/admin/settings/components/SettingsListing.vue index 1fbaeb1f57..fd2ae5c596 100644 --- a/resources/js/admin/settings/components/SettingsListing.vue +++ b/resources/js/admin/settings/components/SettingsListing.vue @@ -374,7 +374,7 @@ export default { dataProvider(context, callback) { this.filter = ''; this.pmql = ''; - if (isPMQL.call(this.searchQuery || "")) { + if (this.searchQuery.isPMQL()) { this.pmql = this.searchQuery; } else { this.filter = this.searchQuery;