Skip to content

Commit b3db76b

Browse files
committed
chore: [#446] add update-dependencies.sh automation script
Add `scripts/update-dependencies.sh` to automate the dependency update workflow. The script handles the full lifecycle from branch creation to optional PR creation: 1. Verifies a clean working tree 2. Fetches and fast-forwards the base branch from the upstream remote 3. Creates (or recreates) the feature branch 4. Runs `cargo update` and captures the full output 5. Exits early if no Cargo.lock changes are produced 6. Optionally runs `./scripts/pre-commit.sh` 7. Commits the `Cargo.lock` changes with the full `cargo update` output in the commit body (signed by default via `git commit -S`) 8. Pushes the branch to the fork remote 9. Optionally creates a PR via `gh pr create` Usage: ./scripts/update-dependencies.sh \ --branch 446-update-dependencies \ --push-remote josecelano \ --create-pr See `./scripts/update-dependencies.sh --help` for all options. Also adds `worktree` to project-words.txt (used in function name `ensure_clean_worktree`) and documents the script in `docs/contributing/README.md`.
1 parent 4622281 commit b3db76b

3 files changed

Lines changed: 325 additions & 0 deletions

File tree

docs/contributing/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ git commit -m "feat: [#42] add new testing feature"
6262
git push origin 42-add-your-feature-name
6363
```
6464

65+
## Dependency Update Automation
66+
67+
For dependency-only updates, you can automate the repetitive git and PR workflow with:
68+
69+
```bash
70+
./scripts/update-dependencies.sh \
71+
--branch 445-update-dependencies \
72+
--push-remote josecelano \
73+
--create-pr
74+
```
75+
76+
Notes:
77+
78+
- The script signs commits by default with `git commit -S`.
79+
- The push remote and branch name are explicit so the workflow works with different forks.
80+
- Reuse `--delete-existing-branch` only when you intentionally want to replace an older update branch.
81+
- Use `--help` to see all options.
82+
6583
## 📖 Additional Resources
6684

6785
- [Main Documentation](../documentation.md) - Project documentation organization

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ youruser
541541
zcat
542542
zeroize
543543
zoneinfo
544+
worktree
544545
zstd
545546
CSPRNG
546547
USERINFO

scripts/update-dependencies.sh

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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

Comments
 (0)