diff --git a/.github/skills/dev/git-workflow/commit-changes/skill.md b/.github/skills/dev/git-workflow/commit-changes/skill.md index ad30e79c..1208a676 100644 --- a/.github/skills/dev/git-workflow/commit-changes/skill.md +++ b/.github/skills/dev/git-workflow/commit-changes/skill.md @@ -19,8 +19,8 @@ This skill guides you through the complete commit process for the Torrust Tracke # 2. Stage changes git add -# 3. Commit with conventional format -git commit -m "{type}: [#{issue}] {description}" +# 3. Commit with conventional format and GPG signature (MANDATORY) +git commit -S -m "{type}: [#{issue}] {description}" ``` ## Conventional Commit Format @@ -60,6 +60,16 @@ When working on a branch with an issue number, include it in your commit message | `ci` | CI/CD related changes | `ci: [#23] add workflow for testing provisioning` | | `perf` | Performance improvements | `perf: [#52] optimize container startup time` | +## GPG Commit Signing (MANDATORY) + +**All commits must be GPG signed.** Use the `-S` flag: + +```bash +git commit -S -m "your commit message" +``` + +Ensure GPG is configured (see Troubleshooting section if signing fails). + ## Pre-commit Verification (MANDATORY) **Before committing any changes**, you **MUST** run: @@ -207,8 +217,8 @@ vim src/main.rs # 4. Stage changes git add src/main.rs -# 5. Commit with conventional format -git commit -m "feat: [#42] add new CLI command" +# 5. Commit with conventional format and GPG signature (MANDATORY) +git commit -S -m "feat: [#42] add new CLI command" # 6. Push to remote git push origin 42-add-new-cli-command @@ -216,6 +226,21 @@ git push origin 42-add-new-cli-command ## Troubleshooting +### GPG Signing Fails + +**Problem**: `git commit -S` fails with "gpg failed to sign the data" + +**Solution**: + +1. Verify GPG is installed: `gpg --version` +2. List your GPG keys: `gpg --list-keys` +3. If no keys exist, create one: `gpg --gen-key` +4. Configure Git to use your GPG key: `git config --global user.signingkey ` +5. Test signing: `echo "test" | gpg --clearsign` +6. Retry commit: `git commit -S -m "your message"` + +If still failing, check that your GPG agent is running and has proper pinentry configured. + ### Pre-commit Script Fails **Problem**: One or more checks fail in `./scripts/pre-commit.sh` @@ -261,8 +286,9 @@ Note: This is only supported in local environments with proper LXD networking an ## Key Reminders -1. **Always run `./scripts/pre-commit.sh` before committing** - This is non-negotiable -2. **Use issue numbers consistently** - Follow the `[#{issue}]` format -3. **Be careful with hashtags** - Only use `#NUMBER` when referencing issues -4. **Keep commits atomic** - One logical change per commit -5. **Write descriptive messages** - Future you will thank present you +1. **Always sign commits with `-S`** - GPG signing is mandatory for audit trail +2. **Always run `./scripts/pre-commit.sh` before committing** - This is non-negotiable +3. **Use issue numbers consistently** - Follow the `[#{issue}]` format +4. **Be careful with hashtags** - Only use `#NUMBER` when referencing issues +5. **Keep commits atomic** - One logical change per commit +6. **Write descriptive messages** - Future you will thank present you diff --git a/.github/skills/dev/maintenance/update-dependencies/skill.md b/.github/skills/dev/maintenance/update-dependencies/skill.md new file mode 100644 index 00000000..3b2688cb --- /dev/null +++ b/.github/skills/dev/maintenance/update-dependencies/skill.md @@ -0,0 +1,317 @@ +--- +name: update-dependencies +description: Guide for updating project dependencies using the update-dependencies.sh automation script. Automates the cargo update workflow including branch creation, commit, push, and optional PR creation. Use when updating dependencies, running cargo update, or automating the dependency lifecycle. Triggers on "update dependencies", "cargo update", "update deps", "bump dependencies", or "run dependency update". +metadata: + author: torrust + version: "1.0" +--- + +# Updating Dependencies + +This skill guides you through updating project dependencies using the `scripts/update-dependencies.sh` automation script, which handles the complete dependency update workflow from branch creation to PR submission. + +## Quick Reference + +```bash +# Simple update (no issue) +./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote {fork-remote} \ + --create-pr + +# Complex update with issue +./scripts/update-dependencies.sh \ + --branch {issue-number}-update-dependencies \ + --push-remote {fork-remote} \ + --create-pr +``` + +## Workflow Overview + +The `update-dependencies.sh` script automates the following steps: + +1. Ensures a clean working tree (no uncommitted changes) +2. Fetches and fast-forwards the base branch from upstream +3. Creates a feature branch with the specified name +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` (default: enabled) +7. **Commits** the `Cargo.lock` changes with full `cargo update` output in commit body +8. **Pushes** the branch to the fork remote +9. **Creates a PR** on GitHub (optional, default: disabled) + +## Usage + +### Basic Invocation + +```bash +./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote {fork-remote} +``` + +### With PR Creation + +```bash +./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote {fork-remote} \ + --create-pr +``` + +### Signing Commits + +Commits are **always signed** with `git commit -S` (GPG signing is mandatory): + +```bash +./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote {fork-remote} +``` + +Unsigned commits are not permitted in this workflow. + +### Skipping Pre-Commit Checks + +Pre-commit checks are run by default. Skip them if needed (not recommended): + +```bash +./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote {fork-remote} \ + --skip-pre-commit +``` + +### Deleting Existing Branch + +If a branch with the same name already exists, delete it first: + +```bash +./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote {fork-remote} \ + --delete-existing-branch +``` + +## All Options + +```bash +./scripts/update-dependencies.sh --help +``` + +| Option | Default | Description | +| -------------------------- | ---------------------------- | ------------------------------------------------------ | +| `--branch` | **required** | Feature branch name (e.g., `123-update-deps`) | +| `--base-branch` | `main` | Target branch for merge base | +| `--base-remote` | Auto-detected | Remote for base branch (prefers `torrust` → `origin`) | +| `--push-remote` | Auto-detected | Remote to push the branch to | +| `--repo` | Auto-detected | GitHub repo slug (owner/repo) | +| `--commit-title` | `chore: update dependencies` | First line of commit message | +| `--pr-title` | `chore: update dependencies` | Pull request title | +| `--skip-pre-commit` | disabled | Skip `./scripts/pre-commit.sh` after update | +| `--create-pr` | disabled | Create a PR after pushing | +| `--delete-existing-branch` | disabled | Delete the branch locally and remotely before starting | +| `--help` | — | Show full usage and all options | + +## When to Create an Issue + +- **Simple updates**: Just running `cargo update` with no special handling → **No issue needed**, use branch name `update-dependencies` +- **Complex updates**: Dependency updates requiring additional changes (migrations, API updates, refactoring) → **Create an issue** and use branch name `{issue-number}-update-dependencies` + +This keeps the issue tracker focused on substantial work while allowing for routine maintenance tasks without issue clutter. + +## Step-by-Step Example (Simple Update) + +### 1. Run the Script (No Issue) + +```bash +./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote {fork-remote} \ + --create-pr +``` + +## Step-by-Step Example (Complex Update with Issue) + +### 1. Create an Issue + +```bash +gh issue create \ + --title "chore: update dependencies and migrate to new API" \ + --body "Update dependencies and handle breaking changes in async library." +``` + +Note the issue number (e.g., `#456`). + +### 2. Run the Script + +```bash +./scripts/update-dependencies.sh \ + --branch {issue-number}-update-dependencies \ + --push-remote {fork-remote} \ + --create-pr +``` + +### 3. Observe the Output + +The script will: + +- Fetch the latest `main` from `torrust` remote +- Create branch `456-update-dependencies` +- Run `cargo update` and show the output +- If dependencies changed: run pre-commit, commit with full output, push, create PR +- If no changes: clean up branch and exit (no-op, which is fine) + +### 4. Review and Merge + +- Visit the PR created by the script (URL printed to stdout) +- Review the `cargo update` output in the commit body +- Let CI checks pass +- Merge when ready + +## Commit Message Format + +The script generates commit messages in this format: + +```text +chore: update dependencies + +[Full cargo update output] + +- run `cargo update` +- commit the resulting `Cargo.lock` changes +``` + +The full `cargo update` output is included in the commit body for traceability. Example: + +```text + Updating crates.io index + Locking 14 packages to latest compatible versions + Updating hyper-rustls from 0.27.7 to 0.27.8 + ... +``` + +## Pre-Commit Checks + +Before committing, the script optionally runs `./scripts/pre-commit.sh` (enabled by default), which verifies: + +1. **Unused dependencies**: `cargo machete` +2. **All linters**: markdown, YAML, TOML, spelling, Clippy, rustfmt, shellcheck +3. **Tests**: `cargo test` +4. **Documentation**: `cargo doc --no-deps` +5. **E2E infrastructure tests**: provisioning and destruction +6. **E2E deployment tests**: full workflow + +If pre-commit fails, the script exits before committing. Fix issues and run the script again. + +### Skip Pre-Commit (Not Recommended) + +```bash +./scripts/update-dependencies.sh \ + --branch {issue-number}-update-dependencies \ + --push-remote {fork-remote} \ + --skip-pre-commit +``` + +## When Dependencies Don't Change + +If `cargo update` produces no changes to `Cargo.lock`, the script will: + +1. Print: `No dependency changes were produced by cargo update` +2. Clean up the feature branch (delete it locally) +3. Exit cleanly with code 0 (success) + +This is **not an error** — it means all dependencies are at their latest compatible versions. + +## Troubleshooting + +### Error: Working tree has unstaged changes + +The script requires a clean working tree. Stage or remove all uncommitted changes: + +```bash +git status # See what's uncommitted +git add # Stage changes +git commit -m "..." # Or commit them +git stash # Or stash them +``` + +Then retry the script. + +### Error: Branch already exists + +The branch exists either locally or on the remote: + +```bash +# Option 1: Use a different branch name +./scripts/update-dependencies.sh \ + --branch {issue-number}-update-dependencies-retry \ + --push-remote {fork-remote} + +# Option 2: Delete the existing branch first +./scripts/update-dependencies.sh \ + --branch {issue-number}-update-dependencies \ + --push-remote {fork-remote} \ + --delete-existing-branch +``` + +### Commit Signing Failures + +GPG signing is mandatory. If commit signing fails: + +```bash +# Check GPG setup +gpg --list-keys + +# Fix GPG configuration, then retry +gh auth logout && gh auth login # Re-authenticate if needed +``` + +Ensure GPG is properly configured before running the script. Unsigned commits are not permitted. + +### PR Creation Fails + +Ensure `gh` CLI is authenticated: + +```bash +gh auth status # Check authentication +gh auth login # Log in if needed +``` + +Then retry with `--create-pr`. + +## After Merge + +Once the PR is merged to `main`: + +1. The updated `Cargo.lock` is now in the base branch +2. All future branches will build with the new dependencies +3. Repeat the workflow for the next update cycle (typically monthly or as needed) + +## Integration with CI + +When the PR is created, GitHub Actions will automatically run: + +- All linters (stable + nightly) +- Full test suite +- E2E infrastructure tests +- E2E deployment tests +- Coverage analysis + +All checks must pass before merging. + +## Best Practices + +- **Run regularly**: Update dependencies monthly or quarterly +- **Always sign commits**: Use GPG signing (default behavior) for audit trails +- **Review the output**: Check the commit message to see which packages were updated +- **Run pre-commit**: Never skip this step (use default behavior) +- **Use issue numbers**: Prefix branch names with issue numbers (`--branch {issue}-update-dependencies`) +- **One branch per run**: Each run creates one branch and optionally one PR +- **Wait for CI**: Never merge until all checks pass + +## See Also + +- [Committing Changes](../../git-workflow/commit-changes/skill.md) — General commit workflow +- [Creating Feature Branches](../../git-workflow/create-feature-branch/skill.md) — Branch naming conventions +- [Pre-Commit Checks](../../git-workflow/run-pre-commit-checks/skill.md) — Understanding the 6-step verification process diff --git a/AGENTS.md b/AGENTS.md index f1d16c84..3ab2381e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,6 +134,7 @@ For detailed information about working with deployed instances, see [`docs/user- - Individual linters: `cargo run --bin linter {markdown|yaml|toml|cspell|clippy|rustfmt|shellcheck}` - Alternative: `./scripts/lint.sh` (wrapper that calls the Rust binary) - **Dependencies**: `cargo machete` (mandatory before commits - no unused dependencies) +- **Commit Signing**: All commits **must** be signed with GPG (`git commit -S`) for audit trail - **Build**: `cargo build` - **Test**: `cargo test` - **Unit Tests**: When writing unit tests, follow conventions described in [`docs/contributing/testing/`](docs/contributing/testing/) diff --git a/docs/contributing/README.md b/docs/contributing/README.md index af71f0d5..ae42afba 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -62,6 +62,24 @@ git commit -m "feat: [#42] add new testing feature" git push origin 42-add-your-feature-name ``` +## Dependency Update Automation + +For dependency-only updates, you can automate the repetitive git and PR workflow with: + +```bash +./scripts/update-dependencies.sh \ + --branch 445-update-dependencies \ + --push-remote josecelano \ + --create-pr +``` + +Notes: + +- The script signs commits by default with `git commit -S`. +- The push remote and branch name are explicit so the workflow works with different forks. +- Reuse `--delete-existing-branch` only when you intentionally want to replace an older update branch. +- Use `--help` to see all options. + ## 📖 Additional Resources - [Main Documentation](../documentation.md) - Project documentation organization diff --git a/project-words.txt b/project-words.txt index 45eac71b..0e5ba1d7 100644 --- a/project-words.txt +++ b/project-words.txt @@ -216,6 +216,9 @@ doctests downcasted downcasting downloadedi +pinentry +signingkey +clearsign dpkg drwxr drwxrwxr @@ -541,6 +544,7 @@ youruser zcat zeroize zoneinfo +worktree zstd CSPRNG USERINFO diff --git a/scripts/update-dependencies.sh b/scripts/update-dependencies.sh new file mode 100755 index 00000000..9e413d99 --- /dev/null +++ b/scripts/update-dependencies.sh @@ -0,0 +1,306 @@ +#!/bin/bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: ./scripts/update-dependencies.sh --branch [options] + +Automates the dependency update workflow: +- sync the base branch from the base remote +- create a fresh working branch +- run cargo update +- run pre-commit checks +- create a signed commit with the full cargo update output +- optionally push the branch and create a pull request + +Options: + --branch Working branch to create (required) + --base-branch Base branch to update from (default: main) + --base-remote Remote that owns the base branch (default: torrust, then origin, then first remote) + --push-remote Remote used to push the branch + --repo Repository slug for PR creation + --commit-title Commit title and default PR title + --pr-title <title> Pull request title override + --delete-existing-branch Delete an existing local/remote branch with the same name + --skip-pre-commit Skip ./scripts/pre-commit.sh + --create-pr Create a PR after pushing the branch + --no-sign-commit Do not use git commit -S + --help Show this help message + +Examples: + ./scripts/update-dependencies.sh \ + --branch 445-update-dependencies \ + --push-remote josecelano \ + --create-pr + + ./scripts/update-dependencies.sh \ + --branch update-dependencies \ + --push-remote josecelano \ + --delete-existing-branch \ + --commit-title "chore: update dependencies" +EOF +} + +log() { + echo "[update-dependencies] $*" +} + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +detect_default_remote() { + if git remote | grep -qx "torrust"; then + echo "torrust" + return + fi + + if git remote | grep -qx "origin"; then + echo "origin" + return + fi + + git remote | head -n 1 +} + +parse_github_slug_from_remote() { + local remote=$1 + local remote_url + local slug + + remote_url=$(git remote get-url "$remote") + slug=$(printf '%s' "$remote_url" | sed -E 's#^(git@github.com:|https://github.com/|ssh://git@github.com/)##; s#\.git$##') + + [[ "$slug" == */* ]] || fail "Could not parse GitHub repository slug from remote '$remote'" + + echo "$slug" +} + +parse_github_owner_from_remote() { + local remote=$1 + local slug + + slug=$(parse_github_slug_from_remote "$remote") + echo "${slug%%/*}" +} + +branch_exists_local() { + local branch=$1 + git show-ref --verify --quiet "refs/heads/$branch" +} + +branch_exists_remote() { + local remote=$1 + local branch=$2 + git ls-remote --exit-code --heads "$remote" "$branch" >/dev/null 2>&1 +} + +ensure_clean_worktree() { + git diff --quiet || fail "Working tree has unstaged changes" + git diff --cached --quiet || fail "Working tree has staged changes" +} + +cleanup_files() { + rm -f "$CARGO_UPDATE_OUTPUT_FILE" "$COMMIT_MESSAGE_FILE" "$PR_BODY_FILE" +} + +BRANCH_NAME="" +BASE_BRANCH="main" +BASE_REMOTE="" +PUSH_REMOTE="" +REPOSITORY_SLUG="" +COMMIT_TITLE="chore: update dependencies" +PR_TITLE="" +DELETE_EXISTING_BRANCH=false +RUN_PRE_COMMIT=true +CREATE_PR=false +SIGN_COMMIT=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --branch) + BRANCH_NAME=${2:-} + shift 2 + ;; + --base-branch) + BASE_BRANCH=${2:-} + shift 2 + ;; + --base-remote) + BASE_REMOTE=${2:-} + shift 2 + ;; + --push-remote) + PUSH_REMOTE=${2:-} + shift 2 + ;; + --repo) + REPOSITORY_SLUG=${2:-} + shift 2 + ;; + --commit-title) + COMMIT_TITLE=${2:-} + shift 2 + ;; + --pr-title) + PR_TITLE=${2:-} + shift 2 + ;; + --delete-existing-branch) + DELETE_EXISTING_BRANCH=true + shift + ;; + --skip-pre-commit) + RUN_PRE_COMMIT=false + shift + ;; + --create-pr) + CREATE_PR=true + shift + ;; + --no-sign-commit) + SIGN_COMMIT=false + shift + ;; + --help) + usage + exit 0 + ;; + *) + fail "Unknown option: $1" + ;; + esac +done + +[[ -n "$BRANCH_NAME" ]] || fail "--branch is required" + +command_exists git || fail "git is required" +command_exists cargo || fail "cargo is required" + +BASE_REMOTE=${BASE_REMOTE:-$(detect_default_remote)} +[[ -n "$BASE_REMOTE" ]] || fail "Could not determine a base remote" + +if [[ -z "$REPOSITORY_SLUG" ]]; then + REPOSITORY_SLUG=$(parse_github_slug_from_remote "$BASE_REMOTE") +fi + +if [[ "$CREATE_PR" == true ]]; then + [[ -n "$PUSH_REMOTE" ]] || fail "--push-remote is required when --create-pr is used" + command_exists gh || fail "gh is required when --create-pr is used" +fi + +if [[ -n "$PUSH_REMOTE" ]]; then + git remote get-url "$PUSH_REMOTE" >/dev/null 2>&1 || fail "Remote '$PUSH_REMOTE' does not exist" +fi + +CARGO_UPDATE_OUTPUT_FILE=$(mktemp) +COMMIT_MESSAGE_FILE=$(mktemp) +PR_BODY_FILE=$(mktemp) +trap cleanup_files EXIT + +ensure_clean_worktree + +if branch_exists_local "$BRANCH_NAME"; then + if [[ "$DELETE_EXISTING_BRANCH" == true ]]; then + log "Deleting local branch '$BRANCH_NAME'" + current_branch=$(git branch --show-current) + if [[ "$current_branch" == "$BRANCH_NAME" ]]; then + git checkout "$BASE_BRANCH" + fi + git branch -D "$BRANCH_NAME" + else + fail "Local branch '$BRANCH_NAME' already exists. Use --delete-existing-branch to replace it." + fi +fi + +if [[ -n "$PUSH_REMOTE" ]] && branch_exists_remote "$PUSH_REMOTE" "$BRANCH_NAME"; then + if [[ "$DELETE_EXISTING_BRANCH" == true ]]; then + log "Deleting remote branch '$BRANCH_NAME' from '$PUSH_REMOTE'" + git push "$PUSH_REMOTE" --delete "$BRANCH_NAME" + else + fail "Remote branch '$BRANCH_NAME' already exists on '$PUSH_REMOTE'. Use --delete-existing-branch to replace it." + fi +fi + +log "Fetching '$BASE_REMOTE/$BASE_BRANCH'" +git fetch "$BASE_REMOTE" "$BASE_BRANCH" + +log "Checking out '$BASE_BRANCH'" +git checkout "$BASE_BRANCH" + +log "Fast-forwarding '$BASE_BRANCH' from '$BASE_REMOTE/$BASE_BRANCH'" +git merge --ff-only "$BASE_REMOTE/$BASE_BRANCH" + +log "Creating branch '$BRANCH_NAME'" +git checkout -b "$BRANCH_NAME" + +log "Running cargo update" +cargo update 2>&1 | tee "$CARGO_UPDATE_OUTPUT_FILE" + +if git diff --quiet; then + log "No dependency changes were produced by cargo update" + git checkout "$BASE_BRANCH" + git branch -D "$BRANCH_NAME" + exit 0 +fi + +if [[ "$RUN_PRE_COMMIT" == true ]]; then + log "Running pre-commit checks" + ./scripts/pre-commit.sh + PRE_COMMIT_SUMMARY="- run \`./scripts/pre-commit.sh\` successfully" +else + PRE_COMMIT_SUMMARY="- skip \`./scripts/pre-commit.sh\` by request" +fi + +{ + printf '%s\n\n' "$COMMIT_TITLE" + printf '%s\n' 'cargo update output:' + printf '%s\n' '```' + cat "$CARGO_UPDATE_OUTPUT_FILE" + printf '%s\n' '```' +} > "$COMMIT_MESSAGE_FILE" + +log "Creating commit" +git add -u +if [[ "$SIGN_COMMIT" == true ]]; then + git commit -S -F "$COMMIT_MESSAGE_FILE" +else + git commit -F "$COMMIT_MESSAGE_FILE" +fi + +if [[ -n "$PUSH_REMOTE" ]]; then + log "Pushing branch to '$PUSH_REMOTE'" + git push -u "$PUSH_REMOTE" "$BRANCH_NAME" +fi + +if [[ "$CREATE_PR" == true ]]; then + HEAD_OWNER=$(parse_github_owner_from_remote "$PUSH_REMOTE") + PR_TITLE=${PR_TITLE:-$COMMIT_TITLE} + + { + printf '%s\n' '## Summary' + printf '%s\n' "- run \`cargo update\`" + printf '%s\n' "- commit the resulting \`Cargo.lock\` changes" + printf '%s\n\n' "$PRE_COMMIT_SUMMARY" + printf '%s\n' '## cargo update output' + printf '%s\n' '```' + cat "$CARGO_UPDATE_OUTPUT_FILE" + printf '%s\n' '```' + } > "$PR_BODY_FILE" + + log "Creating pull request in '$REPOSITORY_SLUG'" + gh pr create \ + --repo "$REPOSITORY_SLUG" \ + --base "$BASE_BRANCH" \ + --head "$HEAD_OWNER:$BRANCH_NAME" \ + --title "$PR_TITLE" \ + --body-file "$PR_BODY_FILE" +fi + +log "Dependency update workflow completed"