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.
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, runnpx dependra initand 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.jsonunderprofiles.<name>.tenantId.
Step 1: Approve the repository in Dependra
- Sign in to dependra.io and open Settings → GitHub.
- Click Add Binding.
- 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.
- Repository — bind a single repository (
- 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.
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:
- Validates the manifest. Every issue becomes a GitHub annotation on the offending file and line.
- 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.
- 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-syncand nothing else, - valid for 15 minutes, and
- single-use — the OIDC token’s
jtiis recorded, so a replay is refused with409.
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.
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.
Related
- Quickstart — connect your coding agent — author the manifest in the first place.
- API keys — scopes, rotation and revocation for the fallback path.
- Model your first landscape — what the manifest is describing.