Skip to content

Deployments — Improvement & Growth Plan

Package: capell-app/deployments · Kind: package · Tier: premium · Product group: Capell Operations · Bundle: operations · Status: Draft

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.

  • Dashboard widget visibility is gatedDeploymentConnectionWidget::canView() mirrors DeploymentConnectionPage::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 intentionallyDeploymentsServiceProvider::registeringPackage() registers DeploymentConnectionWidget on 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 OAuthDeploymentConnectionPage now 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 a DeploymentConnection. The old fictional repoName: '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) while config('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_decodeGitHubProvider::getFile() calls base64_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 ambiguousDeploymentsServiceProvider binds PublishesComposerChanges to the single active deployment connection only when exactly one active connection exists. With multiple active connections it throws a LogicException telling the consuming workflow to call PublishComposerRequirementAction with an explicit DeploymentConnection, 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_in into ConnectDeploymentAction, which writes token_expires_at alongside 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

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/Console directory, no class extends Command, and capell.json.commands is {install:null,setup:null,demo:null,doctor:null}. The manifest no longer advertises the unbacked console surface or deployments-console capability; add them back only with real commands such as deployments:publish {package} {constraint}, deployments:status, or deployments:doctor. (M)
  • Shipped 2026-06-07: OAuth token refresh is wired for GitLab and Bitbucket. RefreshProviderTokenAction refreshes 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_publications rows with package, constraint, branch, commit SHA, PR reference, dry-run flag, and status. Direct commits and PR branch commits call getDeployStatus() 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. CancelDeploymentPublicationAction consumes provider closePullRequest(), marks the publish record cancelled, 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 in composer.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. DeploymentConnectionPage now exposes the three InstallPolicy options beside repository owner/name, persists the selected policy in OAuth state, and the callback passes it to ConnectDeploymentAction. Existing behavior remains the default (PullRequestAutoMerge), while DirectCommit and PullRequestManual are 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)
  • Composer publishing owner is explicitly externalgrep across the package repo still shows no production consumer outside Deployments, so Marketplace/extension-install workflows remain the external owner for invoking PublishesComposerChanges. The package now documents that boundary in README.md and docs/overview.md: simple install flows resolve the contract through the container, while multi-repository workflows must select a DeploymentConnection and call PublishComposerRequirementAction explicitly. 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 reducedgetDeployStatus() is now consumed by Composer publish history/status recording, but getPullRequest() and closePullRequest() remain declared on GitProviderContract, 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 gapdatabase.settings:false and settings:[] are correct (no settings), but the package reads several CAPELL_* 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. PublishComposerRequirementAction now uses deterministic capell/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 accept dryRun: 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 $fillable and stored via EncryptedString cast with forceFill (well-documented in the model and ConnectDeploymentAction); OAuth token responses are redacted before logging (redactTokenResponse). However: (1) the cast is named *_encrypted but the model decrypts on access, so $connection->access_token_encrypted returns 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, GitHubProviderTest faked-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 budgetcapell.json sets adminQueryBudget: 40 and frontendRenderBudgetMs: 0 (correct — no frontend surface). The page issues DeploymentConnection::where('is_active',true)->get() up to three times per render (getConnections() called in @foreach and again in count(...) in the blade), plus Schema::hasTable each call. Memoise getConnections() 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 in PublishComposerRequirementAction (“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.

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.json changes 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.

ItemBucketEffortImpactSection ref
Gate DeploymentConnectionWidget with canView() (stop repo-identity leak)DoneSHigh§2, §4
Register the widget or delete it + its screenshots.json entryDoneSMed§2, §4
Resolve manifest mismatches: drop unbacked console surface/capability, fix screenshot countDoneSHigh§4
Confirm/wire a real consumer of PublishesComposerChanges (or document the external owner)DoneMHigh§4
Fix repo_name => 'app' placeholder; add repo selection to the pageDoneMHigh§2
Implement a real DeploymentsHealthCheck (connection storage + OAuth-config readiness)DoneMHigh§4
Shipped 2026-06-07: OAuth token refresh (RefreshProviderTokenAction, persist expiry/refresh token)DoneMHigh§3, §2
Surface OAuth-misconfiguration in the page UI + test itDoneSMed§2, §4
Make bound publisher connection-aware; handle >1 active connectionDoneMMed§2, §4
Shipped 2026-06-06: Expose InstallPolicy choice in the UI (unlock PullRequestManual)DoneSMed§3
Shipped 2026-06-07: Publish-history table + “recent publishes/status” panel (consume getDeployStatus)DoneLHigh§3, §4
Shipped 2026-06-06: Idempotency: dedupe open PRs for the same package; dry-run for DirectCommitDoneMMed§4
Shipped 2026-06-08: Cancel pending pull-request installs (consume closePullRequest)DoneMMed§3, §4
Shipped 2026-06-08: Health-gated auto-merge before enabling PR auto-mergeDoneMHigh§3
Deploy hooks + success/failure eventsLaterMHigh§3
Provider webhook ingestion for async deploy/PR statusLaterLMed§3
Capture active-connection + OAuth-to-PR media after real assets existBlockedSHigh§5