# Deploying Laravel from CI with a pipeline token

Deploy a Laravel app from GitHub Actions or GitLab CI with a Brewless pipeline token in BREWLESS_API_TOKEN, after your tests pass.

## In short

- A pipeline cannot approve a sign-in in a browser, so it deploys with a token in BREWLESS_API_TOKEN instead.
- A Brewless pipeline token acts as the person who made it, in one organisation, with that person's rights, and stops working the moment it is revoked.
- The command line client deploys the commit the pipeline checked out, so what reaches production is exactly what is in the repository.
- Run your tests in an earlier job and let the deploy job depend on it, because Brewless builds and releases your code but does not test it.

Deploying from your laptop works until two people do it on the same afternoon, or until someone deploys a branch that never passed the tests. Most teams end up wanting the same thing: push to `main`, run the tests, and deploy only when they pass.

This post sets that up for a Laravel app with [Brewless](/), on GitHub Actions and on GitLab CI, with a pipeline token that can be revoked in one click.

## How do you deploy Laravel from CI with Brewless?

Make a pipeline token in the Brewless console, store it in your CI's secrets as `BREWLESS_API_TOKEN`, install the client with `composer global require brewless/cli`, and run `brewless deploy production --no-interaction`. With that variable set, the client needs no sign-in, deploys the commit that is checked out, and fails the pipeline when the release fails.

## Making a pipeline token

A pipeline cannot approve a sign-in in a browser, which is how `brewless login` works on your own machine. It uses a token instead:

1. In the console, open **Account** and go to **Pipeline tokens**.
2. Name the token after the pipeline that will hold it, such as `GitHub Actions shop`, and choose **Create token**.
3. Copy the token. It starts with `bl_` and is shown this once.

Brewless keeps only a keyed hash of the token, so it can check one but never show it again. A token can only be made in the console while you are signed in there; a token cannot make another one.

## What the token may do

| | |
| --- | --- |
| Acts as | You, with your role |
| Reaches | The one organisation you made it in |
| Expires | Never; it works until revoked |
| Stops working | When you revoke it, or when you leave the organisation |
| Shows up | In the audit trail, under your name |

A token with your rights can do everything you can, not only deploy. That is why the name matters: when the list on the **Account** page shows a token that was last used yesterday, you want to know which pipeline holds it.

> [!WARNING]
> Keep the token in your CI's secret store, never in the repository or in a workflow file. Anyone who holds it can deploy and change the organisation as you.

What the pipeline does not need is your application's secrets. The variables of the environment stay in Scaleway Secret Manager in your own project, and the release takes them from there. No `.env` file goes into CI; see [Laravel secrets without a .env file](/blog/laravel-secrets-without-env-file).

## What the client needs

The client needs PHP 8.3 or newer and `git`. Install it with Composer, or download the built `brewless` file that comes with every release of `brewless/cli` on GitHub. In a pipeline it does not look for newer versions of itself.

Three behaviours matter in CI:

- **Only committed code is deployed.** The client packs the commit that is checked out. Uncommitted files, build output from an earlier step and anything in `.gitignore` stay behind. The build runs inside your own Scaleway project.
- **`--no-interaction` asks nothing.** Domains in `brewless.yml` that the environment does not answer on yet are only added with `--domains`, because adding a domain can move its visitors.
- **The exit code follows the release.** The client waits until the release is live or has failed, and exits non-zero on a failure.

The organisation comes from `brewless.yml`, so the token has to be one of that organisation. When Brewless refuses it, the client says so and the pipeline fails.

## Run the tests first

Brewless builds your code and runs your release commands, such as the migrations. It does not run your test suite. Put the tests in a job of their own and make the deploy job depend on it, so a red build never reaches production.

Keep the two jobs apart for a second reason: the test job has your dev dependencies and a test `.env`; the deploy job needs neither, only PHP, `git` and the client.

## GitHub Actions

Store the token under **Settings**, **Secrets and variables**, **Actions** as `BREWLESS_API_TOKEN`.

