Search icon
Request icon
Script icon
Switch icon
Log icon
SlackIncomingWebhook icon
If icon
Create icon
Schedule icon

GitHub Actions Orphaned Secret Reference Gate

Reconcile GitHub Actions workflow secret references against configured repository secrets to catch silent breakage before it happens.

Categories
Infrastructure

GitHub Actions has a quiet footgun: reference secrets.SOME_TOKEN in a workflow step, and if that secret was renamed, deleted, or never configured, the expression does not fail to parse and does not error at trigger time, it just silently evaluates to an empty string. The step runs anyway, often failing somewhere downstream with a confusing error that has nothing obviously to do with a missing secret. Nobody finds out until that step actually executes, which can be days after the secret was removed. This blueprint closes that gap by reading every workflow file's actual content, extracting every secrets.NAME reference with a regex, and diffing that set against the secrets GitHub's own API says are configured on the repository, in both directions.

This blueprint was created by Mohit Jeswani.

How it works

  1. daily_sweep (io.kestra.plugin.core.trigger.Schedule) runs the reconciliation once a day.
  2. search_workflow_files (io.kestra.plugin.github.code.Search) finds every file under .github/workflows in the target repository, returning each hit's path and download_url, since GitHub's code search API returns file metadata, not file content.
  3. list_configured_secrets (io.kestra.plugin.core.http.Request) calls GitHub's REST API for the repository's configured Actions secret names (never values, GitHub's API does not expose those), with allowFailed: true so a token without secrets-read access reports zero configured secrets instead of crashing the execution.
  4. extract_and_reconcile (io.kestra.plugin.scripts.python.Script) downloads every matched workflow file's raw content, regex-extracts every secrets.NAME reference (recording which file referenced it), and diffs the referenced-name set against the configured-name set to classify the repository as HEALTHY, UNUSED_SECRETS_FOUND (configured but never referenced), or ORPHANED_REFERENCES_FOUND (referenced but not configured, the one that actually breaks something).
  5. route (io.kestra.plugin.core.flow.Switch) alerts Slack for either finding, naming the exact secret names and, for orphaned references, which workflow file referenced each one. Filing a GitHub tracking issue is a separate, explicitly authorized action behind confirm_file_issue in both branches, not tied to the Slack alert.
  6. summary logs the final severity and the referenced/configured counts.
  7. errors alerts Slack if the flow itself fails, for example a repository that does not exist.

What you get

  • A true reconciliation between what workflows declare they need and what the repository actually has configured, catching the exact class of bug that normally surfaces as a confusing downstream failure days after a secret was renamed or removed.
  • Every orphaned reference named alongside the specific workflow file that uses it, so there is no second search step before someone can fix it.
  • A separate, lower-urgency signal for secrets nobody's workflow references anymore, worth pruning but not breaking anything today.

Who it's for

  • Platform and DevOps teams who own a repository's CI secrets and want to know about a naming mismatch before a scheduled job trips over it, not after.
  • Teams doing a secret rotation or rename who want one check that confirms every workflow reference was updated to match.

Why orchestrate this with Kestra

GitHub has no built-in check that cross-references a workflow's secrets.* expressions against what is actually configured; the platform will happily run a workflow with a reference that resolves to nothing. Kestra runs this reconciliation on a schedule, keeps repository and confirm_file_issue as flow inputs so the same flow audits any repository without edits, and keeps a full execution history of every sweep, what it found, and whether it filed a tracking issue.

Prerequisites

  • A GitHub personal access token (classic, repo scope, or fine-grained with Secrets: read-only and Issues: read and write repository permissions) for the target repository.
  • A Slack incoming webhook URL.

This blueprint only calls GitHub's hosted API; nothing needs to run locally to test it beyond a repository you can point it at and a token with the permissions above. Point repository at any public repo with workflows to see search_workflow_files and list_configured_secrets run end to end (a token without secrets-read access still exercises the orphaned-reference path, since list_configured_secrets degrades to an empty configured set via allowFailed).

Secrets

  • GITHUB_ACCESS_TOKEN: used by search_workflow_files, list_configured_secrets, the per-file content fetch inside extract_and_reconcile, and issues.Create. In Kestra OSS, provide it as the base64-encoded environment variable SECRET_GITHUB_ACCESS_TOKEN.
  • SLACK_WEBHOOK_URL: used by both Slack alert cases and the errors block. In Kestra OSS, provide it as SECRET_SLACK_WEBHOOK_URL.

Inputs

Input Default Purpose
repository kestra-io/kestra owner/repo to reconcile
confirm_file_issue false Authorizes create_unused_issue / create_orphaned_issue to actually fire

Outputs

  • {{ outputs.extract_and_reconcile.vars.severity }}: HEALTHY, UNUSED_SECRETS_FOUND, or ORPHANED_REFERENCES_FOUND.
  • {{ outputs.extract_and_reconcile.vars.orphaned }} / .orphaned_detail: the secret names referenced with no matching configured secret, and which workflow file referenced each one.
  • {{ outputs.extract_and_reconcile.vars.unused }}: configured secret names no workflow references at all.

Pitfalls

  • The regex matches the dot-notation form secrets.NAME. A workflow using the bracket form (secrets['NAME']) or building the secret name dynamically from an expression is not detected; this covers the overwhelmingly common case, not every syntactically valid one.
  • list_configured_secrets requires a token with permission to read repository secret metadata; without it the call still returns 200 with zero secrets for personal-repo tokens that lack the scope, which this blueprint cannot tell apart from a repository that genuinely has none configured, and reports every reference as orphaned in that case.
  • Organization-level and environment-level secrets (configured above the repository) are invisible to the repository-scoped actions/secrets endpoint used here, so a reference to one of those will be reported as an orphaned reference even though it resolves correctly at runtime. If your workflows lean on org or environment secrets, treat orphaned as a shortlist to check manually rather than a final verdict.

Quick start

  1. Add the GITHUB_ACCESS_TOKEN and SLACK_WEBHOOK_URL secrets.
  2. Point repository at a repo you own with at least one workflow under .github/workflows.
  3. Run the flow once with the defaults; confirm it reaches HEALTHY or correctly names any existing drift.
  4. Rename a secret in repository settings without updating the workflow that references the old name; re-run and confirm alert_orphaned names it.
  5. Set confirm_file_issue: true and re-run; confirm a tracking issue is opened with the same detail.

How to extend

  • Extend the regex to also catch secrets['NAME'] and interpolated names, trading simplicity for completeness.
  • Pull organization-level secret names via the org-scoped secrets endpoint too, so org secrets stop appearing as false-positive orphaned references.
  • Add a second pass over reusable workflow calls (uses: ./.github/workflows/x.yml with secrets: inherit or explicit secrets: mappings) to catch references passed through a caller workflow rather than read directly.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.