Sync paid plugins to Satis automatically, remove them when they go free - #484
Merged
Conversation
Automatic Satis ingestion was switched off in 7d0e59f, leaving the manual "Sync to Satis" admin action as the only reliable route in. A newly submitted paid plugin already has its releases tagged on GitHub, so no webhook ever fires and it never reaches Satis until someone remembers to click the button. Enforce the invariant at the model instead: a paid plugin belongs in Satis, a free one does not. - Add syncToSatis()/removeFromSatis() to Plugin, and drive them from the updated hook when type changes, so the admin form, the Convert to Paid action and the developer's own draft edit are all covered. - Queue a build from submit() and approve(). Ingesting from submission is deliberate: PluginAccessController already grants admins access to pending paid plugins for review, which only works if they're in Satis. - Clear satis_synced_at when a plugin goes free, and remove the package. Composer gives a custom repository precedence over Packagist, so a stale entry keeps shadowing the public package metadata. - Add RemovePluginFromSatis, a queued job taking the package name so it can still run once the row is gone, and use it for deletion too. - Stamp satis_synced_at in buildForPlugin(), so a successful satis:build no longer leaves the admin table's Satis column showing not-synced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SATIS_API_KEY has no default in config/services.php, so it is null whenever the env var is unset — as it is in CI. SatisService typed both properties as non-nullable string, so the service could not be constructed at all and fatalled in its own constructor, which made the "Satis API not configured" guards in removePackage() and triggerBuild() unreachable. This was latent before: SatisService was only ever built in the deleting hook. Now that SyncPluginReleases and RemovePluginFromSatis resolve it via method injection, any free/paid transition tripped it under the sync queue. - Make $apiUrl/$apiKey nullable so the existing degradation path works. - Return [] from fetchReleases() when the GitHub response has no JSON body; a 200 with a non-JSON body otherwise fatals against the array return type. - Cover both with regression tests that null the config explicitly, rather than depending on whether the developer happens to have SATIS_* in .env. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's going wrong
Commit 7d0e59f ("Disable auto Satis ingestion, add manual sync controls") stripped the automatic
SyncPluginReleasesdispatch out of both the submission flow andPlugin::approve(), replacing it with the manual Sync to Satis button in Filament.That left GitHub webhooks as the only automatic route into Satis — and a newly submitted paid plugin already has its releases tagged, so no webhook ever fires. It sits out of Satis until an admin remembers to click the button.
Separately, nothing removed a plugin from Satis when it moved paid → free. Composer gives a custom repository precedence over Packagist, so a stale Satis entry keeps shadowing the public package metadata.
What changed
The invariant is now "a paid plugin belongs in Satis; a free one doesn't", enforced at the model so every route is covered.
app/Models/Plugin.phpsyncToSatis()/removeFromSatis()helpers.syncToSatis()no-ops for free plugins, so callers don't have to guard.updatedhook: whentypechanges, paid → queue a build, free → queue a removal and null outsatis_synced_at. This catches the admin form's type select, the Convert to Paid action, and the developer's own draft edit in the dashboard.submit()andapprove()queue a build for paid plugins. Ingesting from submission rather than approval is deliberate —PluginAccessControlleralready grants admins access to pending paid plugins "for review", which only works if they're in Satis.deletingnow goes through the same job path instead of calling the service inline.app/Jobs/RemovePluginFromSatis.php(new)app/Services/SatisService.phpbuildForPlugin()stampssatis_synced_aton success. It didn't before, sosatis:build/satis:build --plugin=xleft the admin table's "Satis" column reading not-synced even after a successful build — misleading if that column is what you're diagnosing from.EditPlugin.phpsyncToSatis().Tests
test_approval_does_not_dispatch_sync_plugin_releasespinned exactly the behaviour being reported as a bug, so it's been rewritten. Added coverage for submit/approve across both plugin types, both type transitions, the unchanged-type no-op, deletion, the new job, and thesatis_synced_atstamping.Full suite: 1507 passed.
Not included
satis:buildstill isn't in the scheduler. A nightly full rebuild would be a reasonable backstop for anything whose build failed while Satis was down, but it means cloning every paid repo daily — happy to add it if that trade is worth making.🤖 Generated with Claude Code