Forge API v2 migration + GitHub-built static binaries - #2
Merged
Merged
Conversation
ngunyimacharia
force-pushed
the
feat/forge-api-v2-migration
branch
from
July 27, 2026 15:21
1a60477 to
7c46dad
Compare
app('git.version') shells out to git at boot; inside a PHAR the cwd is a phar:// path so proc_open fails and the packaged binary cannot start. Bake the tag in as a literal at build time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Box's requirements checker rejects the embedded PHP without iconv (required by symfony/polyfill-mbstring). Add it to the static-php-cli extension set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Member
Author
|
Release pipeline verified end-to-end via v1.0.0-beta.1 (tag → The beta run caught two bugs that would have shipped a non-booting binary — both fixed here:
Still outstanding (needs real credentials): a live-Forge |
…ation errors Forge API v2 validation requires www_redirect_type (the OpenAPI schema marked it optional, but the live API rejects createSite without it) — default to 'none' for preview subdomains. Also catch Laravel\Forge\Exceptions\ValidationException in both commands and print the field-level errors instead of the opaque 'The given data failed to pass validation.' Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ested error bag
Forge API v2 requires letsencrypt.key_type on createCertificate (default ecdsa). Also fix bailValidation: the SDK passes the full {message, errors} body, so flatten the nested errors bag recursively instead of interpolating arrays into strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anaged databases) Servers using an external database return 404 on database/schemas and database/users. safelyIterate() treats a NotFoundException on a list endpoint as empty so destroy still cleans up the remaining resources and deletes the site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olerate cert conflicts If createSite returns a 'domain has already been added' 422 (e.g. a prior partial/failed create that our site scan missed), look the site up and continue provisioning (setup-command, env, cert, deploy) instead of aborting and leaving it half-configured. Also tolerate a ValidationException on createCertificate (a reused site may already have one). Extract flattenValidationMessages into HandlesOutput. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rovisioning
Forge returns a 422 ('recently created, wait 60s') when updating the .env of a just-created site. Retry the read-modify-write (bounded by --timeout) until provisioning completes, for both the --edit-env and database env-injection paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enabling push_to_deploy in createSite makes Forge auto-deploy the instant the repo finishes cloning — before the deployment script and environment are configured — running Forge's default script and racing/blocking our own deployment. Create the site with push_to_deploy=false, run the explicit deployment (correct script + env), then enable push-to-deploy so future pushes still auto-deploy (skipped when --no-quick-deploy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ wedges) forge-previewer always runs a deployment immediately after creating the site, which installs dependencies. Asking Forge to also install them during creation is redundant and runs an uncontrollable parallel composer install that, on servers hitting composer's download race, wedges and blocks our deployment. The deployment (with its serialized composer + shared-env restore) is the single source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Site creation (repo clone + directory setup) is async. createDeployment was firing before the site directory existed, so the deploy script's first command (cd <site dir>) failed instantly (2s). Poll the site until SiteStatus leaves creating/installing before running setup/cert/deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o a concern Move safelyIterate, findSiteByName, and a 404-safe findPrimaryDomainId into App\Commands\Concerns\FindsSiteResources so deploy and destroy stop maintaining divergent copies of the same Forge lookup loops.
- Only tolerate certificate 422s that indicate the certificate already exists; any other validation error (unconfigured DNS provider for dns-01, invalid domain, rate limit) now fails the deploy instead of exiting 0 without HTTPS. - Bail with a clear error when waitForSiteInstalled hits its deadline while the site is still installing, instead of racing the install. - Narrow the install-wait catch from Throwable to NotFoundException and TimeoutException so permanent errors surface immediately. - Tolerate 404 when listing scheduled jobs and databases, matching the destroy command (servers without a managed database service 404 on those endpoints). - Drop the generic 'could not be updated' phrase from the provisioning retry match; it could retry permanent validation errors for the full timeout. Document the observed Forge messages the remaining phrases come from.
Forge API v2 has no field for DNS-provider credentials, so --route-53-key/--route-53-secret are accepted but never forwarded. Requiring them alongside --wildcard forced users to pass inert values and broke the documented wildcard workflow example. Remove the requirement and mark both options as deprecated in the help output.
…assification - Hoist enablePushToDeploy out of the --no-deploy gate: a site created with --no-deploy would otherwise never get push-to-deploy, because later runs find the existing site and never re-enter the create path. - Treat any settled SiteStatus as ready in waitForSiteInstalled. Only creating/installing mean the install is running; requiring a literal 'installed' made reused sites (deployed/never-deployed) spin until the deadline and fail. Throw on 'failed'; drop 'error', which is not a SiteStatus value. - Introduce ProvisioningFailedException and throw it from the deploy abort paths instead of bare RuntimeException. Guzzle transport exceptions extend RuntimeException, so the previous catch in handle() misclassified transient HTTP failures as clean abort conditions. - Catch the SDK TimeoutException in handle(): createDatabase waits internally for the schema to install and throws it on expiry, which previously crashed with a raw stack trace.
…ing aborts Log when the primary domain record cannot be resolved instead of silently skipping certificate deletion, and convert ProvisioningFailedException into a clean bail like the deploy command.
Forge can return a message-only 422 with a present-but-empty errors array; flattening only the bag discarded the sole human-readable detail.
Every other paginated lookup in the trait tolerates a 404 as an empty collection; make the site lookup consistent.
The tag is spliced into a sed script and a single-quoted PHP string; a tag containing | or & would corrupt config/app.php or abort the sed expression. Restrict tags to [A-Za-z0-9._-] before stamping.
- Abort with a clear message when the site status turns to removing or uninstalling mid-wait (a concurrent destroy) instead of treating a tearing-down site as ready. - Convert an unexpected mid-deploy 404 (site or server deleted concurrently, e.g. from enablePushToDeploy) into a clean bail instead of an uncaught exception. - Retry the deployment-script update while the site is still provisioning, matching the env-file writes that hit the same 'recently created' cooldown. - Sanitize isolated_user: Forge requires ^[a-z][-a-z0-9_]*$ (max 32), so prefix slugs that start with a digit and clamp the length — --isolate previously failed for branch names like 123-fix-thing.
Treat a 404 on any individual delete (scheduled job, database, database user, certificate, site) as already-removed so a retried or concurrent destroy completes cleanly, and bail with a clear message on any other unexpected 404 instead of crashing with a stack trace.
Push-to-deploy is enabled after the first deployment (and independently of --no-deploy); the certificate is requested during site creation.
…rrors - Run waitForSiteInstalled on the found-existing path too: a reused site can be mid-install (concurrent deploy) or mid-teardown (concurrent destroy), the same races already handled on the create path. - Gate push-to-deploy on the site's actual quickDeploy state instead of a created-this-run flag: a first run that bailed after site creation permanently lost push-to-deploy on retry. - Suffix a short hash when clamping isolated_user to 32 characters so long branch names that share a 32-char prefix don't collide on the same isolation user. - Bail cleanly on ForbiddenException, RateLimitExceededException, and FailedActionException instead of crashing with a stack trace.
Move the site lookup inside the guarded block so a 403 or transient failure while listing sites produces a formatted error, and add the same ForbiddenException/RateLimitExceededException/FailedActionException handling as the deploy command.
… boot
config/app.php resolved app('git.version') eagerly, which shells out to
git describe. Inside a PHAR the cwd is a phar:// path and proc_open
fails, so a PHAR built via the documented 'box compile' steps crashed
on every invocation before any command ran. Fall back to 'unreleased'
and document the stamping step for versioned manual builds; the release
workflow still bakes the real tag in before compiling.
A site can report status 'failed' from an earlier run (failed install or deployment). Re-provisioning it is the established recovery path, so only treat 'failed' as fatal for freshly created sites; reuse paths log and continue instead of blocking every retry.
luisdalmolin
approved these changes
Jul 30, 2026
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.
Summary
Migrates
forge-previewerfrom the deprecated Forge API v1 to API v2 usinglaravel/forge-sdk ^4.1, upgrades the application stack, and replaces the committed PHAR with self-contained release binaries built by GitHub Actions.Implements
docs/specs/2026-07-27-forge-api-v2-migration-design.md.Changes
Upstream sync
ryangjchandler/forge-previewer: commitcomposer.lockand removeInspireCommand.--timeoutoption andconfig('app.timeout')default.composer.lockgoing forward.Runtime and dependency upgrades
HandlesOutput::fail()tobail()to avoid a collision withIlluminate\Console\Command::fail().Forge API v2 migration
--orgoption orFORGE_ORGenvironment variable.dns-01for wildcard domains andhttp-01otherwise.php81tophp84.$GITHUB_OUTPUTinstead of using the deprecated::set-outputcommand.Forge API v2 does not accept DNS-provider credentials when creating certificates. The existing
--route-53-keyand--route-53-secretoptions remain for CLI compatibility, but credentials must now be configured in Forge for thedns-01challenge.CI and distribution
v*tag is pushed.builds/forge-previewerand use the source entry script for Composer installations.Verification
./forge-previewer list./forge-previewer deploy --help./forge-previewer destroy --helpcomposer validateThe static-binary release pipeline requires a real
v*tag, and a live deploy/destroy test requires Forge credentials. Both remain gated by their respective external prerequisites.