```yaml .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-production

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer install --no-interaction --prefer-dist
      - run: cp .env.example .env && php artisan key:generate
      - run: php artisan test

  deploy:
    needs: tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer global require brewless/cli
      - run: echo "$(composer global config bin-dir --absolute --quiet)" >> "$GITHUB_PATH"
      - run: brewless deploy production --no-interaction
        env:
          BREWLESS_API_TOKEN: ${{ secrets.BREWLESS_API_TOKEN }}
```

`needs: tests` keeps the deploy from starting until the tests pass. The `concurrency` group lets a second push wait for the first deploy, instead of starting a deploy that Brewless would refuse while one is under way.

## GitLab CI

Add `BREWLESS_API_TOKEN` under **Settings**, **CI/CD**, **Variables**, masked and protected, so only pipelines on protected branches such as `main` get it.

```yaml .gitlab-ci.yml
stages:
  - test
  - deploy

test:
  stage: test
  image: composer:2
  script:
    - composer install --no-interaction --prefer-dist
    - cp .env.example .env
    - php artisan key:generate
    - php artisan test

deploy:
  stage: deploy
  image: composer:2
  resource_group: production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script:
    - composer global require brewless/cli
    - export PATH="$(composer global config bin-dir --absolute --quiet):$PATH"
    - brewless deploy production --no-interaction
```

The `deploy` stage only starts when every job in `test` passed. `resource_group: production` makes GitLab run one deploy to production at a time. Use an image with the PHP extensions your test suite needs; the deploy job only needs PHP and `git`.

> [!TIP]
> Deploy `staging` from a `develop` branch with the same job and another environment name. One token covers every environment of the organisation.

## When a deploy fails in CI

A failed release never goes live: the release that was serving visitors keeps doing so. The pipeline log shows which step failed. When the build failed, the client prints the last lines of the build log under the error, read from your Scaleway project, so you see why without leaving CI.

Two failures happen before anything is built:

- **Missing variables.** When the environment's latest revision lacks a variable Brewless left for you to fill in, the deploy is refused with the missing names. Add them with `brewless env:push` from your own machine.
- **A refused `brewless.yml`.** A name under `variables` that looks like a secret, or a resource the file would switch for another, stops the deploy with the line it is about. Run `brewless plan production` locally to see this before you push.

When a release went live and turns out to be wrong, deploy a fix or roll back; [rollbacks in Laravel](/blog/laravel-rollbacks) covers what that does and does not undo.

## Revoking a token

Revoke a token on the **Account** page when a pipeline is retired, when a CI provider reports a leak, or when you are unsure who holds it. It stops working at once. Make a new one, put it in the CI secrets, and the next pipeline run uses it. The full reference is on [Deploying from a pipeline](https://docs.brewless.eu/deployments/from-a-pipeline).

## Summary

1. Make a pipeline token under **Account**, **Pipeline tokens**, and store it in CI as `BREWLESS_API_TOKEN`.
2. Run the tests in their own job; let the deploy job depend on it.
3. Install the client with `composer global require brewless/cli` and run `brewless deploy production --no-interaction`.
4. Let CI run one deploy per environment at a time.
5. Revoke a token as soon as you are unsure who holds it.

## Frequently asked questions

### How do I deploy Laravel from GitHub Actions with Brewless?

Make a pipeline token in the Brewless console and store it as the repository secret BREWLESS_API_TOKEN. In the workflow, install the client with composer global require brewless/cli and run brewless deploy production --no-interaction with that secret in the environment.

### Does my CI pipeline need my production .env file?

No. The variables of the environment stay in Scaleway Secret Manager in your own project, and the release takes them from there. The pipeline only needs the pipeline token.

### What happens when a deploy fails in CI?

The client follows the release until it is live or has failed, and ends with a non-zero exit code when it failed, so the pipeline fails with it. A failed release never goes live; the previous release keeps serving visitors.

### Does a pipeline token expire?

No. It works until you revoke it on the Account page, or until its maker is removed from the organisation. The list of tokens shows when each one was last used.

### Can two pipelines deploy to the same environment at once?

No. An environment has one release under way at a time, and a second deploy is refused until the first finishes. Let your CI queue deploys per environment so the second run waits instead of failing.

Source: https://test.brewless.eu/blog/deploy-laravel-from-ci
