-
Notifications
You must be signed in to change notification settings - Fork 172
Expand file tree
/
Copy pathtag-deployment.sh
More file actions
executable file
·337 lines (294 loc) · 11 KB
/
Copy pathtag-deployment.sh
File metadata and controls
executable file
·337 lines (294 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/env bash
#
# tag-deployment.sh - Create annotated git tag for a contract deployment
#
# Produces tags in the format `deploy/<env>/YYYY-MM-DD/<name>` as defined by
# DEPLOYMENT.md (the bare-date form `deploy/<env>/YYYY-MM-DD` is permitted as a
# fallback when --name is omitted, but a descriptive name is recommended). The
# tag body records the deployer, network, commit, and a list of changed
# contracts per address book (detected by diffing address-book JSON against a
# base ref).
#
# Usage:
# ./scripts/tag-deployment.sh --deployer <desc> --network <network> [--name <short-name>] [options]
#
# Options:
# --deployer <desc> What performed the deployment (free-form, e.g., "packages/deployment --tags RewardsManager")
# --network <name> Network: arbitrumOne (→ mainnet) or arbitrumSepolia (→ testnet)
# --name <short-name> Recommended release/upgrade short name appended to the tag as a further path segment
# (e.g. "reward-manager-and-subgraph-service" → deploy/<env>/YYYY-MM-DD/<name>).
# If omitted, the tag is the bare-date form deploy/<env>/YYYY-MM-DD — permitted as a
# fallback but exceptional; prefer a name that describes the deploy.
# --base <ref> Git ref to diff address books against. Defaults to the latest `deploy/<env>/*`
# tag for the target environment. If none exists (initial deploy), pass --base
# explicitly (e.g. --base HEAD~1 or the empty-tree sentinel).
# --dry-run Print the preview and exit without creating the tag.
# --yes, -y Skip the interactive confirmation prompt (required for non-interactive use).
# --no-sign Create an unsigned annotated tag. Default is signed (-s).
# --help Show this help
#
# By default the script prints a preview (tag name, commit, annotation body) and
# then asks for confirmation before creating the tag. Use --yes to skip the
# prompt, or --dry-run to stop after the preview.
#
set -euo pipefail
# --- Dependencies ---
for cmd in git jq; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: $cmd is required but not found"
exit 1
fi
done
REPO_ROOT="$(git rev-parse --show-toplevel)"
# --- Defaults ---
DEPLOYER=""
NETWORK=""
UPGRADE_NAME=""
BASE_REF="" # Empty means "auto: use latest deploy/<env>/* tag". Overridden by --base.
DRY_RUN=false
ASSUME_YES=false
SIGN_FLAG="-s" # Signed by default. --no-sign switches to -a (annotated, unsigned).
# --- Address books managed by packages/deployment ---
ADDRESS_BOOKS=(
"packages/horizon/addresses.json:horizon"
"packages/subgraph-service/addresses.json:subgraph-service"
"packages/issuance/addresses.json:issuance"
)
# --- Network to chain ID / label mapping ---
network_to_chain_id() {
case "$1" in
arbitrumOne) echo "42161" ;;
arbitrumSepolia) echo "421614" ;;
*) echo "unknown" ;;
esac
}
network_to_label() {
case "$1" in
arbitrumOne) echo "mainnet" ;;
arbitrumSepolia) echo "testnet" ;;
*) echo "unknown" ;;
esac
}
network_to_display() {
case "$1" in
arbitrumOne) echo "arbitrum-one" ;;
arbitrumSepolia) echo "arbitrum-sepolia" ;;
*) echo "$1" ;;
esac
}
# --- Parse arguments ---
usage() {
sed -n '3,32p' "$0" | sed 's/^# \?//'
exit "${1:-0}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--deployer) DEPLOYER="$2"; shift 2 ;;
--network) NETWORK="$2"; shift 2 ;;
--name) UPGRADE_NAME="$2"; shift 2 ;;
--base) BASE_REF="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
--yes|-y) ASSUME_YES=true; shift ;;
--no-sign) SIGN_FLAG="-a"; shift ;;
--help) usage 0 ;;
*) echo "Unknown option: $1"; usage 1 ;;
esac
done
if [[ -z "$DEPLOYER" ]]; then
echo "Error: --deployer is required"
usage 1
fi
if [[ -z "$NETWORK" ]]; then
echo "Error: --network is required"
usage 1
fi
# --name is recommended but not required. When provided, validate format: lowercase, digits, hyphens only.
if [[ -n "$UPGRADE_NAME" ]]; then
if [[ ! "$UPGRADE_NAME" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then
echo "Error: --name must be lowercase alphanumeric with hyphens (e.g., 'reward-manager-and-subgraph-service')"
exit 1
fi
else
echo "Warning: --name not provided; creating bare-date tag (fallback form)."
echo " Prefer a descriptive --name (e.g. 'reward-manager', 'fix-activation') for self-describing tags."
fi
CHAIN_ID="$(network_to_chain_id "$NETWORK")"
LABEL="$(network_to_label "$NETWORK")"
DISPLAY="$(network_to_display "$NETWORK")"
if [[ "$CHAIN_ID" == "unknown" ]]; then
echo "Error: unknown network '$NETWORK' (expected arbitrumOne or arbitrumSepolia)"
exit 1
fi
# --- Resolve --base default ---
# If --base was not provided, use the latest deploy/<label>/* tag as the diff base.
# This is the common case: every deploy's "contracts changed" section is the diff
# against the previous deploy on the same environment.
if [[ -z "$BASE_REF" ]]; then
BASE_REF="$(git tag -l "deploy/${LABEL}/*" | sort | tail -1)"
if [[ -z "$BASE_REF" ]]; then
echo "Error: no previous deploy/${LABEL}/* tag found to use as --base default."
echo " For an initial deploy on ${LABEL}, pass --base explicitly"
echo " (e.g. --base HEAD~1, or --base \$(git hash-object -t tree /dev/null) for an empty-tree diff)."
exit 1
fi
echo "Using latest deploy/${LABEL}/* tag as --base: ${BASE_REF}"
fi
# --- Preconditions ---
if [[ -n "$(git status --porcelain)" ]]; then
echo "Error: working tree is not clean. Commit or stash changes first."
echo " (Tag must point to a finalized commit)"
exit 1
fi
COMMIT_SHA="$(git rev-parse HEAD)"
COMMIT_SHORT="$(git rev-parse --short HEAD)"
# Check if commit is signed (informational)
if ! git log -1 --format='%G?' HEAD | grep -q '[GU]'; then
echo "Warning: HEAD commit ($COMMIT_SHORT) is not signed"
fi
# Verify base ref exists
if ! git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then
echo "Error: base ref '$BASE_REF' does not exist"
exit 1
fi
# --- Detect changed contracts per address book ---
collect_changes() {
local book_path="$1"
local chain_id="$2"
local base_ref="$3"
# Get the file at base ref and at HEAD (both via git, not filesystem)
local base_json head_json
base_json="$(git show "$base_ref:$book_path" 2>/dev/null || echo '{}')"
head_json="$(git show "HEAD:$book_path" 2>/dev/null || echo '{}')"
if [[ "$head_json" == '{}' ]]; then
return
fi
# Extract contract names for this chain at base and head
local base_contracts head_contracts
base_contracts="$(echo "$base_json" | jq -r --arg cid "$chain_id" '.[$cid] // {} | keys[]' 2>/dev/null || true)"
head_contracts="$(echo "$head_json" | jq -r --arg cid "$chain_id" '.[$cid] // {} | keys[]' 2>/dev/null || true)"
# Find contracts that are new or changed
local all_contracts
all_contracts="$(echo -e "${base_contracts}\n${head_contracts}" | sort -u | grep -v '^$' || true)"
for contract in $all_contracts; do
local base_entry head_entry
base_entry="$(echo "$base_json" | jq -c --arg cid "$chain_id" --arg c "$contract" '.[$cid][$c] // empty' 2>/dev/null || true)"
head_entry="$(echo "$head_json" | jq -c --arg cid "$chain_id" --arg c "$contract" '.[$cid][$c] // empty' 2>/dev/null || true)"
if [[ "$base_entry" != "$head_entry" ]]; then
# Contract changed - extract key details
local impl addr change_type
addr="$(echo "$head_json" | jq -r --arg cid "$chain_id" --arg c "$contract" '.[$cid][$c].address // empty' 2>/dev/null || true)"
impl="$(echo "$head_json" | jq -r --arg cid "$chain_id" --arg c "$contract" '.[$cid][$c].implementation // empty' 2>/dev/null || true)"
if [[ -z "$base_entry" ]]; then
change_type="new"
elif [[ -z "$head_entry" ]]; then
change_type="removed"
else
change_type="updated"
fi
local detail=""
if [[ -n "$impl" ]]; then
detail="implementation: ${impl}"
elif [[ -n "$addr" ]]; then
detail="address: ${addr}"
fi
echo "${change_type}|${contract}|${detail}"
fi
done
}
# Collect all changes grouped by address book
declare -A BOOK_CHANGES
has_changes=false
for entry in "${ADDRESS_BOOKS[@]}"; do
book_path="${entry%%:*}"
book_name="${entry##*:}"
changes="$(collect_changes "$book_path" "$CHAIN_ID" "$BASE_REF")"
if [[ -n "$changes" ]]; then
BOOK_CHANGES["$book_name"]="$changes"
has_changes=true
fi
done
if [[ "$has_changes" == false ]]; then
echo "No address book changes detected for chain $CHAIN_ID between $BASE_REF and HEAD"
echo " Checked:"
for entry in "${ADDRESS_BOOKS[@]}"; do
echo " ${entry%%:*}"
done
exit 1
fi
# --- Generate tag name ---
# Format matches DEPLOYMENT.md: deploy/<env>/YYYY-MM-DD[/<name>]
TAG_DATE="$(date +%Y-%m-%d)"
if [[ -n "$UPGRADE_NAME" ]]; then
TAG_BASE="deploy/${LABEL}/${TAG_DATE}/${UPGRADE_NAME}"
else
TAG_BASE="deploy/${LABEL}/${TAG_DATE}"
fi
TAG_NAME="$TAG_BASE"
# Collisions are resolved by choosing a more specific --name, not by automatic suffixes.
if git tag -l "$TAG_NAME" | grep -q .; then
echo "Error: tag '${TAG_NAME}' already exists."
if [[ -n "$UPGRADE_NAME" ]]; then
echo " Choose a more specific --name (e.g. 'fix-...', 'retry-...') to disambiguate."
else
echo " Provide a --name to disambiguate (the name is the only disambiguator; letter suffixes are not used)."
fi
exit 1
fi
# --- Build annotation ---
ANNOTATION="network: ${DISPLAY} (${CHAIN_ID})
deployed-by: ${DEPLOYER}"
if [[ -n "$UPGRADE_NAME" ]]; then
ANNOTATION="upgrade: ${UPGRADE_NAME}
${ANNOTATION}"
fi
for book_name in $(echo "${!BOOK_CHANGES[@]}" | tr ' ' '\n' | sort); do
changes="${BOOK_CHANGES[$book_name]}"
ANNOTATION="${ANNOTATION}
contracts (${book_name}):"
while IFS='|' read -r change_type contract detail; do
local_line=" - ${contract}"
if [[ -n "$detail" ]]; then
local_line="${local_line} (${detail})"
fi
if [[ "$change_type" == "new" ]]; then
local_line="${local_line} [new]"
elif [[ "$change_type" == "removed" ]]; then
local_line="${local_line} [removed]"
fi
ANNOTATION="${ANNOTATION}
${local_line}"
done <<< "$changes"
done
# --- Create or preview tag ---
echo ""
echo "--- Deployment Tag ---"
echo "Tag: ${TAG_NAME}"
echo "Commit: ${COMMIT_SHORT} ($(git log -1 --format='%s' HEAD))"
echo ""
echo "$ANNOTATION"
echo "----------------------"
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo "[dry-run] Tag not created"
exit 0
fi
# Confirm before creating the tag (unless --yes was given).
if [[ "$ASSUME_YES" == false ]]; then
if [[ ! -t 0 ]]; then
echo "Error: stdin is not a TTY; re-run with --yes to confirm non-interactively, or from a terminal."
exit 1
fi
read -r -p "Create this tag? [y/N] " answer
case "$answer" in
y|Y|yes|YES) ;;
*) echo "Aborted."; exit 1 ;;
esac
fi
MSG_FILE="$(mktemp)"
trap 'rm -f "$MSG_FILE"' EXIT
printf '%s\n' "$ANNOTATION" > "$MSG_FILE"
git tag "$SIGN_FLAG" "$TAG_NAME" -F "$MSG_FILE"
echo "Tag created: ${TAG_NAME}"
echo ""
echo "To push: git push origin ${TAG_NAME}"
echo "To view: git show ${TAG_NAME}"