Composer tokens in CI: GitHub Actions and GitLab CI
Give your pipeline a scoped Composer token through COMPOSER_AUTH, cache the downloads, and report the lock file to your dependency dashboard, without the token ever touching the repository.
About this guide
- Difficulty
- Intermediate
- Time to complete
- 20 minutes
- Reading time
- 4 min read
Prerequisites
- A project whose
composer.jsonlists the MeteorGPL repository - Permission to add secrets (GitHub) or CI/CD variables (GitLab) to the repository
- For a team, an organisation on MeteorGPL, so the token does not belong to one person
In this guide
A pipeline has to install packages without anyone typing credentials, and without the token ending up in the repository or in a build log. Composer reads credentials from the COMPOSER_AUTH environment variable, and CI systems hand secrets to jobs as environment variables. This guide connects the two, and limits what a leaked token could do.
Step 1 — Create a token for the pipeline
For a team project, create an organisation token (your organisation → Composer tokens): it belongs to the team and keeps working when people leave. For a personal project, a personal token (Settings → Composer tokens) is enough.
Limit the token when you create it:
- Packages — only the Composer names the project uses. A pattern such as
meteorgpl-plugin/*allows every plugin. - Networks — only the IP ranges your runners use. This suits self-hosted runners with fixed addresses; leave it empty for hosted runners whose addresses change.
- Expiry — the date the token stops working. You are notified when one of your tokens expires within seven days.
Copy the token when it is shown: it is shown once.
Step 2 — Save it as a secret
Name the secret METEORGPL_TOKEN in both systems:
- GitHub: the repository's Settings → Secrets and variables → Actions → New repository secret.
- GitLab: the project's Settings → CI/CD → Variables. Mark it masked, and protected if only protected branches install packages.
Step 3 — GitHub Actions
COMPOSER_AUTH holds the same JSON as an auth.json file. The user name is the literal word token; the password comes from the secret:
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-
- name: Install PHP dependencies
env:
COMPOSER_AUTH: '{"http-basic":{"composer.meteorgpl.ddev.site":{"username":"token","password":"${{ secrets.METEORGPL_TOKEN }}"}}}'
run: composer install --no-interaction --prefer-dist
Dists never change once published, so the cache is safe to keep between runs; it is keyed on composer.lock, so a new lock file starts a fresh one.
Step 4 — GitLab CI
GitLab expands $METEORGPL_TOKEN inside the variable's value. GitLab only caches paths inside the project directory, so point Composer's cache there:
install:
image: composer:2
variables:
COMPOSER_AUTH: '{"http-basic":{"composer.meteorgpl.ddev.site":{"username":"token","password":"$METEORGPL_TOKEN"}}}'
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
cache:
key:
files:
- composer.lock
paths:
- .composer-cache/
script:
- composer install --no-interaction --prefer-dist
Bitbucket Pipelines and CircleCI work the same way; the CI documentation has their snippets.
Step 5 — Report the lock file (optional)
The dependency dashboard compares a site's composer.lock with the repository and shows each package as up to date, behind (with the changelog entries you are missing), withdrawn or taken down. Create a project under Settings → Projects (or your organisation's Projects), then let the pipeline upload the lock file after each build of the main branch:
- name: Report composer.lock to the dependency dashboard
if: github.ref == 'refs/heads/main'
run: curl -fsS -u "token:${{ secrets.METEORGPL_TOKEN }}" -F [email protected] https://meteorgpl.ddev.site/api/projects/<project-id>/lock
The token must belong to the account or the organisation that owns the project.
Keep the token out of sight
- Never print
COMPOSER_AUTHor the secret. GitHub masks secrets in logs and GitLab masks masked variables, but only in the forms they recognise. - Never commit an
auth.json. WithCOMPOSER_AUTHthere is no file to commit. - Rotate without downtime: create the new token, update the secret, run the pipeline, revoke the old token.
- A token used from many different networks in one day is treated as shared: you are told, and a clearly leaked token is revoked automatically.
Troubleshooting
- 401 Unauthorized — the secret is missing, mistyped, expired or revoked. On GitHub, workflows started by pull requests from forks do not receive repository secrets.
- 403 Forbidden — the token is valid, but the account or organisation it belongs to has no repository access, or the token's package or network limits exclude this request.
- Composer asks for a user name — the host in
COMPOSER_AUTHmust be exactlycomposer.meteorgpl.ddev.site: nohttps://, no path.