Sync your manifest from GitHub Actions

A repository can declare its own slice of the architecture in .dependra/manifest.yaml and push it into your landscape on every merge. The reusable dependra-io/upload-manifest GitHub Action validates the manifest, previews the diff on pull requests, and applies it when the change reaches your default branch or a release.

Authentication is keyless by default: the workflow exchanges its short-lived GitHub Actions OIDC token for a single-use, manifest-sync-scoped Dependra token. No long-lived secret lives in the repository.

Note

A repository can never bind itself. An Owner or Admin approves it once in Dependra — that approval is the whole trust boundary.

Prerequisites

  • An Owner or Admin role in the Dependra workspace the repository should sync into.
  • A repository containing a manifest at .dependra/manifest.yaml. If you don’t have one yet, run npx dependra init and let your coding agent author it — see the agent quickstart.
  • Your Dependra tenant id (a GUID). After dependra login, it is stored in your profile at ~/.dependra/config.json under profiles.<name>.tenantId.

Step 1: Approve the repository in Dependra

  1. Sign in to dependra.io and open Settings → GitHub.
  2. Click Add Binding.
  3. Choose the scope:
    • Repository — bind a single repository (octo-org/octo-repo). Only that repository’s workflow can sync.
    • Organisation — bind an owner (octo-org). Any repository under it can sync.
  4. Click Add binding. The binding appears in the list with the scope manifest-sync.

A binding grants exactly one thing: the right to sync a manifest. It can never be widened into read or write access to the rest of your tenant, and it is revocable from the same page at any time.

Tip

Start with a Repository binding. Organisation scope is convenient for a monorepo-free estate where many repositories sync, but it authorises every repository the owner has — including ones created later.

Step 2: Add the workflow

From inside the repository:

npx dependra github

That scaffolds .github/workflows/dependra-sync.yml, already referencing dependra-io/upload-manifest@v1. It never overwrites an existing file.

Prefer to paste it yourself:

name: Dependra Landscape Sync

on:
  pull_request:
    paths:
      - '.dependra/**'
  push:
    branches: [main]
    paths:
      - '.dependra/**'
  release:
    types: [published]

# Least privilege: id-token mints the OIDC token for the exchange, contents checks out
# the manifest, pull-requests posts the dry-run diff comment.
permissions:
  id-token: write
  contents: read
  pull-requests: write

concurrency:
  group: dependra-sync-${{ github.ref }}
  cancel-in-progress: true

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Sync manifest to Dependra
        uses: dependra-io/upload-manifest@v1
        with:
          tenant-id: ${{ vars.DEPENDRA_TENANT_ID }}
          fail-on: error

Then add the tenant id as a repository variable: Settings → Secrets and variables → Actions → Variables → New repository variable, named DEPENDRA_TENANT_ID. It is an address, not a credential — a variable is the right home for it.

Commit and push the workflow.

Step 3: Watch a pull request preview itself

Open a pull request that touches .dependra/. The Action:

  1. Validates the manifest. Every issue becomes a GitHub annotation on the offending file and line.
  2. Previews the result as a single pull-request comment — the import diff, upserted in place on each push, so the thread never fills with duplicates.
  3. Writes nothing.

Merge to the default branch (or publish a release) and the same workflow applies the manifest. The run is idempotent per commit SHA: a re-run never double-applies.

When a run applies vs. previews

Trigger Mode
pull_request (any branch, any fork) Dry-run
Push to a non-default branch Dry-run
workflow_dispatch Dry-run
Push to the default branch Apply
release (published) Apply

Force a preview on an applying trigger with mode: dry-run. Unreviewed content never writes to your landscape.

How authentication works

GitHub OIDC (default, keyless). With permissions: id-token: write, the Action requests a GitHub OIDC token and posts it to POST https://dependra.io/api/{tenantId}/integrations/github/token. Dependra validates the token’s signature, audience and issuer, reads its repository and repository_owner claims, and checks them against that tenant’s bindings only. On a match it mints a Dependra key that is:

  • scoped to manifest-sync and nothing else,
  • valid for 15 minutes, and
  • single-use — the OIDC token’s jti is recorded, so a replay is refused with 409.

API-key fallback. Supply api-key (a dep_… key carrying the manifest-sync scope) and the OIDC exchange is skipped. Store it as a repository secret, never a variable. Use this only where OIDC is unavailable: a long-lived key is a standing credential, where the OIDC path leaves nothing to steal.

Either way the credential is masked in the logs before it is ever used, and the binding still has to exist — authentication alone syncs nothing.

Action inputs

Input Required Default Description
tenant-id yes — The tenant the manifest syncs into (GUID).
manifest no .dependra/manifest.yaml Path to the root manifest.
dependra-url no https://dependra.io Override only for a self-hosted Dependra.
mode no auto dry-run or apply. Auto follows the trigger table above.
api-key no — dep_… key with the manifest-sync scope. Used instead of OIDC.
audience no https://dependra.io OIDC audience requested for the GitHub token.
fail-on no error Validation severity that fails the step (error or warning).
github-token no ${{ github.token }} Token used to post the diff comment; needs pull-requests: write.

Outputs: applied (true when the manifest was really written), manifest-hash (the fingerprint used for the idempotency key) and diff (the rendered markdown diff).

Pull requests from forks

A fork’s pull request gets no OIDC token and a read-only GITHUB_TOKEN, so the Action cannot authenticate unless you supplied an api-key. It therefore:

  • exits neutral — not a red X — with a “sync unavailable on fork PRs (no OIDC) — will apply on merge” notice;
  • falls back to the job summary for the diff, because a read-only token cannot comment;
  • still fails the check on an invalid manifest. A malformed manifest is a code defect, not a transient condition, so validation is never softened.

The manifest applies on merge, where OIDC is available again.

Warning

Do not run this Action on the pull_request_target event. That event runs in the trusted base context with full secrets against untrusted pull-request code — a classic credential-exfiltration path. The Action refuses the OIDC exchange there and exits neutral unless you explicitly set allow-pull-request-target: true. Use pull_request instead; it is unprivileged on forks and dry-run by design.

Resilience

  • Calls to Dependra have a timeout and a bounded retry — safe, because ingest is idempotent per commit SHA.
  • A Dependra outage fails soft on a pull request (the check posts “sync unavailable”) but fails hard on a release or a default-branch push. A broken mainline sync has to be loud.

Troubleshooting

Symptom Cause and fix
403 — “This repository is not on this tenant’s GitHub whitelist” No binding covers the repository in that tenant. Add it under Settings → GitHub, and check tenant-id points at the workspace you think it does.
409 — “This OIDC token has already been exchanged” The token is single-use. Re-run the job so it mints a fresh one; never cache the exchanged key between steps.
401 — “Invalid OIDC token” Usually a mismatched audience, or permissions: id-token: write missing from the workflow or job.
The run previews but never applies It was not a default-branch push or a published release. Check the trigger table above, and that push.branches names your real default branch.
Validation annotations, red check The manifest itself is invalid. Reproduce locally with npx dependra validate .dependra/manifest.yaml.
No diff comment on a pull request pull-requests: write is missing, or the pull request comes from a fork — look in the job summary instead.

Pinning and upgrades

Consume the moving v1 tag (@v1) for compatible updates, or pin an immutable release (@v1.2.3) for byte-for-byte reproducibility. Breaking changes ship as v2; v1 keeps receiving fixes.