@ebalzan/ci 0.0.0-stage → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +302 -2
- package/bin/_json.mjs +95 -0
- package/bin/_lib.sh +153 -0
- package/bin/cli.mjs +105 -0
- package/bin/gh-bootstrap.sh +622 -0
- package/bin/gh-reset.sh +148 -0
- package/bin/onboard.sh +340 -0
- package/bin/sync-managed.sh +81 -0
- package/config/actions.env +33 -0
- package/config/repo-settings.json +10 -0
- package/config/repos.env +25 -0
- package/config/rulesets/protect-dev.json +10 -0
- package/config/rulesets/protect-main.json +10 -0
- package/config/rulesets/require-pull-request.json +33 -0
- package/config/secrets.env.example +31 -0
- package/docs/ARCHITECTURE.md +477 -0
- package/docs/AUTO_WORK_ITEMS_WORKFLOW.md +305 -0
- package/docs/CI_FLOW.md +205 -0
- package/docs/CLI.md +229 -0
- package/docs/COMMIT_GUIDE.md +133 -0
- package/docs/CONFIGURATION.md +177 -0
- package/docs/GITHUB_ACTIONS_SETUP.md +315 -0
- package/docs/MIGRATION.md +175 -0
- package/docs/ONBOARDING.md +166 -0
- package/docs/REPO_RULES.md +116 -0
- package/managed/scripts/release/validate-conventional-subject.sh +32 -0
- package/package.json +66 -4
- package/renovate/tooling.json +45 -0
- package/templates/.github/actions.env +17 -0
- package/templates/.github/workflows/auto-pr.yml +33 -0
- package/templates/.github/workflows/pr-checks.yml +48 -0
- package/templates/.github/workflows/promote-dev-to-main.yml +26 -0
- package/templates/.github/workflows/release.yml +36 -0
- package/templates/.husky/commit-msg +18 -0
- package/templates/eslint.config.mjs +6 -0
- package/templates/renovate.json +4 -0
package/bin/gh-reset.sh
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# gh-reset.sh — delete GitHub Actions variables (and optionally secrets) on a repo.
|
|
4
|
+
#
|
|
5
|
+
# Lists what exists on the repo, shows it, and deletes it after a typed
|
|
6
|
+
# confirmation. Destructive and irreversible — use --dry-run first.
|
|
7
|
+
#
|
|
8
|
+
# Repository scope by default (organization-level config is never touched).
|
|
9
|
+
# By default only variables are removed; secret deletion must be requested
|
|
10
|
+
# explicitly. Settings and rulesets are not reset here — edit or delete them in
|
|
11
|
+
# the repo's Settings, or let `ebalzan-ci bootstrap --prune` remove managed
|
|
12
|
+
# rulesets no file defines.
|
|
13
|
+
#
|
|
14
|
+
# --env NAME switches to an ENVIRONMENT's config instead of the repository's.
|
|
15
|
+
# The two scopes are separate namespaces — a repo-scope run cannot see, list, or
|
|
16
|
+
# delete an environment's secrets, which is why clearing deploy credentials
|
|
17
|
+
# needs the flag:
|
|
18
|
+
#
|
|
19
|
+
# ebalzan-ci reset -R owner/repo --env production --secrets-only --dry-run
|
|
20
|
+
#
|
|
21
|
+
# Note that gh-bootstrap.sh strips a _FILE suffix when it sets a secret, so
|
|
22
|
+
# FOO_FILE in a dotenv file is named FOO on GitHub.
|
|
23
|
+
#
|
|
24
|
+
# Re-running the bootstrap after a reset restores the repo's variables and
|
|
25
|
+
# the merged shared + repo secrets. To remove only keys that no file defines any more, prefer
|
|
26
|
+
# `gh-bootstrap.sh --prune`, which works out the stale set for you.
|
|
27
|
+
#
|
|
28
|
+
# Usage:
|
|
29
|
+
# ebalzan-ci reset [options] # repo in the current directory
|
|
30
|
+
# ebalzan-ci reset -C ../my-repo [options]
|
|
31
|
+
# ebalzan-ci reset -R owner/repo [options] # no checkout needed
|
|
32
|
+
# bin/gh-reset.sh [options] # same, from a checkout of ebalzan/ci
|
|
33
|
+
#
|
|
34
|
+
# Options:
|
|
35
|
+
# -C, --repo-dir DIR Repo checkout whose remote picks the target (default: .)
|
|
36
|
+
# -R, --repo OWNER/REPO Target repo (default: detected from DIR's git remote)
|
|
37
|
+
# -e, --env NAME Target the environment NAME instead of the repository
|
|
38
|
+
# --include-secrets Also delete secrets (default: variables only)
|
|
39
|
+
# --secrets-only Delete secrets only, leave variables
|
|
40
|
+
# -m, --match REGEX Only delete names matching this extended regex
|
|
41
|
+
# -n, --dry-run List what would be deleted; delete nothing
|
|
42
|
+
# -y, --yes Skip the typed confirmation (dangerous)
|
|
43
|
+
# -h, --help Show this help
|
|
44
|
+
#
|
|
45
|
+
# Requirements: gh (GitHub CLI), authenticated with admin on the target repo.
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
|
|
48
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
49
|
+
# shellcheck source=_lib.sh
|
|
50
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
51
|
+
|
|
52
|
+
# ---- args --------------------------------------------------------------------
|
|
53
|
+
REPO=""
|
|
54
|
+
REPO_DIR="."
|
|
55
|
+
ENVIRONMENT=""
|
|
56
|
+
DO_VARS=true
|
|
57
|
+
DO_SECRETS=false
|
|
58
|
+
MATCH=""
|
|
59
|
+
DRY_RUN=false
|
|
60
|
+
ASSUME_YES=false
|
|
61
|
+
while [[ $# -gt 0 ]]; do
|
|
62
|
+
case "$1" in
|
|
63
|
+
-C|--repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
|
|
64
|
+
-R|--repo) REPO="${2:-}"; shift 2 ;;
|
|
65
|
+
-e|--env) ENVIRONMENT="${2:-}"; shift 2 ;;
|
|
66
|
+
--include-secrets) DO_SECRETS=true; shift ;;
|
|
67
|
+
--secrets-only) DO_SECRETS=true; DO_VARS=false; shift ;;
|
|
68
|
+
-m|--match) MATCH="${2:-}"; shift 2 ;;
|
|
69
|
+
-n|--dry-run) DRY_RUN=true; shift ;;
|
|
70
|
+
-y|--yes) ASSUME_YES=true; shift ;;
|
|
71
|
+
-h|--help) print_help "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
72
|
+
*) die "Unknown option: $1 (try --help)" ;;
|
|
73
|
+
esac
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
# ---- preconditions & targets -------------------------------------------------
|
|
77
|
+
require_gh
|
|
78
|
+
require_auth
|
|
79
|
+
[[ -n "$REPO" ]] || REPO="$(detect_repo "$REPO_DIR")"
|
|
80
|
+
[[ -n "$REPO" ]] || die "Could not determine the repo. Pass --repo OWNER/REPO."
|
|
81
|
+
|
|
82
|
+
# Repo scope unless --env was given. Every gh call takes these flags, so the two
|
|
83
|
+
# scopes can never be mixed up halfway through a run.
|
|
84
|
+
GH_SCOPE=(--repo "$REPO")
|
|
85
|
+
if [[ -n "$ENVIRONMENT" ]]; then
|
|
86
|
+
gh api "repos/$REPO/environments/$ENVIRONMENT" >/dev/null 2>&1 \
|
|
87
|
+
|| die "Environment '$ENVIRONMENT' does not exist on $REPO."
|
|
88
|
+
GH_SCOPE+=(--env "$ENVIRONMENT")
|
|
89
|
+
fi
|
|
90
|
+
SCOPE_LABEL="$REPO${ENVIRONMENT:+ · environment: $ENVIRONMENT}"
|
|
91
|
+
|
|
92
|
+
# List the names of a kind (variable|secret) in scope, filtered by --match.
|
|
93
|
+
list_names() { # list_names KIND
|
|
94
|
+
local names
|
|
95
|
+
names="$(gh "$1" list "${GH_SCOPE[@]}" --json name --jq '.[].name' 2>/dev/null || true)"
|
|
96
|
+
[[ -n "$MATCH" ]] && names="$(printf '%s\n' "$names" | grep -E "$MATCH" || true)"
|
|
97
|
+
printf '%s' "$names"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ---- one kind batch ----------------------------------------------------------
|
|
101
|
+
reset_batch() { # reset_batch KIND (KIND = variable|secret)
|
|
102
|
+
local kind="$1" names count
|
|
103
|
+
names="$(list_names "$kind")"
|
|
104
|
+
count="$(printf '%s\n' "$names" | grep -c . || true)"
|
|
105
|
+
|
|
106
|
+
if [[ "$count" -eq 0 ]]; then
|
|
107
|
+
info " ${DIM}no ${kind}s${MATCH:+ matching /$MATCH/}${RESET}"
|
|
108
|
+
return
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
step "${kind}s to delete from ${BOLD}$SCOPE_LABEL${RESET} ($count)"
|
|
112
|
+
printf ' - %s\n' $names
|
|
113
|
+
|
|
114
|
+
if $DRY_RUN; then return; fi
|
|
115
|
+
|
|
116
|
+
if ! $ASSUME_YES; then
|
|
117
|
+
confirm_typed "$REPO" \
|
|
118
|
+
"Type '${BOLD}$REPO${RESET}' to permanently delete these $count ${kind}(s) from ${BOLD}$SCOPE_LABEL${RESET}:" \
|
|
119
|
+
|| { warn "Skipped ${kind}s (confirmation did not match)."; return; }
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
local name
|
|
123
|
+
while IFS= read -r name; do
|
|
124
|
+
[[ -n "$name" ]] || continue
|
|
125
|
+
gh "$kind" delete "$name" "${GH_SCOPE[@]}" && ok "deleted $kind $name"
|
|
126
|
+
done <<< "$names"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ---- execute -----------------------------------------------------------------
|
|
130
|
+
info ""
|
|
131
|
+
info "${BOLD}Reset GitHub Actions config${RESET} ${DIM}repo: $SCOPE_LABEL${MATCH:+ · match: /$MATCH/}${DRY_RUN:+ · dry-run}${RESET}"
|
|
132
|
+
info ""
|
|
133
|
+
|
|
134
|
+
# An environment holds secrets far more often than variables, and the default
|
|
135
|
+
# is variables-only — so say so rather than reporting an empty run as success.
|
|
136
|
+
if [[ -n "$ENVIRONMENT" ]] && ! $DO_SECRETS; then
|
|
137
|
+
warn "Only variables are in scope. Environment secrets need --secrets-only (or --include-secrets)."
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
if $DO_VARS; then reset_batch variable; fi
|
|
141
|
+
if $DO_SECRETS; then reset_batch secret; fi
|
|
142
|
+
|
|
143
|
+
info ""
|
|
144
|
+
if $DRY_RUN; then
|
|
145
|
+
ok "Dry run complete — nothing was deleted."
|
|
146
|
+
else
|
|
147
|
+
ok "${BOLD}Reset complete.${RESET}"
|
|
148
|
+
fi
|
package/bin/onboard.sh
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# onboard.sh — bring a repo under the shared rules in one step.
|
|
4
|
+
#
|
|
5
|
+
# In the repo's checkout it:
|
|
6
|
+
# 1. creates a branch (ci/onboard-ebalzan-ci) from dev — or from the default
|
|
7
|
+
# branch when dev does not exist yet;
|
|
8
|
+
# 2. writes the templates/ files: the four thin workflow callers, a starter
|
|
9
|
+
# .github/actions.env, renovate.json and, for a Node repo, the husky
|
|
10
|
+
# commit-msg hook. A file that already exists is kept (--force replaces it);
|
|
11
|
+
# 3. adds the .github/secrets*.env files to .gitignore when they are not
|
|
12
|
+
# ignored yet (before any secret file exists);
|
|
13
|
+
# 4. for a Node repo (package.json): installs this package and husky as
|
|
14
|
+
# devDependencies, sets `prepare: husky` when no prepare script exists,
|
|
15
|
+
# and wires the code-style rules of @ebalzan/config — package.json
|
|
16
|
+
# "prettier" and an eslint.config.mjs — where the repo has no Prettier or
|
|
17
|
+
# ESLint config of its own (--no-config skips this);
|
|
18
|
+
# 5. commits it all as `ci: onboard <ci repo>`.
|
|
19
|
+
# Then, unless --no-push:
|
|
20
|
+
# 6. creates the dev branch on GitHub if it is missing;
|
|
21
|
+
# 7. tags the repo with REPOS_TOPIC (config/repos.env), so `--all` finds it;
|
|
22
|
+
# 8. runs the bootstrap (variables, secrets, settings, rulesets);
|
|
23
|
+
# 9. pushes the branch. The auto-pr.yml it adds runs on that very push and
|
|
24
|
+
# opens the tracking issue and the PR into dev. Review and squash-merge it.
|
|
25
|
+
#
|
|
26
|
+
# Usage:
|
|
27
|
+
# ebalzan-ci onboard [options] # the repo in the current directory
|
|
28
|
+
# ebalzan-ci onboard -C ../my-repo [options]
|
|
29
|
+
#
|
|
30
|
+
# Options:
|
|
31
|
+
# -C, --repo-dir DIR Checkout of the repo to onboard (default: .)
|
|
32
|
+
# -R, --repo OWNER/REPO Its slug (default: from DIR's git remote)
|
|
33
|
+
# --runs-on LABEL Runner label for every job (default: ubuntu-latest)
|
|
34
|
+
# --ref REF Ref the callers pin (default: v<major of this package>)
|
|
35
|
+
# --branch NAME Branch to commit on (default: ci/onboard-ebalzan-ci)
|
|
36
|
+
# --force Overwrite files that already exist
|
|
37
|
+
# --no-npm Skip the npm installs even when package.json exists
|
|
38
|
+
# --no-config Do not wire @ebalzan/config (Prettier, ESLint)
|
|
39
|
+
# --no-push Stop after the local commit (no GitHub changes at all)
|
|
40
|
+
# -n, --dry-run Show what would happen; change nothing
|
|
41
|
+
# -y, --yes Skip the confirmations (also for the bootstrap)
|
|
42
|
+
# -h, --help Show this help
|
|
43
|
+
#
|
|
44
|
+
# Requirements: git; gh authenticated with admin on the repo (not needed with
|
|
45
|
+
# --no-push or --dry-run); npm for a Node repo.
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
|
|
48
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
49
|
+
CI_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
50
|
+
# shellcheck source=_lib.sh
|
|
51
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
52
|
+
|
|
53
|
+
TEMPLATES_DIR="$CI_ROOT/templates"
|
|
54
|
+
REPOS_FILE="$CI_ROOT/config/repos.env"
|
|
55
|
+
CI_REPO="$(unquote "$(env_get "$REPOS_FILE" CI_REPO)")"
|
|
56
|
+
CI_PACKAGE="$(node "$SCRIPT_DIR/_json.mjs" get "$CI_ROOT/package.json" name)"
|
|
57
|
+
CI_VERSION="$(node "$SCRIPT_DIR/_json.mjs" get "$CI_ROOT/package.json" version)"
|
|
58
|
+
TOPIC="$(unquote "$(env_get "$REPOS_FILE" REPOS_TOPIC)")"
|
|
59
|
+
|
|
60
|
+
# ---- args --------------------------------------------------------------------
|
|
61
|
+
REPO_DIR="."
|
|
62
|
+
REPO=""
|
|
63
|
+
RUNS_ON="ubuntu-latest"
|
|
64
|
+
REF="v${CI_VERSION%%.*}"
|
|
65
|
+
BRANCH="ci/onboard-ebalzan-ci"
|
|
66
|
+
FORCE=false
|
|
67
|
+
NPM=true
|
|
68
|
+
CONFIG=true
|
|
69
|
+
CONFIG_PACKAGE="@ebalzan/config"
|
|
70
|
+
PUSH=true
|
|
71
|
+
DRY_RUN=false
|
|
72
|
+
ASSUME_YES=false
|
|
73
|
+
while [[ $# -gt 0 ]]; do
|
|
74
|
+
case "$1" in
|
|
75
|
+
-C|--repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
|
|
76
|
+
-R|--repo) REPO="${2:-}"; shift 2 ;;
|
|
77
|
+
--runs-on) RUNS_ON="${2:-}"; shift 2 ;;
|
|
78
|
+
--ref) REF="${2:-}"; shift 2 ;;
|
|
79
|
+
--branch) BRANCH="${2:-}"; shift 2 ;;
|
|
80
|
+
--force) FORCE=true; shift ;;
|
|
81
|
+
--no-npm) NPM=false; shift ;;
|
|
82
|
+
--no-config) CONFIG=false; shift ;;
|
|
83
|
+
--no-push) PUSH=false; shift ;;
|
|
84
|
+
-n|--dry-run) DRY_RUN=true; shift ;;
|
|
85
|
+
-y|--yes) ASSUME_YES=true; shift ;;
|
|
86
|
+
-h|--help) print_help "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
87
|
+
*) die "Unknown option: $1 (try --help)" ;;
|
|
88
|
+
esac
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
[[ -n "$CI_REPO" ]] || die "CI_REPO is not set in $REPOS_FILE."
|
|
92
|
+
[[ -n "$RUNS_ON" && -n "$REF" && -n "$BRANCH" ]] || die "--runs-on, --ref and --branch need a value."
|
|
93
|
+
|
|
94
|
+
# ---- preconditions -----------------------------------------------------------
|
|
95
|
+
[[ -d "$REPO_DIR/.git" || -f "$REPO_DIR/.git" ]] || die "$REPO_DIR is not a git checkout."
|
|
96
|
+
REPO_DIR="$(cd "$REPO_DIR" && pwd)"
|
|
97
|
+
git_() { git -C "$REPO_DIR" "$@"; }
|
|
98
|
+
|
|
99
|
+
if $PUSH && ! $DRY_RUN; then
|
|
100
|
+
require_gh
|
|
101
|
+
require_auth
|
|
102
|
+
fi
|
|
103
|
+
if [[ -z "$REPO" ]] && command -v gh >/dev/null 2>&1; then REPO="$(detect_repo "$REPO_DIR")"; fi
|
|
104
|
+
if $PUSH && [[ -z "$REPO" ]]; then
|
|
105
|
+
die "Could not determine the repo for $REPO_DIR. Pass --repo OWNER/REPO (or --no-push)."
|
|
106
|
+
fi
|
|
107
|
+
if [[ -n "$(git_ status --porcelain)" ]] && ! $DRY_RUN; then
|
|
108
|
+
die "$REPO_DIR has uncommitted changes. Commit or stash them first."
|
|
109
|
+
fi
|
|
110
|
+
if git_ rev-parse --verify --quiet "refs/heads/$BRANCH" >/dev/null; then
|
|
111
|
+
die "Branch $BRANCH already exists in $REPO_DIR. Delete it or pass --branch."
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Branch point: dev if it exists, else the default branch, else HEAD (no remote).
|
|
115
|
+
HAS_ORIGIN=false
|
|
116
|
+
git_ remote get-url origin >/dev/null 2>&1 && HAS_ORIGIN=true
|
|
117
|
+
if $HAS_ORIGIN && ! $DRY_RUN; then git_ fetch --quiet origin || warn "git fetch origin failed; using local refs."; fi
|
|
118
|
+
DEV_EXISTS=false
|
|
119
|
+
if $HAS_ORIGIN && git_ rev-parse --verify --quiet refs/remotes/origin/dev >/dev/null; then
|
|
120
|
+
DEV_EXISTS=true; BASE_REF="origin/dev"
|
|
121
|
+
elif $HAS_ORIGIN && git_ rev-parse --verify --quiet refs/remotes/origin/HEAD >/dev/null; then
|
|
122
|
+
BASE_REF="$(git_ symbolic-ref --short refs/remotes/origin/HEAD)"
|
|
123
|
+
else
|
|
124
|
+
BASE_REF="HEAD"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
IS_NODE=false
|
|
128
|
+
[[ -f "$REPO_DIR/package.json" ]] && IS_NODE=true
|
|
129
|
+
$IS_NODE || CONFIG=false
|
|
130
|
+
|
|
131
|
+
# present FILE... — the given repo-relative paths that exist, space-separated.
|
|
132
|
+
present() {
|
|
133
|
+
local f out=""
|
|
134
|
+
for f in "$@"; do [[ -e "$REPO_DIR/$f" ]] && out+="$f "; done
|
|
135
|
+
printf '%s' "${out% }"
|
|
136
|
+
}
|
|
137
|
+
PRETTIER_FILES=(.prettierrc .prettierrc.json .prettierrc.json5 .prettierrc.yaml .prettierrc.yml .prettierrc.toml
|
|
138
|
+
.prettierrc.js .prettierrc.cjs .prettierrc.mjs .prettierrc.ts prettier.config.js prettier.config.cjs
|
|
139
|
+
prettier.config.mjs prettier.config.ts)
|
|
140
|
+
ESLINT_FILES=(eslint.config.js eslint.config.cjs eslint.config.mjs eslint.config.ts eslint.config.mts
|
|
141
|
+
eslint.config.cts .eslintrc .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yaml .eslintrc.yml)
|
|
142
|
+
|
|
143
|
+
# ---- plan --------------------------------------------------------------------
|
|
144
|
+
# applies REL — whether template REL belongs in this repo at all; when it does
|
|
145
|
+
# not, SKIP_REASON says why.
|
|
146
|
+
SKIP_REASON=""
|
|
147
|
+
applies() {
|
|
148
|
+
local f
|
|
149
|
+
case "$1" in
|
|
150
|
+
.husky/*)
|
|
151
|
+
$IS_NODE || { SKIP_REASON="no package.json"; return 1; } ;;
|
|
152
|
+
eslint.config.mjs)
|
|
153
|
+
$CONFIG || { SKIP_REASON="$($IS_NODE && echo '--no-config' || echo 'no package.json')"; return 1; }
|
|
154
|
+
# A repo with its own ESLint config extends the preset there; never add a second one.
|
|
155
|
+
f="$(present "${ESLINT_FILES[@]}")"
|
|
156
|
+
if [[ -n "$f" && "$f" != eslint.config.mjs ]]; then SKIP_REASON="repo has $f — extend $CONFIG_PACKAGE/eslint/node there"; return 1; fi ;;
|
|
157
|
+
renovate.json)
|
|
158
|
+
# Any existing Renovate config is the repo's own; extend the preset there.
|
|
159
|
+
for f in renovate.json renovate.json5 .renovaterc .renovaterc.json .github/renovate.json .github/renovate.json5; do
|
|
160
|
+
if [[ -e "$REPO_DIR/$f" ]] && ! $FORCE; then SKIP_REASON="repo has its own Renovate config"; return 1; fi
|
|
161
|
+
done ;;
|
|
162
|
+
esac
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Secrets files must be ignored before the first one exists.
|
|
167
|
+
GITIGNORE=false
|
|
168
|
+
if ! git_ check-ignore -q .github/secrets.env || ! git_ check-ignore -q .github/secrets.production.env; then
|
|
169
|
+
GITIGNORE=true
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Prettier: point package.json at the shared config unless the repo has its own.
|
|
173
|
+
PRETTIER_SET=false
|
|
174
|
+
PRETTIER_OWN=""
|
|
175
|
+
if $CONFIG; then
|
|
176
|
+
PRETTIER_OWN="$(present "${PRETTIER_FILES[@]}")"
|
|
177
|
+
if [[ -z "$PRETTIER_OWN" ]] && [[ "$(cd "$REPO_DIR" && npm pkg get prettier)" == "{}" ]]; then
|
|
178
|
+
PRETTIER_SET=true
|
|
179
|
+
elif [[ -z "$PRETTIER_OWN" ]]; then
|
|
180
|
+
PRETTIER_OWN="package.json \"prettier\""
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
# TypeScript is reported, never edited: tsconfig files are JSON with comments.
|
|
184
|
+
TSCONFIG_NOTE=""
|
|
185
|
+
if $CONFIG && [[ -f "$REPO_DIR/tsconfig.json" ]] && ! grep -q "$CONFIG_PACKAGE/tsconfig/" "$REPO_DIR/tsconfig.json"; then
|
|
186
|
+
TSCONFIG_NOTE="tsconfig.json does not extend $CONFIG_PACKAGE/tsconfig/*.json — add \"extends\" by hand"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
PLAN=() # "action<TAB>rel" with action = create | overwrite | keep | skip
|
|
190
|
+
while IFS= read -r rel; do
|
|
191
|
+
if ! applies "$rel"; then PLAN+=("skip $rel $SKIP_REASON")
|
|
192
|
+
elif [[ -e "$REPO_DIR/$rel" ]]; then
|
|
193
|
+
if $FORCE; then PLAN+=("overwrite $rel"); else PLAN+=("keep $rel"); fi
|
|
194
|
+
else PLAN+=("create $rel")
|
|
195
|
+
fi
|
|
196
|
+
done < <(cd "$TEMPLATES_DIR" && find . -type f | sed 's#^\./##' | sort)
|
|
197
|
+
|
|
198
|
+
info ""
|
|
199
|
+
info "${BOLD}Onboard${RESET} ${DIM}${REPO:-$(basename "$REPO_DIR")} ← $CI_REPO@$REF (runs-on: $RUNS_ON)${RESET}"
|
|
200
|
+
info ""
|
|
201
|
+
info " ${BOLD}files${RESET} ${DIM}(branch $BRANCH from $BASE_REF)${RESET}"
|
|
202
|
+
for entry in "${PLAN[@]}"; do
|
|
203
|
+
IFS=' ' read -r action rel reason <<< "$entry"
|
|
204
|
+
case "$action" in
|
|
205
|
+
create|overwrite) printf ' %-10s %s\n' "$action" "$rel" ;;
|
|
206
|
+
keep) printf ' %s%-10s %s (exists — --force replaces it)%s\n' "$DIM" keep "$rel" "$RESET" ;;
|
|
207
|
+
skip) printf ' %s%-10s %s (%s)%s\n' "$DIM" skip "$rel" "$reason" "$RESET" ;;
|
|
208
|
+
esac
|
|
209
|
+
done
|
|
210
|
+
$GITIGNORE && printf ' %-10s %s\n' update ".gitignore (ignore .github/secrets*.env)"
|
|
211
|
+
$PRETTIER_SET && printf ' %-10s %s\n' update "package.json \"prettier\": \"$CONFIG_PACKAGE/prettier\""
|
|
212
|
+
[[ -z "$PRETTIER_OWN" ]] || info " ${DIM}keep $PRETTIER_OWN (own Prettier config — extend $CONFIG_PACKAGE/prettier there)${RESET}"
|
|
213
|
+
[[ -z "$TSCONFIG_NOTE" ]] || info " ${DIM}note $TSCONFIG_NOTE${RESET}"
|
|
214
|
+
if $IS_NODE && $NPM; then
|
|
215
|
+
info " ${BOLD}npm${RESET}"
|
|
216
|
+
info " install --save-dev $CI_PACKAGE@^${CI_VERSION%%.*}.0.0 (and husky, if missing)"
|
|
217
|
+
! $CONFIG || info " install --save-dev --save-exact $CONFIG_PACKAGE (and eslint, prettier, if missing)"
|
|
218
|
+
fi
|
|
219
|
+
if $PUSH; then
|
|
220
|
+
info " ${BOLD}GitHub${RESET} ${DIM}($REPO)${RESET}"
|
|
221
|
+
$DEV_EXISTS || info " create branch dev from $BASE_REF"
|
|
222
|
+
[[ -z "$TOPIC" ]] || info " add topic '$TOPIC'"
|
|
223
|
+
info " bootstrap: variables, secrets, settings, rulesets"
|
|
224
|
+
info " push $BRANCH → auto-pr.yml opens the PR into dev"
|
|
225
|
+
else
|
|
226
|
+
info " ${DIM}--no-push: GitHub is left untouched${RESET}"
|
|
227
|
+
fi
|
|
228
|
+
info ""
|
|
229
|
+
|
|
230
|
+
if $DRY_RUN; then
|
|
231
|
+
ok "Dry run complete — nothing was changed."
|
|
232
|
+
exit 0
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# ---- local changes -----------------------------------------------------------
|
|
236
|
+
git_ switch --quiet --no-track -c "$BRANCH" "$BASE_REF"
|
|
237
|
+
|
|
238
|
+
render() { # render REL — template → repo, placeholders filled in
|
|
239
|
+
local src="$TEMPLATES_DIR/$1" dest="$REPO_DIR/$1"
|
|
240
|
+
mkdir -p "$(dirname "$dest")"
|
|
241
|
+
sed -e "s#__CI_REPO__#$CI_REPO#g" \
|
|
242
|
+
-e "s#__CI_REPO_NAME__#${CI_REPO#*/}#g" \
|
|
243
|
+
-e "s#__CI_REF__#$REF#g" \
|
|
244
|
+
-e "s#__CI_PACKAGE__#$CI_PACKAGE#g" \
|
|
245
|
+
-e "s#__RUNS_ON__#$RUNS_ON#g" "$src" > "$dest"
|
|
246
|
+
[[ ! -x "$src" && "$1" != .husky/* ]] || chmod +x "$dest"
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for entry in "${PLAN[@]}"; do
|
|
250
|
+
IFS=' ' read -r action rel reason <<< "$entry"
|
|
251
|
+
case "$action" in create|overwrite) render "$rel"; ok "$action $rel" ;; esac
|
|
252
|
+
done
|
|
253
|
+
|
|
254
|
+
if $GITIGNORE; then
|
|
255
|
+
[[ ! -s "$REPO_DIR/.gitignore" ]] || [[ -z "$(tail -c1 "$REPO_DIR/.gitignore")" ]] || printf '\n' >> "$REPO_DIR/.gitignore"
|
|
256
|
+
cat >> "$REPO_DIR/.gitignore" <<'GITIGNORE'
|
|
257
|
+
|
|
258
|
+
# Actions secrets, synced by `ebalzan-ci bootstrap`. Never commit them.
|
|
259
|
+
/.github/secrets.env
|
|
260
|
+
/.github/secrets.*.env
|
|
261
|
+
!/.github/secrets*.env.example
|
|
262
|
+
GITIGNORE
|
|
263
|
+
ok "update .gitignore"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
if $PRETTIER_SET; then
|
|
267
|
+
(cd "$REPO_DIR" && npm pkg set "prettier=$CONFIG_PACKAGE/prettier") && ok "set package.json \"prettier\""
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
if $IS_NODE && $NPM; then
|
|
271
|
+
step "Installing devDependencies"
|
|
272
|
+
(cd "$REPO_DIR" && npm install --save-dev --no-audit --no-fund "$CI_PACKAGE@^${CI_VERSION%%.*}.0.0") \
|
|
273
|
+
|| warn "Could not install $CI_PACKAGE (is it published?). The hook skips the local check until it is installed."
|
|
274
|
+
if ! (cd "$REPO_DIR" && node -e 'const p=require("./package.json");process.exit((p.devDependencies||{}).husky||(p.dependencies||{}).husky?0:1)'); then
|
|
275
|
+
(cd "$REPO_DIR" && npm install --save-dev --no-audit --no-fund husky) || warn "Could not install husky."
|
|
276
|
+
fi
|
|
277
|
+
if [[ "$(cd "$REPO_DIR" && npm pkg get scripts.prepare)" == "{}" ]]; then
|
|
278
|
+
(cd "$REPO_DIR" && npm pkg set scripts.prepare=husky) && ok "set scripts.prepare = husky"
|
|
279
|
+
fi
|
|
280
|
+
if $CONFIG; then
|
|
281
|
+
# Its peers are pinned by the repo, so add the ones it does not list yet.
|
|
282
|
+
config_pkgs=("$CONFIG_PACKAGE")
|
|
283
|
+
for peer in eslint prettier; do
|
|
284
|
+
(cd "$REPO_DIR" && node -e 'const p=require("./package.json");process.exit({...p.dependencies,...p.devDependencies}[process.argv[1]]?0:1)' "$peer") \
|
|
285
|
+
|| config_pkgs+=("$peer")
|
|
286
|
+
done
|
|
287
|
+
(cd "$REPO_DIR" && npm install --save-dev --save-exact --no-audit --no-fund "${config_pkgs[@]}") \
|
|
288
|
+
|| warn "Could not install ${config_pkgs[*]}. Install them before running Prettier or ESLint."
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
git_ add -A
|
|
293
|
+
if git_ diff --cached --quiet; then
|
|
294
|
+
warn "Nothing to commit — every file already exists. Use --force to replace them."
|
|
295
|
+
git_ switch --quiet -
|
|
296
|
+
git_ branch --quiet -D "$BRANCH"
|
|
297
|
+
exit 0
|
|
298
|
+
fi
|
|
299
|
+
git_ commit --quiet -m "ci: onboard $CI_REPO shared workflows and rules"
|
|
300
|
+
ok "Committed on $BRANCH: $(git_ log -1 --pretty=%s)"
|
|
301
|
+
|
|
302
|
+
if ! $PUSH; then
|
|
303
|
+
info ""
|
|
304
|
+
ok "${BOLD}Local onboarding done.${RESET} Review with: git -C $REPO_DIR show --stat"
|
|
305
|
+
info "Then push it yourself, or re-run without --no-push after deleting the branch."
|
|
306
|
+
exit 0
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# ---- GitHub ------------------------------------------------------------------
|
|
310
|
+
if ! $ASSUME_YES; then
|
|
311
|
+
confirm "Apply the GitHub steps above to $REPO and push $BRANCH?" || die "Stopped. The local commit on $BRANCH is kept."
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
if ! $DEV_EXISTS; then
|
|
315
|
+
default_branch="$(gh repo view "$REPO" --json defaultBranchRef -q .defaultBranchRef.name)"
|
|
316
|
+
sha="$(gh api "repos/$REPO/commits/$default_branch" -q .sha)"
|
|
317
|
+
gh api -X POST "repos/$REPO/git/refs" -f ref=refs/heads/dev -f sha="$sha" >/dev/null
|
|
318
|
+
ok "created branch dev from $default_branch (${sha:0:7})"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
if [[ -n "$TOPIC" ]]; then
|
|
322
|
+
gh repo edit "$REPO" --add-topic "$TOPIC" >/dev/null && ok "added topic '$TOPIC'"
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
BOOTSTRAP_ARGS=(--repo-dir "$REPO_DIR" --repo "$REPO")
|
|
326
|
+
$ASSUME_YES && BOOTSTRAP_ARGS+=(--yes)
|
|
327
|
+
"$SCRIPT_DIR/gh-bootstrap.sh" "${BOOTSTRAP_ARGS[@]}" \
|
|
328
|
+
|| warn "Bootstrap did not finish; fix the cause and re-run: ebalzan-ci bootstrap -C $REPO_DIR"
|
|
329
|
+
|
|
330
|
+
git_ push --quiet -u origin "$BRANCH"
|
|
331
|
+
ok "pushed $BRANCH"
|
|
332
|
+
|
|
333
|
+
info ""
|
|
334
|
+
ok "${BOLD}$REPO is onboarded.${RESET}"
|
|
335
|
+
info " 1. The PR into dev opens within a minute (auto-pr.yml). If it does not:"
|
|
336
|
+
info " https://github.com/$REPO/compare/dev...$BRANCH"
|
|
337
|
+
info " 2. Add the repo's own checks to .github/workflows/pr-checks.yml and its"
|
|
338
|
+
info " ship job to release.yml (both have a commented example)."
|
|
339
|
+
[[ -z "$TSCONFIG_NOTE" ]] || info " · $TSCONFIG_NOTE"
|
|
340
|
+
info " 3. Squash-merge the PR, then the dev → main PR: the first release."
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# sync-managed.sh — refresh repos' local copies of ebalzan/ci's managed/ files.
|
|
4
|
+
#
|
|
5
|
+
# DEPRECATED since 2.0. managed/ holds files a repo's local tooling runs —
|
|
6
|
+
# today only the Conventional Commits validator behind the husky commit-msg
|
|
7
|
+
# hook. Repos now run it from the npm package (`ebalzan-ci commit-msg`, see
|
|
8
|
+
# templates/.husky/commit-msg) and keep NO copy. This script remains for repos
|
|
9
|
+
# that still keep a verbatim copy at <repo>/<path>: it keeps those copies
|
|
10
|
+
# current until they move to the package. The PR-title check
|
|
11
|
+
# (actions/pr-title) fails on any copy that differs.
|
|
12
|
+
#
|
|
13
|
+
# Only copies that already exist are updated; --install creates missing ones.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# ebalzan-ci sync-managed [-C DIR | --all] [--check | --install]
|
|
17
|
+
#
|
|
18
|
+
# Options:
|
|
19
|
+
# -C, --repo-dir DIR Repo checkout to update (default: current directory)
|
|
20
|
+
# -a, --all Every repo config/repos.env selects that has a local
|
|
21
|
+
# checkout under $CI_REPOS_ROOT
|
|
22
|
+
# --check Report differing copies; change nothing (exit 1 on drift)
|
|
23
|
+
# --install Also create copies the repo does not have yet
|
|
24
|
+
# -h, --help Show this help
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
CI_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
29
|
+
# shellcheck source=_lib.sh
|
|
30
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
31
|
+
MANAGED_DIR="$CI_ROOT/managed"
|
|
32
|
+
|
|
33
|
+
REPO_DIRS=()
|
|
34
|
+
ALL=false
|
|
35
|
+
CHECK=false
|
|
36
|
+
INSTALL=false
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
-C|--repo-dir) REPO_DIRS+=("${2:-}"); shift 2 ;;
|
|
40
|
+
-a|--all) ALL=true; shift ;;
|
|
41
|
+
--check) CHECK=true; shift ;;
|
|
42
|
+
--install) INSTALL=true; shift ;;
|
|
43
|
+
-h|--help) print_help "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
44
|
+
*) die "Unknown option: $1 (try --help)" ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
if $ALL; then
|
|
49
|
+
require_gh
|
|
50
|
+
require_auth
|
|
51
|
+
for slug in $(managed_repos "$CI_ROOT/config/repos.env" "$CI_ROOT"); do
|
|
52
|
+
dir="$(local_checkout "$slug" "$CI_ROOT")"
|
|
53
|
+
if [[ -n "$dir" ]]; then REPO_DIRS+=("$dir"); else info "${DIM}$slug — no local checkout, skipped${RESET}"; fi
|
|
54
|
+
done
|
|
55
|
+
fi
|
|
56
|
+
[[ ${#REPO_DIRS[@]} -gt 0 ]] || REPO_DIRS=(".")
|
|
57
|
+
|
|
58
|
+
drift=0
|
|
59
|
+
for dir in "${REPO_DIRS[@]}"; do
|
|
60
|
+
[[ -d "$dir" ]] || { warn "No checkout at $dir — skipped."; drift=1; continue; }
|
|
61
|
+
step "$(basename "$(cd "$dir" && pwd)")"
|
|
62
|
+
while IFS= read -r rel; do
|
|
63
|
+
target="$dir/$rel"
|
|
64
|
+
if [[ ! -f "$target" ]] && ! $INSTALL; then
|
|
65
|
+
info " ${DIM}$rel — no local copy (the hook runs it from the package)${RESET}"; continue
|
|
66
|
+
fi
|
|
67
|
+
if [[ -f "$target" ]] && cmp -s "$MANAGED_DIR/$rel" "$target"; then
|
|
68
|
+
ok "$rel up to date"; continue
|
|
69
|
+
fi
|
|
70
|
+
if $CHECK; then
|
|
71
|
+
warn "$rel differs"; drift=1; continue
|
|
72
|
+
fi
|
|
73
|
+
mkdir -p "$(dirname "$target")"
|
|
74
|
+
cp "$MANAGED_DIR/$rel" "$target"
|
|
75
|
+
chmod +x "$target"
|
|
76
|
+
ok "$rel updated"
|
|
77
|
+
done < <(cd "$MANAGED_DIR" && find . -type f | sed 's#^\./##' | sort)
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
if $CHECK && [[ "$drift" -ne 0 ]]; then die "Managed files out of date — run sync-managed without --check."; fi
|
|
81
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SHARED GitHub Actions VARIABLES — the house rules every repo calling the
|
|
2
|
+
# shared workflows gets. Non-sensitive; committed; published with the package.
|
|
3
|
+
#
|
|
4
|
+
# A repo adds or overrides any key in its own .github/actions.env; the repo's
|
|
5
|
+
# value wins. Keep here only what is genuinely the same everywhere — anything
|
|
6
|
+
# tied to one project (AUTO_PROJECT_NUMBER, AUTO_MILESTONE_TITLE, deploy
|
|
7
|
+
# settings) belongs to that repo.
|
|
8
|
+
#
|
|
9
|
+
# Read at run time, never copied onto repos: the actions load this file from the
|
|
10
|
+
# copy of ebalzan/ci the runner downloads for `uses: …@v2`
|
|
11
|
+
# (actions/auto-work-items/src/sharedVars.ts) and fill every key the repo leaves
|
|
12
|
+
# unset. A change here reaches the repos when it is released and the major tag
|
|
13
|
+
# moves to it — no bootstrap run.
|
|
14
|
+
#
|
|
15
|
+
# Only values that differ from the code defaults live here
|
|
16
|
+
# (actions/auto-work-items/src/config.ts, describe.ts, project.ts): priority
|
|
17
|
+
# comes from TYPE_CONFIG, size defaults to M, estimate to 1, dates to
|
|
18
|
+
# today → +7 days, strict metadata on, the board owner is the repo owner, and
|
|
19
|
+
# no board or milestone unless the repo sets one. The workflows pass the repo's
|
|
20
|
+
# variables whole (REPO_VARS_JSON), so setting a key here or in a repo is all it
|
|
21
|
+
# takes — no workflow edit.
|
|
22
|
+
#
|
|
23
|
+
# Full key reference: docs/CONFIGURATION.md.
|
|
24
|
+
|
|
25
|
+
# Claude model that writes the issue/PR descriptions (describe.ts). Kept here,
|
|
26
|
+
# not just as the code default, so it can be switched without a code change.
|
|
27
|
+
# AUTO_DESCRIBE_EFFORT: low | medium | high | xhigh | max — ignored for Haiku,
|
|
28
|
+
# which does not accept it.
|
|
29
|
+
AUTO_DESCRIBE_MODEL=claude-sonnet-5-5
|
|
30
|
+
AUTO_DESCRIBE_EFFORT=low
|
|
31
|
+
|
|
32
|
+
AUTO_SIZE_MAP_JSON={"feat":"M","fix":"S","docs":"S","style":"S","refact":"M","perf":"M","test":"S","build":"S","ci":"S","chore":"S","revert":"S"}
|
|
33
|
+
AUTO_ESTIMATE_MAP_JSON={"feat":"3","fix":"2","docs":"1","style":"1","refact":"2","perf":"2","test":"1","build":"1","ci":"1","chore":"1","revert":"1"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Repository settings every managed repo gets (PATCH /repos/{owner}/{repo}). A repo overrides any key in its own .github/repo-settings.json. Keys starting with $ are documentation and are never sent. Reference: docs/REPO_RULES.md.",
|
|
3
|
+
"allow_squash_merge": true,
|
|
4
|
+
"allow_merge_commit": false,
|
|
5
|
+
"allow_rebase_merge": true,
|
|
6
|
+
"squash_merge_commit_title": "PR_TITLE",
|
|
7
|
+
"squash_merge_commit_message": "COMMIT_MESSAGES",
|
|
8
|
+
"delete_branch_on_merge": true,
|
|
9
|
+
"allow_update_branch": true
|
|
10
|
+
}
|
package/config/repos.env
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Which repos `--all` acts on (bin/gh-bootstrap.sh, bin/sync-managed.sh).
|
|
2
|
+
# The union of both lists below is used; a repo listed twice is synced once.
|
|
3
|
+
#
|
|
4
|
+
# Discovery by topic (recommended): every repo of REPOS_OWNER tagged with
|
|
5
|
+
# REPOS_TOPIC. `ebalzan-ci onboard` adds the topic, so onboarding a repo needs
|
|
6
|
+
# no edit here. Leave REPOS_TOPIC empty to turn discovery off.
|
|
7
|
+
REPOS_TOPIC=ebalzan-ci
|
|
8
|
+
# Account whose repos are searched for the topic. Empty = the account gh is
|
|
9
|
+
# logged in as (`gh api user`).
|
|
10
|
+
REPOS_OWNER=
|
|
11
|
+
|
|
12
|
+
# Explicit extra repos, as OWNER/REPO slugs separated by spaces — e.g. repos of
|
|
13
|
+
# another account or organization you administer.
|
|
14
|
+
REPOS=
|
|
15
|
+
|
|
16
|
+
# Where `--all` looks for local checkouts: <CI_REPOS_ROOT>/<repo name>. A repo
|
|
17
|
+
# with a local checkout gets its gitignored .github/secrets*.env synced too; one
|
|
18
|
+
# without is synced from its default branch through the API (variables,
|
|
19
|
+
# settings, rulesets, shared secrets — never its own secrets, which only exist
|
|
20
|
+
# locally). Default: the directory holding this checkout. Override with the
|
|
21
|
+
# CI_REPOS_ROOT environment variable.
|
|
22
|
+
|
|
23
|
+
# Where this repo lives — the slug every caller's `uses:` names, and what
|
|
24
|
+
# `ebalzan-ci onboard` writes into the templates. Change it only if you fork.
|
|
25
|
+
CI_REPO=ebalzan/ci
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "dev is the integration branch: it cannot be deleted. Force-pushes stay allowed because release.yml's sync-dev job force-points dev at each release commit. Reference: docs/REPO_RULES.md.",
|
|
3
|
+
"name": "ci: protect dev",
|
|
4
|
+
"target": "branch",
|
|
5
|
+
"enforcement": "active",
|
|
6
|
+
"conditions": {
|
|
7
|
+
"ref_name": { "include": ["refs/heads/dev"], "exclude": [] }
|
|
8
|
+
},
|
|
9
|
+
"rules": [{ "type": "deletion" }]
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "main is the release branch: it cannot be deleted or force-pushed. The release job only fast-forwards it, so nothing in the pipeline needs a bypass. Reference: docs/REPO_RULES.md.",
|
|
3
|
+
"name": "ci: protect main",
|
|
4
|
+
"target": "branch",
|
|
5
|
+
"enforcement": "active",
|
|
6
|
+
"conditions": {
|
|
7
|
+
"ref_name": { "include": ["refs/heads/main"], "exclude": [] }
|
|
8
|
+
},
|
|
9
|
+
"rules": [{ "type": "deletion" }, { "type": "non_fast_forward" }]
|
|
10
|
+
}
|