👋đŸģ Currently looking for work!
Please see my LinkedIn profile and get in touch, or see ways to support me in the interim.

â€Ļ has too many hobbies.

Quick Devops Tool: gha-secrets-setup

Here's a small script I wrote a few days ago, gha-secrets-setup.

This script accepts a GitHub repository URL and checks its GitHub Actions workflows for secrets references. Then, using the gh CLI tool, it checks which secrets haven't yet been configure. It interactively prompts you for each secret's value and sets it up on the repo, again using gh.

This has already saved me so much time compared to setting secrets via the GitHub web UI!

The full script is copied below:

#!/bin/bash

set -euo pipefail

readonly SCRIPT_NAME="$(basename "$0")"
readonly TEMP_DIR="$(mktemp -d)"

usage() {
    cat << EOF
Usage: $SCRIPT_NAME <repository-url>

Sets up GitHub Actions secrets for a repository by:
1. Cloning the repository to a temporary directory
2. Scanning GitHub Actions workflows for secret references
3. Checking which secrets already exist
4. Prompting for missing secrets and creating them

Arguments:
  repository-url    GitHub repository URL (https://github.com/owner/repo or owner/repo)

Requirements:
  - gh CLI tool must be installed and authenticated
  - git must be installed

EOF
}

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
}

error() {
    log "ERROR: $*"
    exit 1
}

cleanup() {
    if [[ -d "$TEMP_DIR" ]]; then
        log "Cleaning up temporary directory: $TEMP_DIR"
        rm -rf "$TEMP_DIR"
    fi
}

trap cleanup EXIT

check_requirements() {
    log "Checking requirements..."
    
    if ! command -v gh >/dev/null 2>&1; then
        error "gh CLI tool is not installed. Please install it first."
    fi
    
    if ! command -v git >/dev/null 2>&1; then
        error "git is not installed. Please install it first."
    fi
    
    if ! gh auth status >/dev/null 2>&1; then
        error "gh CLI is not authenticated. Please run 'gh auth login' first."
    fi
    
    log "Requirements check passed"
}

normalize_repo_url() {
    local repo_input="$1"
    
    if [[ "$repo_input" =~ ^https://github\.com/([^/]+/[^/]+)/?$ ]]; then
        echo "${BASH_REMATCH[1]}"
    elif [[ "$repo_input" =~ ^([^/]+/[^/]+)$ ]]; then
        echo "$repo_input"
    else
        error "Invalid repository format. Use 'owner/repo' or 'https://github.com/owner/repo'"
    fi
}

clone_repository() {
    local repo_url="$1"
    local clone_dir="$2"
    
    log "Cloning repository $repo_url to $clone_dir..."
    
    if ! git clone --depth 1 "https://github.com/$repo_url.git" "$clone_dir" >/dev/null 2>&1; then
        error "Failed to clone repository $repo_url"
    fi
    
    log "Repository cloned successfully"
}

find_secrets_in_workflows() {
    local repo_dir="$1"
    local workflows_dir="$repo_dir/.github/workflows"
    
    if [[ ! -d "$workflows_dir" ]]; then
        log "No GitHub Actions workflows found in $workflows_dir"
        return 0
    fi
    
    log "Scanning workflows for secret references..."
    
    # Find all secret references in workflow files
    # Look for patterns like: secrets.SECRET_NAME, ${{ secrets.SECRET_NAME }}
    # Exclude GITHUB_ secrets as they are provided by GitHub automatically
    find "$workflows_dir" -name "*.yml" -o -name "*.yaml" | while read -r workflow_file; do
        grep -oE '\$\{\{\s*secrets\.[A-Z_][A-Z0-9_]*\s*\}\}|secrets\.[A-Z_][A-Z0-9_]*' "$workflow_file" 2>/dev/null || true
    done | sed -E 's/.*secrets\.([A-Z_][A-Z0-9_]*).*/\1/' | grep -v '^GITHUB_' | sort -u
}

get_existing_secrets() {
    local repo="$1"
    
    log "Checking existing secrets for repository $repo..."
    
    # Get list of existing secrets (names only)
    gh secret list --repo "$repo" --json name --jq '.[].name' 2>/dev/null || true
}

prompt_for_secret() {
    local secret_name="$1"
    local repo="$2"
    
    echo
    echo "Setting up secret: $secret_name"
    echo -n "Enter value for $secret_name (input will be hidden): "
    
    # Read secret value without echoing to terminal
    read -rs secret_value < /dev/tty
    echo
    
    if [[ -z "$secret_value" ]]; then
        echo "Skipping empty secret value for $secret_name"
        return 0
    fi
    
    log "Creating secret $secret_name..."
    
    if echo "$secret_value" | gh secret set "$secret_name" --repo "$repo"; then
        log "Successfully created secret: $secret_name"
    else
        error "Failed to create secret: $secret_name"
    fi
}

main() {
    if [[ $# -ne 1 ]]; then
        usage
        exit 1
    fi
    
    local repo_input="$1"
    
    if [[ "$repo_input" == "-h" || "$repo_input" == "--help" ]]; then
        usage
        exit 0
    fi
    
    check_requirements
    
    local repo
    repo="$(normalize_repo_url "$repo_input")"
    
    local clone_dir="$TEMP_DIR/repo"
    clone_repository "$repo" "$clone_dir"
    
    local secrets_found
    secrets_found="$(find_secrets_in_workflows "$clone_dir")"
    
    if [[ -z "$secrets_found" ]]; then
        log "No secrets found in GitHub Actions workflows"
        exit 0
    fi
    
    log "Found secrets in workflows:"
    echo "$secrets_found" | while read -r secret; do
        echo "  - $secret"
    done
    
    local existing_secrets
    existing_secrets="$(get_existing_secrets "$repo")"
    
    if [[ -n "$existing_secrets" ]]; then
        log "Existing secrets:"
        echo "$existing_secrets" | while read -r secret; do
            echo "  - $secret"
        done
    else
        log "No existing secrets found"
    fi
    
    echo
    log "Processing secrets..."
    
    local missing_count=0
    
    # Process secrets using a for loop to avoid stdin conflicts
    while IFS= read -r secret_name; do
        if [[ -z "$secret_name" ]]; then
            continue
        fi
        
        if echo "$existing_secrets" | grep -q "^$secret_name$"; then
            log "Secret $secret_name already exists, skipping"
        else
            prompt_for_secret "$secret_name" "$repo" < /dev/tty
            ((missing_count++))
        fi
    done <<< "$secrets_found"
    
    if [[ $missing_count -eq 0 ]]; then
        log "All secrets already exist. No action needed."
    else
        log "Successfully processed $missing_count missing secrets"
    fi
    
    log "Secret setup complete!"
}

main "$@"