Skip to content

functions:secrets:set and :prune never destroy anything: pruneSecrets filters firebase-managed=true, but labels() writes firebase-managed=functions #11066

Description

@zuhairizainuddin

Environment info

firebase-tools: 15.28.1, and still present on main / 15.30.0

Platform: macOS 15 (Darwin 25.5.0)

Test case

functions:secrets:set never destroys the stale version, and functions:secrets:prune never finds anything to prune, for any secret the CLI itself created. pruneSecrets filters on firebase-managed=true, but labels() writes firebase-managed=functions.

This is the same user-visible symptom as #6074, which was closed in 2023 after the destroy step was rebuilt. The rebuilt destroy is fine; it is just always handed an empty list.

Steps to reproduce

  1. firebase functions:secrets:set MY_KEY and deploy, so the CLI creates the secret.
  2. Rotate it: firebase functions:secrets:set MY_KEY, answer Y to "re-deploy the functions and destroy the stale version".
  3. gcloud secrets versions list MY_KEY — the superseded version is still ENABLED.
  4. gcloud secrets describe MY_KEY --format="value(labels)"firebase-managed=functions.
  5. firebase functions:secrets:pruneAll secrets are in use. Nothing to prune today.

Expected behavior

The superseded version is DESTROYED, or at minimum the CLI does not report success.

Actual behavior

Nothing is destroyed, and both commands print a success-shaped message.

functions:secrets:set prints the removal line with an empty list, which reads as a no-op that succeeded:

Removing secret versions:

functions:secrets:prune takes the pruned.length === 0 branch and issues a clean bill of health over a set it never examined:

All secrets are in use. Nothing to prune today.

Cause

src/functions/secrets.ts#L241pruneSecrets only selects secrets whose label value is the string true:

const haveSecrets = await listSecrets(projectId, `labels.${FIREBASE_MANAGED}=true`);

But src/gcp/secretManager.ts#L503 writes the product name when a secret is created:

export function labels(product: "functions" | "apphosting" = "functions"): Record<string, string> {
  return { [FIREBASE_MANAGED]: product };   // firebase-managed=functions
}

Every other call site carries a back-compat shim for both values — src/gcp/secretManager.ts#L477:

export function isFunctionsManaged(secret: Secret): boolean {
  return secret.labels[FIREBASE_MANAGED] === "true" || secret.labels[FIREBASE_MANAGED] === "functions";
}

pruneSecrets is the only place that hardcodes =true, so the list query returns zero secrets, prunedSecrets stays empty, and both callers act on an empty array:

  • src/commands/functions-secrets-set.ts#L156-L162
  • src/commands/functions-secrets-prune.ts#L38-L42

This cannot self-heal. Because isFunctionsManaged accepts functions, ensureSecret already treats the secret as managed and never offers the "Would you like to have your secret managed by Cloud Functions for Firebase?" relabel prompt. So a project created by a recent CLI is permanently invisible to prune, with no path back and no warning.

Suggested fix

Use the existing shim in the query rather than a literal, e.g. list once and filter with isFunctionsManaged, or issue both label filters. A one-line change to pruneSecrets.

Note for anyone finding this while auditing their own project

The obvious workaround — gcloud secrets update MY_KEY --update-labels=firebase-managed=true — is worth thinking twice about. pruneSecrets selects versions with NOT state: DESTROYED, so DISABLED versions are prune candidates. If you have disabled superseded versions as a reversible first step (rather than destroying them), relabelling will destroy all of them on your next unrelated secrets:set, irreversibly and without a dry run.

Related: #6074, #4459.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions