A useful Laravel CI/CD pipeline connects a particular code revision to evidence that it works, then preserves that connection through deployment. Passing application tests is one part. Web-server redirects, publication files, database compatibility and the release mechanism can fail independently.

This walkthrough uses the GitHub Actions workflow for this website, a Laravel 12 application, as reviewed on 27 September 2026. The implementation details and test results below come from that project. Its automated workflow performs continuous integration; it has no deployment job. The release sequence later in this article is our recommended design, not a claim that this repository already automates production delivery.

What the GitHub Actions workflow actually checks

The workflow runs for pushes and pull requests targeting master, main and develop. A PHP matrix tests 8.2 and 8.3; Node 20 builds the frontend. These describe the examined configuration, rather than version recommendations for a new project. A separate job exercises Apache. Each has a different boundary:

Evidence produced by the checked-in workflow
CheckWhat it establishesWhat remains outside it
Dependencies and assetsComposer installation, npm installation and the frontend build complete.Production runtime configuration and browser layout.
Laravel testsThe repository's application tests pass in its testing environment.Real external services and production database behavior.
Article selection and validationChanged publication candidates reach the content checker; failures stop the job.Editorial quality, factual accuracy and every older article.
Apache integrationThe actual rewrite rules produce expected headers and front-controller handoffs.Public DNS, TLS, CDN settings and Laravel's final response.

Configure required checks to match the jobs that matter before allowing merges. A workflow file alone does not establish that branch protection requires those jobs; that is a separate repository setting.

Build from locked dependencies in an isolated environment

The workflow uses composer install, npm ci and npm run build. Keeping lockfiles in the reviewed change makes dependency selection traceable. Composer's install documentation explains that installation uses the versions recorded in an existing lockfile. Updating dependencies belongs in a reviewed change, rather than becoming an incidental part of a release.

CI creates a disposable testing environment with SQLite, array-backed mail, session and cache, and synchronous queues. It generates a test application key, runs migrations and invokes composer test. That Composer script clears configuration before running Laravel's tests, reducing the chance of accidentally testing cached configuration from another environment.

These choices make a fast baseline, but synchronous jobs do not exercise worker lifecycles. SQLite also cannot establish how another database engine handles locking, query plans or schema changes. Where a change depends on those behaviors, add a test using the production database engine and a representative workload. Keep test credentials separate from production credentials, and never copy the disposable key-generation step into a production release procedure.

Test redirects in the server that executes them

A Laravel feature test can prove that the framework redirects an old URL. It does not execute Apache's .htaccess. If the web server first redirects HTTP to HTTPS at the old path, Laravel can still produce a correct second redirect while visitors encounter an unnecessary chain.

Our Apache integration test starts an isolated loopback server with the real rewrite file and a minimal front-controller fixture. For example, it expects /reactnative/ to point directly to https://nomadicsoft.io/technologies/reactnative, while preserving a supplied query string.

The fixture covers legacy aliases, locale prefixes, trailing slashes, forwarded-protocol variations and an encoded plus sign. It also checks that the final path reaches the front controller without another Apache redirect. Unknown paths must reach the application rather than being silently redirected to the homepage.

We reran this fixture while preparing the article: 285 checks passed. That count describes server-level assertions, not 285 browser journeys. Because PHP is replaced by a fixed response, separate Laravel tests still need to establish that the destination page exists and renders correctly. This separation makes a failure easier to locate.

Validate the selector as well as the content

This website stores articles as a pair: meta.yaml and content.blade.php. A checker can reject malformed content only if the workflow actually asks it to inspect that article.

Our changed-article selector compares Git revisions and deduplicates changed files into locale-and-slug pairs. The PHP job fetches full history. Its BLOG_CHECK_BASE value comes from the pull request's base SHA or the push event's previous SHA.

Three failure cases deserve explicit handling:

  • Unavailable comparison base: a missing commit or all-zero push base must not turn into “nothing changed.” The script falls back to an empty tree, so every retained article is checked.
  • Partial deletion: deleting only metadata still leaves a publication candidate. The script invokes the checker if either file remains. Removing both files is treated as removing the article.
  • Checker failure: a nonzero exit status must become a failed CI step, even if the output looks empty or harmless.

