Skip to content

push-secrets

Push secrets from a .dev.vars file to a Cloudflare Pages project via wrangler.

Metadata

  • Author: ropean, Claude Sonnet (Anthropic)
  • Version: 1.0.0
  • Dependencies: wrangler (in PATH)
  • See Also: md-files/help/script-template.md

Code

python
#!/usr/bin/env python3
"""
@title push-secrets
@description Push secrets from a .dev.vars file to a Cloudflare Pages project via wrangler.
@author ropean, Claude Sonnet (Anthropic)
@version 1.0.0

Reads KEY=VALUE pairs from a .dev.vars (or any env-style) file and pushes
them as Cloudflare Pages secrets using `wrangler pages secret put`.

Secrets are discovered automatically from the file — no hard-coded list.

Supports Windows, macOS, Linux, and WSL paths for DEV_VARS_PATH.

@example
    # Full args:
    python push-secrets.py --vars-path C:/project/.dev.vars --project my-app

    # Interactive prompts (no args):
    python push-secrets.py

@requires wrangler (in PATH)
@see md-files/help/script-template.md
"""

import argparse
import subprocess
import sys
from pathlib import Path

# resolve_path lives next to this file
sys.path.insert(0, str(Path(__file__).parent))
from path_utils import resolve_path  # noqa: E402  (local import after sys.path tweak)

# ════════════════════════════════════════════════════════════
#  COLORS
# ════════════════════════════════════════════════════════════

def _supports_color() -> bool:
    return sys.stdout.isatty() and sys.platform != "win32" or (
        sys.platform == "win32" and "ANSICON" in __import__("os").environ
    )


def _c(code: int, text: str) -> str:
    if not _supports_color():
        return text
    return f"\033[{code}m{text}\033[0m"


INFO  = lambda t: print(_c(36, t))
OK    = lambda t: print(_c(32, t))
WARN  = lambda t: print(_c(33, f"WARN:  {t}"), file=sys.stderr)
ERR   = lambda t: print(_c(31, f"ERROR: {t}"), file=sys.stderr)

# ════════════════════════════════════════════════════════════
#  PARSING
# ════════════════════════════════════════════════════════════

def parse_env_file(path: Path) -> dict[str, str]:
    """Return KEY→value pairs from an env-style file, skipping blanks/comments."""
    vars_: dict[str, str] = {}
    with path.open(encoding="utf-8") as fh:
        for raw in fh:
            line = raw.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                continue
            key, _, value = line.partition("=")
            key   = key.strip()
            value = value.strip()
            # Strip surrounding quotes if present
            if len(value) >= 2 and value[0] in ('"', "'") and value[-1] == value[0]:
                value = value[1:-1]
            if key:
                vars_[key] = value
    return vars_


# ════════════════════════════════════════════════════════════
#  PUSH
# ════════════════════════════════════════════════════════════

def push_secrets(vars_path: Path, project: str) -> None:
    INFO(f"\nReading vars from: {vars_path}")

    if not vars_path.exists():
        ERR(f"{vars_path} not found.")
        ERR("Create it from the example:  cp .dev.vars.example .dev.vars")
        sys.exit(1)

    secrets = parse_env_file(vars_path)
    if not secrets:
        ERR("No KEY=VALUE pairs found in the file.")
        sys.exit(1)

    INFO(f"Found {len(secrets)} secret(s): {', '.join(secrets)}")
    INFO(f"Target project: {project}\n")

    # On Windows, npm-installed CLIs are .cmd shims that require shell=True to resolve.
    use_shell = sys.platform == "win32"

    pushed = 0
    for key, value in secrets.items():
        try:
            result = subprocess.run(
                ["wrangler", "pages", "secret", "put", key, "--project-name", project],
                input=value,
                encoding="utf-8",
                errors="replace",
                capture_output=True,
                shell=use_shell,
            )
            if result.returncode == 0:
                OK(f"  + {key}")
                pushed += 1
            else:
                WARN(f"wrangler exited {result.returncode} for {key}: {result.stderr.strip()}")
        except FileNotFoundError:
            ERR("wrangler not found. Install it:  npm install -g wrangler")
            sys.exit(1)

    print()
    OK(f"Done - {pushed}/{len(secrets)} secret(s) pushed to project '{project}'.")


# ════════════════════════════════════════════════════════════
#  INTERACTIVE PROMPT
# ════════════════════════════════════════════════════════════

def _prompt(label: str, default: str = "") -> str:
    hint = f" [{default}]" if default else ""
    try:
        value = input(f"  {label}{hint}: ").strip()
    except (EOFError, KeyboardInterrupt):
        print()
        sys.exit(0)
    return value or default


def gather_args_interactively() -> tuple[Path, str]:
    print(_c(36, "\n── push-secrets interactive mode ──\n"))
    raw_path = _prompt("Path to .dev.vars file (supports Windows/WSL/Unix paths)")
    while not raw_path:
        WARN("DEV_VARS_PATH is required.")
        raw_path = _prompt("Path to .dev.vars file")

    project = _prompt("Cloudflare Pages project name")
    while not project:
        WARN("PROJECT is required.")
        project = _prompt("Cloudflare Pages project name")

    return resolve_path(raw_path), project


# ════════════════════════════════════════════════════════════
#  CLI
# ════════════════════════════════════════════════════════════

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Push .dev.vars secrets to a Cloudflare Pages project.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  python push-secrets.py --vars-path C:/proj/.dev.vars --project my-app\n"
            "  python push-secrets.py --vars-path /mnt/c/proj/.dev.vars --project my-app\n"
            "  python push-secrets.py                    # interactive prompts\n"
        ),
    )
    parser.add_argument(
        "--vars-path", "-v",
        metavar="DEV_VARS_PATH",
        help="Path to the .dev.vars (or any KEY=VALUE) file. "
             "Accepts Windows (C:\\…), WSL (/mnt/c/…), macOS/Linux paths.",
    )
    parser.add_argument(
        "--project", "-p",
        metavar="PROJECT",
        help="Cloudflare Pages project name.",
    )
    args = parser.parse_args()

    if args.vars_path and args.project:
        vars_path = resolve_path(args.vars_path)
        project   = args.project
    elif args.vars_path or args.project:
        # Partial args — fill missing ones interactively
        if not args.vars_path:
            WARN("--vars-path is required.")
            raw = _prompt("Path to .dev.vars file")
            vars_path = resolve_path(raw)
        else:
            vars_path = resolve_path(args.vars_path)

        if not args.project:
            WARN("--project is required.")
            project = _prompt("Cloudflare Pages project name")
        else:
            project = args.project
    else:
        vars_path, project = gather_args_interactively()

    push_secrets(vars_path, project)


if __name__ == "__main__":
    main()

File Information

  • Filename: push-secrets.py
  • Category: python
  • Language: PYTHON

View on GitHub