# Record deploys

> Send one request after each deploy, from GitHub Actions, Forge, Ploi, Envoyer, Vercel or any CI, so charts get a marker and new errors point to the deploy.

A deploy marker tells Lampwick which release is live. Charts get a line at each deploy, every issue shows the deploy it was first seen in, and the issue list can show only the errors that are new since the last deploy. Recording one is a single `POST` request; add it as the last step of your deploy.

## Requirements

- The project's ingest token, from the **Connect your app** card on its **Settings** page. It's the same token your app sends data with (`LAMPWICK_TOKEN`), so CI needs no personal API token. Only team owners see it.
- `curl` and `jq` for the shell recipes, or the `lampwick/laravel` package for `php artisan lampwick:deploy`.

## GitHub Actions

Add the ingest token as a repository secret named `LAMPWICK_TOKEN` (**Settings → Secrets and variables → Actions**), then add this step after your deploy step:

```yaml
- name: Record deploy in Lampwick
  env:
    LAMPWICK_TOKEN: ${{ secrets.LAMPWICK_TOKEN }}
  run: |
    body=$(jq -n --arg version "${GITHUB_SHA::12}" --arg commit_sha "$GITHUB_SHA" \
      --arg repo_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
      --arg author "$GITHUB_ACTOR" --arg environment production '$ARGS.named')
    printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" |
    curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \
      -H "Content-Type: application/json" --data "$body" \
      || echo "::warning::Lampwick: deploy marker not recorded"
```

It sends the first 12 characters of the commit as the version, with the full commit, the repository URL and the GitHub user who started the workflow. The token goes to curl on stdin, and GitHub masks it in the log. For a workflow that runs on tags, use `"$GITHUB_REF_NAME"` as the version. A failed request is a warning in the log and the step still succeeds: your deploy went fine, only its marker is missing. Leave out the `|| echo` line to fail the step instead.

## Laravel Forge

Record the deploy from the server, in the deploy script. Then the marker lands when the release is live, and the token is the one in the site's `.env`, so it needs no secret in CI.

With the `lampwick/laravel` package, add one line to the site's **Deploy script** after `$ACTIVATE_RELEASE()` (zero-downtime deployments) or at the end (other sites):

```bash
$ACTIVATE_RELEASE()

$FORGE_PHP artisan lampwick:deploy --commit="$FORGE_DEPLOY_COMMIT" --author="$FORGE_DEPLOY_AUTHOR" || true
```

The command reads `LAMPWICK_TOKEN` from `.env` and sends `LAMPWICK_DEPLOY` as the version, or the first 12 characters of the commit when that isn't set. The environment is the app's (`APP_ENV`), and the repository URL comes from the git remote `origin` when there is one. It prints `Recorded deploy 9f1c2ab3d4e5 in Lampwick.` `|| true` keeps a failed request from marking a live release as a failed deployment.

Without the package (any app with `LAMPWICK_TOKEN` in its `.env`):

```bash
LAMPWICK_TOKEN=$(grep -E '^LAMPWICK_TOKEN=' .env | head -n1 | cut -d= -f2- | tr -d "\"' \r")
body=$(jq -n --arg version "${FORGE_DEPLOY_COMMIT:0:12}" --arg commit_sha "$FORGE_DEPLOY_COMMIT" --arg author "$FORGE_DEPLOY_AUTHOR" \
  '{version: $version, commit_sha: $commit_sha, author: $author, environment: "production"}')
printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" |
curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \
  -H "Content-Type: application/json" --data "$body" || true
```

The script runs in the release directory, where `.env` is the site's shared one. It reads only the token line from `.env` (it doesn't `source` the file), and hands the token to curl on stdin so it stays out of the server's process list. `FORGE_DEPLOY_COMMIT` and `FORGE_DEPLOY_AUTHOR` are [Forge's deploy script variables](https://forge.laravel.com/docs/sites/deployments) (checked 10 Nov 2026).

## Ploi

Add the line at the end of the site's **Deploy script**:

```bash
php artisan lampwick:deploy --commit={COMMIT_HASH} || true
```

Ploi replaces `{COMMIT_HASH}` with the deployed commit before the script runs.  With zero-downtime deployment, Ploi runs the deploy script before it switches the `current` link to the new release, so the marker can land a few seconds early.

## Envoyer

Add a deployment hook (**Deployment Hooks**) that runs after **Activate New Release**, as the user that owns the app:

```bash
cd "{{release}}" && php artisan lampwick:deploy --commit={{sha}} || true
```

Envoyer cancels a deployment when a hook fails; `|| true` stops a failed request from doing that. `{{release}}` and `{{sha}}` are [Envoyer's hook variables](https://docs.envoyer.io/projects/deployment-hooks) (checked 10 Nov 2026).

## Vercel

Vercel has no deploy script to add a step to. Its GitHub integration reports each deployment to GitHub, so a workflow on `deployment_status` can record the deploy once Vercel says it's live:

```yaml
# .github/workflows/lampwick-deploy.yml
name: Lampwick deploy
on: deployment_status
permissions: {}
jobs:
  record:
    if: github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Production'
    runs-on: ubuntu-latest
    steps:
      - name: Record deploy in Lampwick
        env:
          LAMPWICK_TOKEN: ${{ secrets.LAMPWICK_TOKEN }}
        run: |
          body=$(jq -n --arg version "${GITHUB_SHA::12}" --arg commit_sha "$GITHUB_SHA" \
            --arg repo_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
            --arg author "$GITHUB_ACTOR" --arg environment production '$ARGS.named')
          printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" |
          curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \
            -H "Content-Type: application/json" --data "$body" \
            || echo "::warning::Lampwick: deploy marker not recorded"
```

The job doesn't check out or run your code, and `permissions: {}` leaves it no access to the repository; keep it that way. Leave Vercel's fork protection on: every deployment, also one of a pull request from a fork, triggers this workflow with your secrets. The workflow file must be on the default branch. Drop the environment check to record preview deployments too, but send those to a separate project. On `deployment_status`, `GITHUB_SHA` is the deployed commit. Deploying with `vercel deploy --prod` from your own CI instead? Add the step after that, as in [GitHub Actions](#github-actions).

## Any other CI

GitLab CI, Bitbucket Pipelines, CircleCI, Buildkite or a shell script: store the token as a secret variable `LAMPWICK_TOKEN` and run this after the deploy:

```bash
body=$(jq -n --arg version "$VERSION" --arg commit_sha "$COMMIT_SHA" \
  '{version: $version, commit_sha: $commit_sha, environment: "production"}')
printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" |
curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \
  -H "Content-Type: application/json" --data "$body" \
  || echo "Lampwick: deploy marker not recorded"
```

Set `VERSION` and `COMMIT_SHA` from your CI's variables, for example `$CI_COMMIT_SHA` on GitLab or `$BITBUCKET_COMMIT` on Bitbucket. `jq` builds the JSON, so a version with quotes can't break it. The token goes to curl on stdin (`-K -`), not on its command line, where other processes on the machine could read it. The `|| echo` keeps a failed request from failing the pipeline; leave it out if a missing marker should stop the pipeline. The answer is `201` with the deploy:

```json
{"deploy":{"author":null,"commit_sha":"9f1c2ab3d4e5f60718293a4b5c6d7e8f90a1b2c3","environment":"production","id":3,"new_issues":0,"source":"ingest_token","version":"v1.4.2", …}}
```

## What to send

| Field | | What to put in it |
|---|---|---|
| `version` | required | What's live now, at most 100 characters: a tag (`v1.4.2`), a release name or the commit. Use the same value you set as `LAMPWICK_DEPLOY`, if any. |
| `commit_sha` | optional | The deployed commit, hex. |
| `repo_url` | optional | The repository's `https://` URL. Lampwick links the commit as `{repo_url}/commit/{commit_sha}`. |
| `author` | optional | Who deployed. |
| `environment` | optional | `production`, `staging`, … |
| `deployed_at` | optional | RFC 3339 time it went live. Default: now. Never later than now. |

Send it when the new release is live, not when the build starts. A deploy stays live until the next one, and issues belong to the deploy that was live when they were first seen. Staging and production in one project mix up: use a project per environment.

## Let your agent add it

Paste this into Claude Code, Cursor or Codex in your repository:

```text
Add a Lampwick deploy marker to this project's deploy. Docs: https://lampwick.dev/docs/deploys.md
1. Find where the app is deployed: a GitHub Actions workflow, a Forge/Ploi/Envoyer deploy script
   in the repo or its README, vercel.json, or another CI config. If you can't tell, ask me.
2. Add the matching recipe from the docs as the last step, after the release is live. Use the
   project's ingest token from the LAMPWICK_TOKEN secret or the server's .env; never write the
   token into a file in the repository.
3. A failed marker must never fail the deploy (keep the recipe's `|| true` or `|| echo`).
4. Tell me which secret I need to add and where, and show me the diff.
```

## Check that it works

Run a deploy, then open the project's **Deploys** page in Lampwick. The deploy shows its version, commit and **from CI**. The charts get a marker at that time.

## Troubleshooting

| Message | Cause | Fix |
|---|---|---|
| `401` *send the project's ingest token* | The token is a personal API token, or empty. | Use the ingest token from the project's **Settings** page. |
| `400` *commit_sha must be a hexadecimal commit hash (4 to 64 characters)* | A tag or branch name in `commit_sha`. | Put the tag in `version` and the hash in `commit_sha`. |
| `400` *version is required* | `version` is empty, for example an unset CI variable. | Check the variable you send as `version`. |
| `429` *at most 20 deploys sent with the ingest token per project per hour* | More than 20 deploys in an hour, often a workflow that runs on every push to every branch. | Record only production deploys. |
| `Could not record the deploy: HTTP 401` | `lampwick:deploy` sent a token that isn't the project's ingest token. | Copy the ingest token into `.env` again. |
| `LAMPWICK_TOKEN is not set.` | `lampwick:deploy` runs where `.env` has no token. | Add `LAMPWICK_TOKEN` to the site's `.env`. |

Next: [work with issues](/docs/issues/), including the list of issues new since the last deploy.