The selector's eight regression tests passed in our rerun. They create real temporary Git histories and use a recording stand-in for PHP. This proves which articles are selected and how failure statuses propagate; it does not run Laravel's content validator.

We therefore checked one boundary with the actual Artisan command as well. An isolated article with content but no metadata produced JSON [] and exit status 1. Adding valid metadata produced one result with issues: [] and status 0. The useful distinction is the process status: an empty JSON array was not a successful publication check.

Checking changed articles keeps an existing audit backlog from blocking unrelated improvements. It does not certify the entire archive. Keep a separate full-content audit, and expect the fallback above to expose older problems when the comparison base is unavailable.

A small GitHub Actions test-job excerpt

These standard build steps are an excerpt to place inside a job's steps list, not a complete workflow. They assume checkout has completed, PHP and Node match the project, and an isolated testing environment and test database are ready. In our project, composer test clears configuration and runs php artisan test; adapt it to your repository's script.

- name: Install locked PHP dependencies
  run: composer install --prefer-dist --no-interaction --no-progress

- name: Build frontend assets
  run: |
    npm ci
    npm run build

- name: Run application tests
  run: composer test

Put failures that must block delivery in separate steps with normal failure propagation. Avoid continue-on-error: true for required checks. Our content and Apache checks are project-specific additions; Laravel does not supply those commands automatically. Their useful pattern is to turn an observed failure into a check at the layer that controls it, then test the selection logic that decides when it runs.

Carry the tested commit into the release

For continuous delivery, make the release input an exact commit with successful required checks. Deploying “whatever is on master now” creates a race: another merge can move the branch between validation and deployment. Record the commit SHA with the release and either promote its verified build artifact or rebuild that exact revision with the same lockfiles and recorded toolchain.

Keep production credentials in a separate trusted deployment context. Our recommended workflow grants read-only repository access to ordinary test jobs and narrowly scoped permissions to release jobs. Review third-party actions and pin them to full commit SHAs; our examined workflow currently uses version tags, so this is a hardening recommendation rather than a description of its configuration. GitHub's secure-use reference covers these controls and the risks of executing untrusted pull-request code in a privileged workflow.

Serialize releases to the same environment. A slower, older run should not activate after a newer release. Keep the release log specific: commit, artifact or build, schema work, activation time and smoke-check result.

Use a release sequence with explicit stop points

The following is a design to adapt to your hosting setup. It is not a deployment script from the repository:

  1. Prepare a separate release: place the selected code, locked dependencies and built assets together. Attach the intended environment configuration and persistent storage without replacing the active release.
  2. Check schema compatibility: identify changes required before activation and confirm that the current application can continue using the database while they run. Stop if that assumption is false.
  3. Prepare runtime caches: generate the release's configuration, route and view caches using its intended environment. Laravel's deployment documentation explains these caches and why application code should read configuration rather than calling env() outside configuration files.
  4. Activate the prepared release: use the host's release switch, then restart or reload the long-running processes that need the new code. Laravel's queue deployment guidance describes graceful worker restarts and the process monitor needed to bring workers back.
  5. Check the live result: verify a representative page, asset, canonical redirect and essential application operation. Observe errors and job processing. A successful activation command is not the same as a working customer journey.

An atomic code switch shortens the activation step. It does not make a blocking migration, incompatible queued job or missing configuration harmless. Define which failures prevent activation and which post-activation failures trigger recovery.

Plan code recovery separately from database recovery

Returning to the previous release restores its files. It does not reverse schema changes, undo completed payments or restore data that a migration removed. Laravel's migration rollback command executes rollback operations for migrations; it is not a general recovery mechanism for application state.

For example, replace a heavily used column in stages: add the new structure, deploy code that can coexist with the old structure, backfill and verify data, then move reads. Remove the old structure only after the compatibility window has closed. The exact sequence depends on concurrent writes and the database engine, but the decision to preserve compatibility must precede deployment.

Before release, name the previous revision that remains usable, check its compatibility with the new schema and queued payloads, and decide whether failure calls for a code switch, a forward fix or data restoration. Test restoration separately. A backup that has never been restored is an assumption in the recovery plan.

For a Laravel project review, bring the workflow, one representative failed release and the current recovery procedure. Our Laravel development services cover application changes and the tests needed to make those changes maintainable. The most useful first improvement is usually a specific missing check tied to a failure your project can actually encounter.