|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +usage() { |
| 6 | + cat <<'EOF' |
| 7 | +Usage: ./scripts/update-dependencies.sh --branch <branch-name> [options] |
| 8 | +
|
| 9 | +Automates the dependency update workflow: |
| 10 | +- sync the base branch from the base remote |
| 11 | +- create a fresh working branch |
| 12 | +- run cargo update |
| 13 | +- run pre-commit checks |
| 14 | +- create a signed commit with the full cargo update output |
| 15 | +- optionally push the branch and create a pull request |
| 16 | +
|
| 17 | +Options: |
| 18 | + --branch <name> Working branch to create (required) |
| 19 | + --base-branch <name> Base branch to update from (default: main) |
| 20 | + --base-remote <name> Remote that owns the base branch (default: torrust, then origin, then first remote) |
| 21 | + --push-remote <name> Remote used to push the branch |
| 22 | + --repo <owner/repo> Repository slug for PR creation |
| 23 | + --commit-title <title> Commit title and default PR title |
| 24 | + --pr-title <title> Pull request title override |
| 25 | + --delete-existing-branch Delete an existing local/remote branch with the same name |
| 26 | + --skip-pre-commit Skip ./scripts/pre-commit.sh |
| 27 | + --create-pr Create a PR after pushing the branch |
| 28 | + --no-sign-commit Do not use git commit -S |
| 29 | + --help Show this help message |
| 30 | +
|
| 31 | +Examples: |
| 32 | + ./scripts/update-dependencies.sh \ |
| 33 | + --branch 445-update-dependencies \ |
| 34 | + --push-remote josecelano \ |
| 35 | + --create-pr |
| 36 | +
|
| 37 | + ./scripts/update-dependencies.sh \ |
| 38 | + --branch update-dependencies \ |
| 39 | + --push-remote josecelano \ |
| 40 | + --delete-existing-branch \ |
| 41 | + --commit-title "chore: update dependencies" |
| 42 | +EOF |
| 43 | +} |
| 44 | + |
| 45 | +log() { |
| 46 | + echo "[update-dependencies] $*" |
| 47 | +} |
| 48 | + |
| 49 | +fail() { |
| 50 | + echo "Error: $*" >&2 |
| 51 | + exit 1 |
| 52 | +} |
| 53 | + |
| 54 | +command_exists() { |
| 55 | + command -v "$1" >/dev/null 2>&1 |
| 56 | +} |
| 57 | + |
| 58 | +detect_default_remote() { |
| 59 | + if git remote | grep -qx "torrust"; then |
| 60 | + echo "torrust" |
| 61 | + return |
| 62 | + fi |
| 63 | + |
| 64 | + if git remote | grep -qx "origin"; then |
| 65 | + echo "origin" |
| 66 | + return |
| 67 | + fi |
| 68 | + |
| 69 | + git remote | head -n 1 |
| 70 | +} |
| 71 | + |
| 72 | +parse_github_slug_from_remote() { |
| 73 | + local remote=$1 |
| 74 | + local remote_url |
| 75 | + local slug |
| 76 | + |
| 77 | + remote_url=$(git remote get-url "$remote") |
| 78 | + slug=$(printf '%s' "$remote_url" | sed -E 's#^(git@github.com:|https://github.com/|ssh://git@github.com/)##; s#\.git$##') |
| 79 | + |
| 80 | + [[ "$slug" == */* ]] || fail "Could not parse GitHub repository slug from remote '$remote'" |
| 81 | + |
| 82 | + echo "$slug" |
| 83 | +} |
| 84 | + |
| 85 | +parse_github_owner_from_remote() { |
| 86 | + local remote=$1 |
| 87 | + local slug |
| 88 | + |
| 89 | + slug=$(parse_github_slug_from_remote "$remote") |
| 90 | + echo "${slug%%/*}" |
| 91 | +} |
| 92 | + |
| 93 | +branch_exists_local() { |
| 94 | + local branch=$1 |
| 95 | + git show-ref --verify --quiet "refs/heads/$branch" |
| 96 | +} |
| 97 | + |
| 98 | +branch_exists_remote() { |
| 99 | + local remote=$1 |
| 100 | + local branch=$2 |
| 101 | + git ls-remote --exit-code --heads "$remote" "$branch" >/dev/null 2>&1 |
| 102 | +} |
| 103 | + |
| 104 | +ensure_clean_worktree() { |
| 105 | + git diff --quiet || fail "Working tree has unstaged changes" |
| 106 | + git diff --cached --quiet || fail "Working tree has staged changes" |
| 107 | +} |
| 108 | + |
| 109 | +cleanup_files() { |
| 110 | + rm -f "$CARGO_UPDATE_OUTPUT_FILE" "$COMMIT_MESSAGE_FILE" "$PR_BODY_FILE" |
| 111 | +} |
| 112 | + |
| 113 | +BRANCH_NAME="" |
| 114 | +BASE_BRANCH="main" |
| 115 | +BASE_REMOTE="" |
| 116 | +PUSH_REMOTE="" |
| 117 | +REPOSITORY_SLUG="" |
| 118 | +COMMIT_TITLE="chore: update dependencies" |
| 119 | +PR_TITLE="" |
| 120 | +DELETE_EXISTING_BRANCH=false |
| 121 | +RUN_PRE_COMMIT=true |
| 122 | +CREATE_PR=false |
| 123 | +SIGN_COMMIT=true |
| 124 | + |
| 125 | +while [[ $# -gt 0 ]]; do |
| 126 | + case "$1" in |
| 127 | + --branch) |
| 128 | + BRANCH_NAME=${2:-} |
| 129 | + shift 2 |
| 130 | + ;; |
| 131 | + --base-branch) |
| 132 | + BASE_BRANCH=${2:-} |
| 133 | + shift 2 |
| 134 | + ;; |
| 135 | + --base-remote) |
| 136 | + BASE_REMOTE=${2:-} |
| 137 | + shift 2 |
| 138 | + ;; |
| 139 | + --push-remote) |
| 140 | + PUSH_REMOTE=${2:-} |
| 141 | + shift 2 |
| 142 | + ;; |
| 143 | + --repo) |
| 144 | + REPOSITORY_SLUG=${2:-} |
| 145 | + shift 2 |
| 146 | + ;; |
| 147 | + --commit-title) |
| 148 | + COMMIT_TITLE=${2:-} |
| 149 | + shift 2 |
| 150 | + ;; |
| 151 | + --pr-title) |
| 152 | + PR_TITLE=${2:-} |
| 153 | + shift 2 |
| 154 | + ;; |
| 155 | + --delete-existing-branch) |
| 156 | + DELETE_EXISTING_BRANCH=true |
| 157 | + shift |
| 158 | + ;; |
| 159 | + --skip-pre-commit) |
| 160 | + RUN_PRE_COMMIT=false |
| 161 | + shift |
| 162 | + ;; |
| 163 | + --create-pr) |
| 164 | + CREATE_PR=true |
| 165 | + shift |
| 166 | + ;; |
| 167 | + --no-sign-commit) |
| 168 | + SIGN_COMMIT=false |
| 169 | + shift |
| 170 | + ;; |
| 171 | + --help) |
| 172 | + usage |
| 173 | + exit 0 |
| 174 | + ;; |
| 175 | + *) |
| 176 | + fail "Unknown option: $1" |
| 177 | + ;; |
| 178 | + esac |
| 179 | +done |
| 180 | + |
| 181 | +[[ -n "$BRANCH_NAME" ]] || fail "--branch is required" |
| 182 | + |
| 183 | +command_exists git || fail "git is required" |
| 184 | +command_exists cargo || fail "cargo is required" |
| 185 | + |
| 186 | +BASE_REMOTE=${BASE_REMOTE:-$(detect_default_remote)} |
| 187 | +[[ -n "$BASE_REMOTE" ]] || fail "Could not determine a base remote" |
| 188 | + |
| 189 | +if [[ -z "$REPOSITORY_SLUG" ]]; then |
| 190 | + REPOSITORY_SLUG=$(parse_github_slug_from_remote "$BASE_REMOTE") |
| 191 | +fi |
| 192 | + |
| 193 | +if [[ "$CREATE_PR" == true ]]; then |
| 194 | + [[ -n "$PUSH_REMOTE" ]] || fail "--push-remote is required when --create-pr is used" |
| 195 | + command_exists gh || fail "gh is required when --create-pr is used" |
| 196 | +fi |
| 197 | + |
| 198 | +if [[ -n "$PUSH_REMOTE" ]]; then |
| 199 | + git remote get-url "$PUSH_REMOTE" >/dev/null 2>&1 || fail "Remote '$PUSH_REMOTE' does not exist" |
| 200 | +fi |
| 201 | + |
| 202 | +CARGO_UPDATE_OUTPUT_FILE=$(mktemp) |
| 203 | +COMMIT_MESSAGE_FILE=$(mktemp) |
| 204 | +PR_BODY_FILE=$(mktemp) |
| 205 | +trap cleanup_files EXIT |
| 206 | + |
| 207 | +ensure_clean_worktree |
| 208 | + |
| 209 | +if branch_exists_local "$BRANCH_NAME"; then |
| 210 | + if [[ "$DELETE_EXISTING_BRANCH" == true ]]; then |
| 211 | + log "Deleting local branch '$BRANCH_NAME'" |
| 212 | + current_branch=$(git branch --show-current) |
| 213 | + if [[ "$current_branch" == "$BRANCH_NAME" ]]; then |
| 214 | + git checkout "$BASE_BRANCH" |
| 215 | + fi |
| 216 | + git branch -D "$BRANCH_NAME" |
| 217 | + else |
| 218 | + fail "Local branch '$BRANCH_NAME' already exists. Use --delete-existing-branch to replace it." |
| 219 | + fi |
| 220 | +fi |
| 221 | + |
| 222 | +if [[ -n "$PUSH_REMOTE" ]] && branch_exists_remote "$PUSH_REMOTE" "$BRANCH_NAME"; then |
| 223 | + if [[ "$DELETE_EXISTING_BRANCH" == true ]]; then |
| 224 | + log "Deleting remote branch '$BRANCH_NAME' from '$PUSH_REMOTE'" |
| 225 | + git push "$PUSH_REMOTE" --delete "$BRANCH_NAME" |
| 226 | + else |
| 227 | + fail "Remote branch '$BRANCH_NAME' already exists on '$PUSH_REMOTE'. Use --delete-existing-branch to replace it." |
| 228 | + fi |
| 229 | +fi |
| 230 | + |
| 231 | +log "Fetching '$BASE_REMOTE/$BASE_BRANCH'" |
| 232 | +git fetch "$BASE_REMOTE" "$BASE_BRANCH" |
| 233 | + |
| 234 | +log "Checking out '$BASE_BRANCH'" |
| 235 | +git checkout "$BASE_BRANCH" |
| 236 | + |
| 237 | +log "Fast-forwarding '$BASE_BRANCH' from '$BASE_REMOTE/$BASE_BRANCH'" |
| 238 | +git merge --ff-only "$BASE_REMOTE/$BASE_BRANCH" |
| 239 | + |
| 240 | +log "Creating branch '$BRANCH_NAME'" |
| 241 | +git checkout -b "$BRANCH_NAME" |
| 242 | + |
| 243 | +log "Running cargo update" |
| 244 | +cargo update 2>&1 | tee "$CARGO_UPDATE_OUTPUT_FILE" |
| 245 | + |
| 246 | +if git diff --quiet; then |
| 247 | + log "No dependency changes were produced by cargo update" |
| 248 | + git checkout "$BASE_BRANCH" |
| 249 | + git branch -D "$BRANCH_NAME" |
| 250 | + exit 0 |
| 251 | +fi |
| 252 | + |
| 253 | +if [[ "$RUN_PRE_COMMIT" == true ]]; then |
| 254 | + log "Running pre-commit checks" |
| 255 | + ./scripts/pre-commit.sh |
| 256 | + PRE_COMMIT_SUMMARY="- run \`./scripts/pre-commit.sh\` successfully" |
| 257 | +else |
| 258 | + PRE_COMMIT_SUMMARY="- skip \`./scripts/pre-commit.sh\` by request" |
| 259 | +fi |
| 260 | + |
| 261 | +{ |
| 262 | + printf '%s\n\n' "$COMMIT_TITLE" |
| 263 | + printf '%s\n' 'cargo update output:' |
| 264 | + printf '%s\n' '```' |
| 265 | + cat "$CARGO_UPDATE_OUTPUT_FILE" |
| 266 | + printf '%s\n' '```' |
| 267 | +} > "$COMMIT_MESSAGE_FILE" |
| 268 | + |
| 269 | +log "Creating commit" |
| 270 | +git add -u |
| 271 | +if [[ "$SIGN_COMMIT" == true ]]; then |
| 272 | + git commit -S -F "$COMMIT_MESSAGE_FILE" |
| 273 | +else |
| 274 | + git commit -F "$COMMIT_MESSAGE_FILE" |
| 275 | +fi |
| 276 | + |
| 277 | +if [[ -n "$PUSH_REMOTE" ]]; then |
| 278 | + log "Pushing branch to '$PUSH_REMOTE'" |
| 279 | + git push -u "$PUSH_REMOTE" "$BRANCH_NAME" |
| 280 | +fi |
| 281 | + |
| 282 | +if [[ "$CREATE_PR" == true ]]; then |
| 283 | + HEAD_OWNER=$(parse_github_owner_from_remote "$PUSH_REMOTE") |
| 284 | + PR_TITLE=${PR_TITLE:-$COMMIT_TITLE} |
| 285 | + |
| 286 | + { |
| 287 | + printf '%s\n' '## Summary' |
| 288 | + printf '%s\n' "- run \`cargo update\`" |
| 289 | + printf '%s\n' "- commit the resulting \`Cargo.lock\` changes" |
| 290 | + printf '%s\n\n' "$PRE_COMMIT_SUMMARY" |
| 291 | + printf '%s\n' '## cargo update output' |
| 292 | + printf '%s\n' '```' |
| 293 | + cat "$CARGO_UPDATE_OUTPUT_FILE" |
| 294 | + printf '%s\n' '```' |
| 295 | + } > "$PR_BODY_FILE" |
| 296 | + |
| 297 | + log "Creating pull request in '$REPOSITORY_SLUG'" |
| 298 | + gh pr create \ |
| 299 | + --repo "$REPOSITORY_SLUG" \ |
| 300 | + --base "$BASE_BRANCH" \ |
| 301 | + --head "$HEAD_OWNER:$BRANCH_NAME" \ |
| 302 | + --title "$PR_TITLE" \ |
| 303 | + --body-file "$PR_BODY_FILE" |
| 304 | +fi |
| 305 | + |
| 306 | +log "Dependency update workflow completed" |
0 commit comments