Skip to main content

GitLab CI

Install Composer and npm dependencies from a broker project using masked CI/CD variables and lock-file cache keys.

Prerequisites​

Complete Composer setup and npm setup in the consuming application. Commit composer.lock and package-lock.json. The example assumes both ecosystems are in the repository root and that a Docker runner can reach your broker over HTTPS.

Use a service principal bound to one project with project:read and project:packages:read, and issue a token with scope read. Creating the principal requires project:service-principals:write; issuing the token requires project:tokens:create, the permissions it confers and fresh re-authentication. See token kinds for expiry and project boundaries.

Add masked variables​

In the application's GitLab CI/CD variable settings, add NPM_TOKEN containing the complete project service token as a variable value, not a file variable. Select masked visibility, disable variable-reference expansion and mark it protected if the pipeline runs only on protected refs. Store the raw token once; the Composer job constructs COMPOSER_AUTH from it. JSON cannot be stored directly as a masked value under GitLab's masked-variable validation.

Keep the literal ${NPM_TOKEN} in the committed npm config; the client substitutes the CI variable when it runs. Do not print the token or the constructed JSON, enable shell tracing, or commit auth.json.

Add the pipeline​

Save this as .gitlab-ci.yml in the consuming application. Replace <organization> and <project> in both cache prefixes with the broker organization and project identifiers. Set BROKER_HOST to the broker host, including any nondefault port, without a scheme or path. Choose the runner images according to your application's PHP extensions and Node requirements.

stages:
- install

composer-install:
stage: install
image: composer:2
variables:
COMPOSER_CACHE_DIR: '$CI_PROJECT_DIR/.ci-cache/composer'
BROKER_HOST: '<broker-host>'
cache:
key:
prefix: '<organization>-<project>-$CI_PROJECT_ID-composer'
files:
- composer.lock
paths:
- .ci-cache/composer/files/
script:
- |
export COMPOSER_AUTH="$(php -r 'echo json_encode(["http-basic" => [getenv("BROKER_HOST") => ["username" => "token", "password" => getenv("NPM_TOKEN")]]], JSON_THROW_ON_ERROR);')"
- composer install --no-interaction --prefer-dist --no-progress --no-plugins --no-scripts

npm-install:
stage: install
image: node:24
variables:
npm_config_cache: '$CI_PROJECT_DIR/.ci-cache/npm'
cache:
key:
prefix: '<organization>-<project>-$CI_PROJECT_ID-npm'
files:
- package-lock.json
paths:
- .ci-cache/npm/_cacache/
script:
- npm ci

Both jobs receive NPM_TOKEN from CI/CD settings; the Composer job constructs COMPOSER_AUTH in its environment without printing it. Cache keys separate the broker organization, project, GitLab project and client, and change with the relevant lock file. These caches contain package archives, including private package contents. Restrict cache access to trusted runners and the intended project; keep protected and unprotected pipeline caches separate. Do not cache home directories, authentication files or npm logs.

Composer plugins and scripts are disabled in this recipe. Review any plugins or scripts your application needs before enabling them, since jobs have access to CI credentials. For an application using only one ecosystem, keep just that job and its lock file.

If you adapt this recipe for pnpm 12, commit only the scope registry line in the project's .npmrc and put the literal token reference in the runner user's ~/.npmrc, as shown in pnpm setup. Before the pnpm install, run the following line after replacing the host and project placeholders. The single quotes preserve the literal ${NPM_TOKEN} reference. Supply NPM_TOKEN to the install step. Use the pnpm lock and cache paths for that job; project-level _authToken settings are ignored.

printf '%s\n' '//<broker-host>/<project>/npm/:_authToken=${NPM_TOKEN}' >> "$HOME/.npmrc"

Verify and separate publishing​

Run the pipeline on a ref allowed to read the variables. Confirm both install jobs succeed, then rerun to check cache reuse. Inspect the locks for broker distribution URLs and the logs for credential exposure. If authentication fails, check that protected variables are available to this ref before replacing the token.

The install token has scope read, which excludes package-write permission. npm writes currently return HTTP 405, so the pipeline has no broker publish job. Keep credentials for another publishing destination in a separate job; do not widen this install token to make a publish command work.

If a token leaked​

Stop affected jobs. Have someone with project:tokens:revoke revoke the token through the project's Tokens page or the authenticated administrative route DELETE /_pb/projects/:project/tokens/:token. Use the token record ID for :token, never the credential. Revocation makes subsequent authentication fail. Issue a replacement with scope read, replace the masked NPM_TOKEN variable and rerun the pipeline. Removing the exposed value from Git history or logs does not revoke it.

For planned rotation, use POST /_pb/projects/:project/tokens/:token/rotate with overlapDays set to 0, 1 or 7. It requires project:tokens:create and project:tokens:revoke and fresh re-authentication. Update the masked NPM_TOKEN variable with the replacement credential before the overlap ends. After a leak, revoke the exposed token first and issue a replacement without an overlap.

For GitHub's secret and workflow equivalents, use GitHub Actions.