Deployments — Improvement & Growth Plan
Package: capell-app/deployments · Kind: package · Tier: premium · Product group: Capell Operations · Bundle: operations · Status: Draft
1. Snapshot
Section titled “1. Snapshot”Deployments stores per-repository Git provider connections (GitHub / GitLab / Bitbucket) and publishes Composer requirement changes back to the connected repository as either a direct commit or a pull request (optionally auto-merged). It exposes two admin surfaces (DeploymentConnectionPage, DeploymentConnectionWidget), three OAuth callback routes (routes/oauth.php), a single model/table (DeploymentConnection / deployment_connections), and the PublishesComposerChanges contract intended to be consumed by other packages’ install flows. Domain logic lives in five Actions (ConnectDeploymentAction, PublishComposerRequirementAction, PrepareComposerRequirementCommitAction, plus two OAuth-state Actions) delegating to three GitProviderContract implementations behind GitProviderFactory. Dependencies are light: capell-app/admin, capell-app/core, lorisleiva/laravel-actions, spatie/laravel-data, spatie/laravel-package-tools.
Current marketplace summary: “Connect a Git repository once, then install or update Capell extensions from admin workflows that publish composer.json changes as reviewed pull requests.” Manifest declares the committed marketplace card plus 2 functional screenshots under docs/screenshots/ (deployment-connection-page.png, -dark.png), and docs/screenshots.json now maps to those committed pre-connection OAuth entry-point captures.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”- Dashboard widget visibility is gated —
DeploymentConnectionWidget::canView()mirrorsDeploymentConnectionPage::canAccess(), unauthenticated users and non-permitted admins cannot resolve the active connection, and Livewire coverage asserts the repository coordinate is not rendered to unauthorized viewers —src/Filament/Widgets/DeploymentConnectionWidget.php,tests/Feature/Filament/DeploymentConnectionWidgetTest.php. (Shipped) - Dashboard widget is registered intentionally —
DeploymentsServiceProvider::registeringPackage()registersDeploymentConnectionWidgeton the System Health dashboard when the package is enabled, and provider registration coverage asserts the widget is present alongside the page —src/Providers/DeploymentsServiceProvider.php,tests/Feature/DeploymentsServiceProviderTest.php. (Shipped) - OAuth client misconfiguration is surfaced in the UI — provider connect buttons are disabled until both repository coordinates and the relevant provider client id are configured, with translated disabled reasons such as “GitLab OAuth is not configured.” The page no longer exposes a broken authorize URL for unconfigured providers, and coverage asserts the disabled states —
src/Filament/Pages/DeploymentConnectionPage.php,resources/views/filament/pages/deployment-connection.blade.php,tests/Feature/Filament/DeploymentConnectionPageTest.php. (Shipped) - Repository coordinates are captured before OAuth —
DeploymentConnectionPagenow requires repository owner/name input before building provider OAuth URLs, persists those coordinates in the one-time OAuth state, and every callback consumes that state before creating aDeploymentConnection. The old fictionalrepoName: 'app'callback placeholder is gone, and callback coverage asserts the stored GitHub/GitLab/Bitbucket owner/name values —src/Filament/Pages/DeploymentConnectionPage.php,src/Actions/OAuth/*,src/Http/Controllers/OAuth/*,tests/Feature/OAuth/OAuthControllersTest.php. (Shipped) - Centralise HTTP timeout — providers hard-code
->timeout(10)->connectTimeout(5)(e.g.GitHubProvider::client()line ~255) whileconfig('capell-deployments.http_timeout')exists and is honoured only by the OAuth controllers. Read the config value in the provider clients so the budget is configurable in one place —src/Services/GitProvider/GitHubProvider.php,GitLabProvider.php,BitbucketProvider.php. (S) - Simplify double
base64_decode—GitHubProvider::getFile()callsbase64_decode($rawContent, strict: true)twice inside a ternary; decode once into a variable and branch on it —src/Services/GitProvider/GitHubProvider.php. (S) - Bound publisher fails loudly when connection choice is ambiguous —
DeploymentsServiceProviderbindsPublishesComposerChangesto the single active deployment connection only when exactly one active connection exists. With multiple active connections it throws aLogicExceptiontelling the consuming workflow to callPublishComposerRequirementActionwith an explicitDeploymentConnection, and coverage locks that behavior —src/Providers/DeploymentsServiceProvider.php,tests/Feature/GitOps/PublishComposerRequirementActionTest.php. (Shipped) - Shipped 2026-06-07: OAuth expiry is persisted for expiring providers. GitLab and Bitbucket callbacks now pass
expires_inintoConnectDeploymentAction, which writestoken_expires_atalongside encrypted access/refresh tokens. This gives provider clients a real expiry boundary for refresh decisions. —src/Http/Controllers/OAuth/GitLabCallbackController.php,src/Http/Controllers/OAuth/BitbucketCallbackController.php,src/Actions/ConnectDeploymentAction.php
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”Manifest capabilities: ["deployments", "deployments-admin", "deployments-install-policy", "deployments-publish-history", "deployments-publish-idempotency", "deployments-token-refresh"]. Manifest surfaces: ["admin"].
- Console command workflow — there is no
src/Consoledirectory, no classextends Command, andcapell.json.commandsis{install:null,setup:null,demo:null,doctor:null}. The manifest no longer advertises the unbackedconsolesurface ordeployments-consolecapability; add them back only with real commands such asdeployments:publish {package} {constraint},deployments:status, ordeployments:doctor. (M) - Shipped 2026-06-07: OAuth token refresh is wired for GitLab and Bitbucket.
RefreshProviderTokenActionrefreshes expired GitLab/Bitbucket tokens with the stored encrypted refresh token, persists rotated access/refresh tokens and expiry, and provider clients invoke it before API calls. GitHub remains unchanged because its OAuth tokens are not modelled with refresh expiry in this package. —src/Actions/RefreshProviderTokenAction.php,src/Services/GitProvider/GitLabProvider.php,src/Services/GitProvider/BitbucketProvider.php - Shipped 2026-06-07: release / deploy tracking and version history. Composer requirement publishes now write
deployment_publicationsrows with package, constraint, branch, commit SHA, PR reference, dry-run flag, and status. Direct commits and PR branch commits callgetDeployStatus()when a commit SHA exists, and the Deployment Repository page shows a recent publishes/status table for each active connection. —database/migrations/2026_06_07_090000_02_create_deployment_publications_table.php,src/Actions/RecordDeploymentPublicationAction.php,src/Actions/RefreshDeploymentPublicationStatusAction.php,src/Actions/PublishComposerRequirementAction.php,src/Filament/Pages/DeploymentConnectionPage.php,resources/views/filament/pages/deployment-connection.blade.php - Shipped 2026-06-08: cancel pending pull-request installs.
CancelDeploymentPublicationActionconsumes providerclosePullRequest(), marks the publish recordcancelled, and the Deployment Repository page exposes the action for pending PR-based publishes only. Direct-commit reverts remain future depth. - Shipped 2026-06-08: health-gated auto-merge. Pull-request publishes now read
getDeployStatus()for the PR head commit before enabling auto-merge. Passing checks enable auto-merge, while pending/failing checks leave the PR open and record the current status for the recent-publishes table. - Maintenance-mode / deploy-hook coordination — for a CMS “Operations” bundle package, there are no deploy hooks (pre/post-publish callbacks), no maintenance-mode toggling around a direct-commit deploy, and no event emission (
subscriberManager/AdminEventRegistry) when a publish succeeds or fails so other packages (Diagnostics, Insights) can react. (M) - Changelog generation for published changes — the PR body is a hard-coded one-liner (
PublishComposerRequirementAction); generating a richer changelog/diff summary (what changed incomposer.json, links to the extension) is a differentiator over a raw API commit. (S) - Shipped 2026-06-06: per-connection install policy is selectable before OAuth.
DeploymentConnectionPagenow exposes the threeInstallPolicyoptions beside repository owner/name, persists the selected policy in OAuth state, and the callback passes it toConnectDeploymentAction. Existing behavior remains the default (PullRequestAutoMerge), whileDirectCommitandPullRequestManualare now reachable from admin connection setup. —src/Filament/Pages/DeploymentConnectionPage.php,src/Actions/OAuth/*,src/Actions/ConnectDeploymentAction.php - Webhook ingestion for deploy/PR status — polling
getDeployStatus()is the only model; a provider webhook receiver to update publish status asynchronously would round out the “deployments” story. (L)
4. Issues / Risks
Section titled “4. Issues / Risks”- Composer publishing owner is explicitly external —
grepacross the package repo still shows no production consumer outside Deployments, so Marketplace/extension-install workflows remain the external owner for invokingPublishesComposerChanges. The package now documents that boundary inREADME.mdanddocs/overview.md: simple install flows resolve the contract through the container, while multi-repository workflows must select aDeploymentConnectionand callPublishComposerRequirementActionexplicitly. The remaining commercial risk is integration timing in the external Marketplace/install package, not an unowned in-package contract —src/Contracts/PublishesComposerChanges.php,src/Actions/PublishComposerRequirementAction.php,src/Providers/DeploymentsServiceProvider.php. - Dead contract surface is reduced —
getDeployStatus()is now consumed by Composer publish history/status recording, butgetPullRequest()andclosePullRequest()remain declared onGitProviderContract, implemented in all three providers, and unit-tested with no production caller. Build rollback/cancel-pending-install to use them or trim the unused surface —src/Contracts/GitProviderContract.php,src/Actions/PublishComposerRequirementAction.php,src/Actions/RefreshDeploymentPublicationStatusAction.php. - Shipped 2026-06-03: health check is real.
DeploymentsHealthCheck::runDiagnostics()now verifies the connection storage table and at least one configured Git provider OAuth client id, reports only provider labels, and exposes aggregate pass/fail state for Diagnostics. Token freshness remains a separate OAuth-refresh feature rather than part of the current health check. - Manifest settings gap —
database.settings:falseandsettings:[]are correct (no settings), but the package reads severalCAPELL_*env vars (config/capell-deployments.php) that aren’t surfaced as configurable settings anywhere —capell.json. - Multiple-active-connection ambiguity is reduced but not fully modelled — the bound publisher now refuses to choose among multiple active connections and tells callers to pass an explicit
DeploymentConnection; the dashboard widget still displays the first active connection as a summary affordance. A richer multi-connection admin model remains useful, but Composer publishing no longer silently targets an arbitrary repository —src/Providers/DeploymentsServiceProvider.php,src/Filament/Widgets/DeploymentConnectionWidget.php. - Shipped 2026-06-06: publish idempotency and direct-commit dry-run.
PublishComposerRequirementActionnow uses deterministiccapell/add-extension-<slug>branches for pull-request policies, asks the provider for an existing open PR on that branch before writing, and returns the existing PR result instead of creating duplicate branches/PRs. Direct-commit publishes acceptdryRun: true, prepare the composer patch, and return a dry-run result without committing to the default branch. —src/Actions/PublishComposerRequirementAction.php,src/Contracts/GitProviderContract.php,src/Services/GitProvider/*Provider.php - Secret handling — mostly good, two notes — tokens are excluded from
$fillableand stored viaEncryptedStringcast withforceFill(well-documented in the model andConnectDeploymentAction); OAuth token responses are redacted before logging (redactTokenResponse). However: (1) the cast is named*_encryptedbut the model decrypts on access, so$connection->access_token_encryptedreturns plaintext and is passed straight into->withToken(...)— the column name is misleading and invites accidental logging of the attribute; (2) there is no encryption-key-rotation/re-encrypt path. —src/Casts/EncryptedString.php,src/Models/DeploymentConnection.php,src/Services/GitProvider/GitHubProvider.php. - Test gaps — coverage is good for Data/enums/casts/OAuth-state/providers (
BitbucketProviderTest,GitLabProviderTest,GitHubProviderTestfaked-HTTP), OAuth callback repository persistence, page access/mutation gates, OAuth-config disabled states, widget authorization/rendering, and the bound publisher’s multi-connection failure. Remaining gaps: no idempotency test for double-publish, no token-refresh coverage, no publish-history/status consumer coverage, and no active-connection/OAuth-to-PR screenshot capture. —tests/Feature/Filament/DeploymentConnectionWidgetTest.php,tests/Feature/Filament/DeploymentConnectionPageTest.php,tests/Feature/GitOps/PublishComposerRequirementActionTest.php. - Performance budget —
capell.jsonsetsadminQueryBudget: 40andfrontendRenderBudgetMs: 0(correct — no frontend surface). The page issuesDeploymentConnection::where('is_active',true)->get()up to three times per render (getConnections()called in@foreachand again incount(...)in the blade), plusSchema::hasTableeach call. MemoisegetConnections()to stay well within budget —resources/views/filament/pages/deployment-connection.blade.php,src/Filament/Pages/DeploymentConnectionPage.php. - i18n — generally good: page, widget, OAuth errors, and enum labels all use
__('capell-deployments::...'). The hard-coded PR title/body inPublishComposerRequirementAction(“Add extension …”, “Auto-generated by Capell…”) and the commit message are not translatable — acceptable for Git artefacts, but worth a note if localisation of repo output is desired.
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Current summary: “Connect a Git repository once, then install or update Capell extensions from admin workflows that publish composer.json changes as reviewed pull requests.” This now leads with the admin outcome instead of package plumbing, and the composer.json description uses the same buyer-facing framing.
Improved 1-sentence summary:
Connect a Git repository once, then install or update Capell extensions from admin workflows that publish
composer.jsonchanges as reviewed pull requests.
Improved 3–4 sentence description:
Deployments lets site operators connect GitHub, GitLab, or Bitbucket repositories from the Capell admin and publish Composer requirement changes without server shell access. OAuth-backed connections store encrypted tokens, remember the repository owner/name, and let extension install flows open pull requests, auto-merge them, or make direct commits according to policy. It is the Operations bundle bridge between Marketplace installs and reviewed repository changes.
Screenshot / media gaps: Manifest advertises the committed marketplace card plus 2 real screenshots (light + dark), and docs/screenshots.json maps to those captures. The two functional captures show the pre-connection OAuth entry point; there is still no screenshot of an active connection and no GIF/flow of the OAuth-to-PR journey. Do not claim active-connection or full OAuth-flow media coverage until those assets are committed.
Pricing / tier / bundle positioning: tier: premium, bundle: operations. Premium is defensible only if the differentiators ship — today the live feature set (store a token, render a connection, open a PR with no history/rollback/health gate) is closer to a mid-tier utility. Strengthen with deploy history + rollback + health-gated auto-merge (Section 3) to earn “premium.” It pairs naturally inside the Operations bundle alongside Diagnostics (publish health surfaced as a real check) and Migration Assistant (both are in this package’s docs/Read Next).
Cross-sell: Depends on capell-app/admin + capell-app/core only. Natural cross-sell to the (presumed) marketplace/extension-install package that should consume PublishesComposerChanges, and into the diagnostics and migration-assistant Extension Suites — e.g. Diagnostics reads a real DeploymentsHealthCheck, Migration Assistant triggers a publish when a migration requires a new package.
Differentiators / value props / target buyer: Differentiator = “install extensions via a reviewed PR, not SSH.” Value props = no-CLI installs, encrypted tokens, choice of direct-commit vs PR-auto-merge vs manual review, multi-provider. Target buyer = agency/operator running client Capell sites who wants to add paid extensions without giving every editor shell access.
Keywords/tags: deployments, composer, pull-request, github, gitlab, bitbucket, oauth, extension-install, git-integration, operations, auto-merge, repository-connection.
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
Gate DeploymentConnectionWidget with canView() (stop repo-identity leak) | Done | S | High | §2, §4 |
Register the widget or delete it + its screenshots.json entry | Done | S | Med | §2, §4 |
Resolve manifest mismatches: drop unbacked console surface/capability, fix screenshot count | Done | S | High | §4 |
Confirm/wire a real consumer of PublishesComposerChanges (or document the external owner) | Done | M | High | §4 |
Fix repo_name => 'app' placeholder; add repo selection to the page | Done | M | High | §2 |
Implement a real DeploymentsHealthCheck (connection storage + OAuth-config readiness) | Done | M | High | §4 |
Shipped 2026-06-07: OAuth token refresh (RefreshProviderTokenAction, persist expiry/refresh token) | Done | M | High | §3, §2 |
| Surface OAuth-misconfiguration in the page UI + test it | Done | S | Med | §2, §4 |
| Make bound publisher connection-aware; handle >1 active connection | Done | M | Med | §2, §4 |
Shipped 2026-06-06: Expose InstallPolicy choice in the UI (unlock PullRequestManual) | Done | S | Med | §3 |
Shipped 2026-06-07: Publish-history table + “recent publishes/status” panel (consume getDeployStatus) | Done | L | High | §3, §4 |
Shipped 2026-06-06: Idempotency: dedupe open PRs for the same package; dry-run for DirectCommit | Done | M | Med | §4 |
Shipped 2026-06-08: Cancel pending pull-request installs (consume closePullRequest) | Done | M | Med | §3, §4 |
| Shipped 2026-06-08: Health-gated auto-merge before enabling PR auto-merge | Done | M | High | §3 |
| Deploy hooks + success/failure events | Later | M | High | §3 |
| Provider webhook ingestion for async deploy/PR status | Later | L | Med | §3 |
| Capture active-connection + OAuth-to-PR media after real assets exist | Blocked | S | High | §5 |