Skip to content
Solo ProTeamBusinessEnterprise

CI/CD pipelines

Load PulsVault secrets into GitHub Actions and GitLab CI, and gate a deploy on whether those credentials still work.

Two jobs, two integrations. Loading puts secrets into your job as masked environment variables. Gating refuses to deploy with a credential that no longer works. They are separate on purpose — most pipelines want one without the other, and a gate that also rewrites your environment is harder to reason about than two steps that each do one thing.

Both consume the same machine data-plane. Store your API key as an encrypted CI variable (PULSVAULT_TOKEN) and grant its service account a read policy for the paths you need.

Loading secrets#

GitHub Actions#

yaml
- name: Load secrets from PulsVault
  uses: PulsVault/load-secrets-action@v2
  with:
    token: ${{ secrets.PULSVAULT_TOKEN }}
    namespace: acme-web/prod/product/web
    paths: |
      db/password -> DATABASE_PASSWORD
      stripe/api-key -> STRIPE_KEY

The action bulk-reads the mapped secrets, masks each value in the logs with ::add-mask::, and exports them as environment variables for later steps.

GitLab CI#

yaml
include:
  - remote: 'https://templates.pulsvault.com/gitlab-ci/v2/load-secrets.yml'

deploy:
  extends: .pulsvault-load-secrets
  variables:
    PULSVAULT_NAMESPACE: acme-web/prod/product/web
    PULSVAULT_PATHS: |
      db/password -> DATABASE_PASSWORD
  script:
    - ./deploy.sh          # DATABASE_PASSWORD is exported for the job

Set PULSVAULT_TOKEN (and optionally PULSVAULT_HOST) as masked CI/CD variables.

Pinning a version#

include: remote: has no version syntax of its own, so the version lives in the path:

URL Resolves to
…/gitlab-ci/v2/load-secrets.yml the newest 2.x — use this
…/gitlab-ci/v2.0.0/load-secrets.yml that exact release, forever

v2 moves as 2.x releases happen, the way @v2 does for a GitHub Action. Pin the full version if your pipeline must never change under you.

Universal pattern (any CI)#

Anywhere the CLI runs, run injects a whole namespace as env vars around your command:

bash
pulsvault run --namespace acme-web/prod/product/web -- ./deploy.sh

Gating a deploy on credential health#

A secret being present is not the same as it being usable. A rotated password or a revoked token reads back perfectly and breaks the deploy that uses it. Gating closes that gap.

Which check to run#

Two questions, and most pipelines are asking the second one:

You need Use Cost
Was this known-good recently? max-age Reads the result your scheduled checks already produced. Nothing reaches your provider
Does it work right now? no max-age Runs the probe — a real sign-in with your credential

Reach for max-age by default. PulsVault is already checking these credentials on a schedule you pay for, so asking "was it green within the hour" is free and truthful. Save the live probe for the moments where now is the question: just after rotating a credential, or immediately before promoting a build.

GitHub Actions#

yaml
- name: Check credentials
  id: check
  uses: PulsVault/validate-action@v2
  with:
    token: ${{ secrets.PULSVAULT_TOKEN }}
    path: acme-web/prod/product/web/db
    max-age: 1h            # omit to probe now

- run: ./deploy.sh

The action reports an outcome and fails the step only for the outcomes you name in fail-on:

outcome Means In fail-on by default
pass the credential works —
failed it genuinely failed yes
transport could not reach or authenticate to PulsVault yes
incomplete the check could not be completed — a timeout, a probe error, rate-limited no, warns
not-configured no validation is configured for that path no, warns
yaml
    fail-on: failed,transport,incomplete   # strict: stop even when we could not tell
    fail-on: failed,transport,not-configured  # enforce "production paths must be validated"

outcome, result and exit-code are outputs, so a later step can branch on them:

yaml
- if: steps.check.outputs.outcome == 'incomplete'
  run: echo "Could not verify — deploying on the last known-good result"

GitLab CI#

The template uses GitLab's own allow_failure: exit_codes, which is exactly the feature this split was designed for:

yaml
include:
  - remote: 'https://templates.pulsvault.com/gitlab-ci/v2/validate.yml'

check-credentials:
  extends: .pulsvault-validate
  variables:
    PULSVAULT_PATH: acme-web/prod/product/web/db
    PULSVAULT_MAX_AGE: 1h        # omit to probe now

deploy:
  needs: [check-credentials]
  script:
    - ./deploy.sh

Exit 1 (the credential failed) and 5 (could not reach PulsVault) fail the job. Exit 3 (could not complete the check) and 4 (nothing configured) are allow_failure by default — they show as warnings and the pipeline continues. For a strict gate:

yaml
check-credentials:
  extends: .pulsvault-validate
  allow_failure: false           # 3 and 4 now stop the pipeline too

Any other CI#

The CLI carries the same contract, so a plain script works anywhere:

bash
pulsvault validation-status acme-web/prod/product/web/db --max-age 1h && ./deploy.sh
pulsvault validate acme-web/prod/product/web/db && ./deploy.sh          # probe now
Do not silence the gate

The reason the outcomes are separate is so you never have to. If a timeout is failing your pipeline, move incomplete out of fail-on (or leave GitLab's allow_failure alone) — that keeps the warning visible while letting the deploy through. Appending || true throws away the revoked-credential signal along with the noisy one, and it is the change nobody remembers to undo.

The full exit-code contract is on Validating credentials.

Rotate keys without breaking pipelines

Issue a new key, update the CI variable, verify the pipeline, then revoke the old key — see rotation.

A pipeline with both#

yaml
include:
  - remote: 'https://templates.pulsvault.com/gitlab-ci/v2/validate.yml'
  - remote: 'https://templates.pulsvault.com/gitlab-ci/v2/load-secrets.yml'

check-credentials:
  extends: .pulsvault-validate
  variables:
    PULSVAULT_PATH: acme-web/prod/product/web/db
    PULSVAULT_MAX_AGE: 1h

deploy:
  needs: [check-credentials]
  extends: .pulsvault-load-secrets
  variables:
    PULSVAULT_NAMESPACE: acme-web/prod/product/web
    PULSVAULT_PATHS: |
      db/password -> DATABASE_PASSWORD
  script:
    - ./deploy.sh

Gate first, load second. The gate costs nothing and fails in seconds; loading is the step you only want to reach once you know the credentials behind it are good.

Was this page helpful?