diff --git a/scripts/deploy-standard-workflows.sh b/scripts/deploy-standard-workflows.sh index ff5832062..998034567 100755 --- a/scripts/deploy-standard-workflows.sh +++ b/scripts/deploy-standard-workflows.sh @@ -29,9 +29,23 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" STANDARDS_DIR="$REPO_ROOT/standards/workflows" -# Repos to never touch. +# Repos exempt from blanket standard-workflow deployment. +# .github — self-host source of truth; its own callers use local refs +# (e.g. add-to-project.yml pins ./.github/...), which a +# channel-pinned stub must never overwrite. +# .github-private — self-manages its workflow fleet (dev-lead runs inline, etc). +# A repo here may still opt into a *specific* workflow via SKIP_OVERRIDES below. SKIP_REPOS=(".github" ".github-private") +# Per-workflow opt-ins for otherwise-skipped repos. Keyed by workflow filename; +# value is a space-separated list of SKIP_REPOS that should still receive it. +# .github-private participates in the org Initiatives board, so it receives the +# add-to-project.yml caller (the org board is a single shared target — the stub +# is identical for every repo) while staying exempt from all other stubs. +declare -A SKIP_OVERRIDES=( + ["add-to-project.yml"]=".github-private" +) + # Workflows deployable from standards/workflows/ verbatim. # Excludes ci.yml, sonarcloud.yml (tech-stack-specific) and # feature-ideation.yml (requires repo-specific project_context input). @@ -43,6 +57,7 @@ DEPLOYABLE_WORKFLOWS=( dependabot-automerge.yml dependabot-rebase.yml dependency-audit.yml + add-to-project.yml ) # --------------------------------------------------------------------------- @@ -84,6 +99,25 @@ is_skipped_repo() { return 1 } +# True if a normally-skipped repo has opted into this specific workflow via +# SKIP_OVERRIDES — lets a self-managed repo receive one stub while staying +# exempt from the rest. +repo_opts_into() { + local repo="$1" workflow="$2" + local opt_in="${SKIP_OVERRIDES[$workflow]:-}" + [[ -z "$opt_in" ]] && return 1 + + # Split the space-separated opt-in list into an array (read -r -a avoids the + # pathname expansion an unquoted expansion would be subject to). + local -a allowed_repos + read -r -a allowed_repos <<< "$opt_in" + local r + for r in "${allowed_repos[@]}"; do + [[ "$repo" == "$r" ]] && return 0 + done + return 1 +} + # Fetch a file from the repo API in one call; outputs "shadecoded-content". # Returns empty string on 404. fetch_existing() { @@ -194,12 +228,27 @@ fi log "Deploying ${#WORKFLOWS[@]} workflow(s) to ${#REPOS[@]} repo(s)" for repo in "${REPOS[@]}"; do + # Fully-exempt repos (no per-workflow opt-in) skip with a single log line + # instead of one per workflow. if is_skipped_repo "$repo"; then - skip "$repo (exempt)" - continue + opts_in_any=false + for w in "${WORKFLOWS[@]}"; do + if repo_opts_into "$repo" "$w"; then + opts_in_any=true + break + fi + done + if [[ "$opts_in_any" == "false" ]]; then + skip "$repo (exempt)" + continue + fi fi for workflow in "${WORKFLOWS[@]}"; do + if is_skipped_repo "$repo" && ! repo_opts_into "$repo" "$workflow"; then + skip "$repo/$workflow (exempt)" + continue + fi deploy_workflow_to_repo "$repo" "$workflow" done done