-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlib.sh
More file actions
executable file
·228 lines (208 loc) · 6.8 KB
/
Copy pathlib.sh
File metadata and controls
executable file
·228 lines (208 loc) · 6.8 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
#!/bin/bash
#
# Shared library for the Craft CMS deployment scripts.
# Sourced by deploy.sh and rollback.sh.
# @see https://github.com/elfacht/craft-deploy
#
# @author Martin Szymanski <martin@elfacht.com>
# @license MIT
#######################################
# Single source of truth for the version.
# Used by the scripts that source this file.
#######################################
# shellcheck disable=SC2034
CRAFT_DEPLOY_VERSION="0.7.0"
#######################################
# Runtime flags (may be overridden by callers / CLI flags).
#######################################
DRY_RUN="${DRY_RUN:-0}"
LOG_FILE="${LOG_FILE:-}"
#######################################
# Logging helpers.
# Everything is timestamped and, if LOG_FILE is set, mirrored into it.
#######################################
_log_ts() {
date +'%Y-%m-%dT%H:%M:%S%z'
}
_log_write() {
[ -n "$LOG_FILE" ] || return 0
printf '%s\n' "$1" >> "$LOG_FILE" 2>/dev/null || true
}
log() {
local line
line="[$(_log_ts)] $*"
printf '%s\n' "$line"
_log_write "$line"
}
warn() {
local line
line="[$(_log_ts)] WARN: $*"
printf '%s\n' "$line" >&2
_log_write "$line"
}
err() {
local line
line="[$(_log_ts)] ERROR: $*"
printf '%s\n' "$line" >&2
_log_write "$line"
}
die() {
err "$@"
exit 1
}
#######################################
# Run a command, or just print it when DRY_RUN=1.
# Use for mutating/external commands (git, composer, ln, rm, curl, php).
# Builtins like `cd` must be called directly, not through this wrapper.
#######################################
run() {
if [ "$DRY_RUN" = "1" ]; then
log "[dry-run] $*"
return 0
fi
"$@"
}
#######################################
# Robustly load a .env-style file.
# Pure-bash line parser (no sourcing, no /dev/stdin, no process
# substitution) so it works in restricted server environments and never
# executes code from the config file.
# - Blank lines and comment lines (#) are ignored.
# - Only valid KEY=VALUE lines are taken; the value keeps any '='.
# - Surrounding single/double quotes are stripped; CRLF is tolerated.
# - Variables are exported so child processes inherit them.
# Arguments:
# $1 - path to the env file
#######################################
load_env() {
local file="$1"
[ -f "$file" ] || die "Config file not found: $file"
local line key val
while IFS= read -r line || [ -n "$line" ]; do
line="${line%$'\r'}" # tolerate CRLF
line="${line#"${line%%[![:space:]]*}"}" # strip leading whitespace
case "$line" in
''|'#'*) continue ;; # blank or comment
[A-Za-z_]*=*) ;; # looks like KEY=VALUE
*) continue ;;
esac
key="${line%%=*}"
val="${line#*=}"
key="${key%"${key##*[![:space:]]}"}" # trim trailing space from key
case "$key" in *[!A-Za-z0-9_]*) continue ;; esac
if [ "${#val}" -ge 2 ]; then # strip matching surrounding quotes
case "$val" in
\"*\") val="${val#\"}"; val="${val%\"}" ;;
\'*\') val="${val#\'}"; val="${val%\'}" ;;
esac
fi
export "$key=$val"
done < "$file"
}
#######################################
# Normalize an optional subdirectory value so it can be safely appended
# as "${dir:+/$dir}". Collapses "", ".", "./", "./foo", "foo/" to a clean
# relative name (or empty for the repo root).
# Arguments:
# $1 - raw value
# Returns:
# normalized value on stdout
#######################################
normalize_subdir() {
local d="$1"
d="${d#./}" # drop a leading ./
d="${d%/}" # drop a trailing /
[ "$d" = "." ] && d=""
printf '%s' "$d"
}
#######################################
# Fail fast if a required variable is empty.
# Arguments:
# $@ - one or more variable names
#######################################
require_var() {
local name val
for name in "$@"; do
val="${!name:-}"
[ -n "$val" ] || die "Required config '$name' is empty. Set it in your config file."
done
}
#######################################
# List the immediate children of a directory into the global array
# LIST_ENTRIES, sorted ascending by name. Uses sorted glob expansion only
# (no find, no pipes, no process substitution) so it works on servers
# without /dev/fd. Dotfiles (e.g. .DS_Store) are intentionally skipped.
# Arguments:
# $1 - parent directory
#######################################
list_entries() {
local parent="$1" e
LIST_ENTRIES=()
local _ng=0
shopt -q nullglob && _ng=1
shopt -s nullglob
for e in "$parent"/*; do
LIST_ENTRIES+=("$e")
done
[ "$_ng" = "1" ] || shopt -u nullglob
}
#######################################
# Keep the newest N entries in a directory, delete the rest.
# Entries (files or dirs) are sorted ascending by name; release and backup
# names are timestamped, so name-sort is chronological. The oldest entries
# beyond the keep count are removed.
# Arguments:
# $1 - parent directory
# $2 - number of entries to keep
# $3 - label for log output (e.g. "release", "backup")
#######################################
prune_old() {
local parent="$1" keep="$2" label="${3:-entry}"
[ -d "$parent" ] || return 0
[[ "$keep" =~ ^[0-9]+$ ]] || keep=5
list_entries "$parent"
local entries=( ${LIST_ENTRIES[@]+"${LIST_ENTRIES[@]}"} )
local total=${#entries[@]}
local remove=$(( total - keep ))
(( remove > 0 )) || return 0
local i
for (( i = 0; i < remove; i++ )); do
log "- Delete old ${label} '$(basename "${entries[$i]}")'"
run rm -rf "${entries[$i]}"
done
}
#######################################
# Point <root>/current at a release, replacing any existing symlink.
# `ln -sfn` only replaces `current` correctly when it is missing or already
# a symlink. If `current` exists as a real file/directory (e.g. a docroot
# the host pre-created), `ln` would nest the link *inside* it — so we refuse
# and tell the operator to clean up.
# Arguments:
# $1 - target release path
# $2 - root path
#######################################
switch_current() {
local target="$1" root="$2"
local link="$root/current"
if [ -e "$link" ] && [ ! -L "$link" ]; then
die "'$link' exists as a real file/directory, not a symlink. 'current' must be a symlink to a release. Remove the stray '$link' (it should only contain deploy symlinks) and re-run."
fi
run ln -sfn "$target" "$link"
}
#######################################
# Run an optional hook script if it is set and executable.
# The hook inherits the exported DEPLOY_* vars plus RELEASE_PATH/ROOT_PATH.
# Arguments:
# $1 - hook script path (may be empty)
# $2 - label for log output (e.g. "before", "after")
#######################################
run_hook() {
local hook="$1" label="$2"
[ -n "$hook" ] || return 0
if [ ! -f "$hook" ]; then
warn "Hook '$label' not found: $hook (skipping)"
return 0
fi
log "- Run ${label} hook: $hook"
run bash "$hook"
}