Skip to content

Sync paid plugins to Satis automatically, remove them when they go free - #484

Merged
simonhamp merged 2 commits into
mainfrom
satis-paid-plugin-sync
Aug 25, 2026
Merged

Sync paid plugins to Satis automatically, remove them when they go free#484
simonhamp merged 2 commits into
mainfrom
satis-paid-plugin-sync

Conversation

@simonhamp

Copy link
Copy Markdown
Member

What's going wrong

Commit 7d0e59f ("Disable auto Satis ingestion, add manual sync controls") stripped the automatic SyncPluginReleases dispatch out of both the submission flow and Plugin::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.php

  • syncToSatis() / removeFromSatis() helpers. syncToSatis() no-ops for free plugins, so callers don't have to guard.
  • updated hook: when type changes, paid → queue a build, free → queue a removal and null out satis_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() and approve() queue a build for paid plugins. Ingesting from submission rather than approval is deliberate — PluginAccessController already grants admins access to pending paid plugins "for review", which only works if they're in Satis.
  • deleting now goes through the same job path instead of calling the service inline.

app/Jobs/RemovePluginFromSatis.php (new)

  • Queued, and takes the package name rather than the model so it still runs once the row is gone. Also keeps a 30s HTTP call out of the Filament save request.

app/Services/SatisService.php

  • buildForPlugin() stamps satis_synced_at on success. It didn't before, so satis:build / satis:build --plugin=x left 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.php

  • Convert to Paid no longer dispatches explicitly (the model event does it); Sync to Satis goes through syncToSatis().

Tests

test_approval_does_not_dispatch_sync_plugin_releases pinned 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 the satis_synced_at stamping.

Full suite: 1507 passed.

Not included

satis:build still 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

simonhamp and others added 2 commits August 25, 2026 09:39
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>
@simonhamp
simonhamp marked this pull request as ready for review August 25, 2026 09:20
@simonhamp
simonhamp merged commit 56c1060 into main Aug 25, 2026
3 checks passed
@simonhamp
simonhamp deleted the satis-paid-plugin-sync branch August 25, 2026 09:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant