diff --git a/.github/skills/dev/git-workflow/open-pull-request/skill.md b/.github/skills/dev/git-workflow/open-pull-request/skill.md new file mode 100644 index 00000000..4277f1b3 --- /dev/null +++ b/.github/skills/dev/git-workflow/open-pull-request/skill.md @@ -0,0 +1,100 @@ +--- +name: open-pull-request +description: Open a pull request from a feature branch using GitHub CLI (preferred) or GitHub MCP tools. Covers pre-flight checks, correct base/head configuration for fork workflows, title/body conventions, and post-creation validation. Use when asked to "open PR", "create pull request", or "submit branch for review". +metadata: + author: torrust + version: "1.0" +--- + +# Open a Pull Request + +This skill explains how to create a pull request for this repository in a repeatable way. + +## CLI vs MCP decision rule + +Use the tool that matches the loop: + +- **Inner loop (fast local branch work):** prefer GitHub CLI (`gh`) because it is fast and low overhead. +- **Outer loop (cross-system coordination):** use MCP when you need structured/authenticated access across shared systems. + +For opening a PR from the current local branch, prefer `gh pr create`. + +## Pre-flight checks + +Before opening a PR: + +- [ ] Working tree is clean (`git status`) +- [ ] Branch is pushed to remote +- [ ] Commits are signed (`git log --show-signature -n 1`) +- [ ] Required checks have been run (`./scripts/pre-commit.sh`) + +## Title and description convention + +Use conventional commit style in the PR title when possible, including issue reference. + +Examples: + +- `ci: [#448] add crate publish workflow` +- `docs: [#448] define release process` + +Include in PR body: + +- Summary of changes +- Files/workflows touched +- Validation performed +- Issue link (`Closes #`) + +## Option A (Preferred): GitHub CLI + +### Same-repo branch + +```bash +gh pr create \ + --repo torrust/torrust-tracker-deployer \ + --base main \ + --head \ + --title "" \ + --body "<body>" +``` + +### Fork branch (common maintainer flow) + +```bash +gh pr create \ + --repo torrust/torrust-tracker-deployer \ + --base main \ + --head <fork-owner>:<branch-name> \ + --title "<title>" \ + --body "<body>" +``` + +If successful, `gh` prints the PR URL. + +## Option B: GitHub MCP tools + +When MCP pull request management tools are available: + +1. Create branch remotely if needed +2. Open PR with base `main` and correct head branch +3. Capture and share resulting PR URL + +## Post-creation validation + +After PR creation: + +- [ ] Verify PR points to `torrust/torrust-tracker-deployer:main` +- [ ] Verify head branch is correct +- [ ] Confirm CI workflows started +- [ ] Confirm issue is linked in description + +## Troubleshooting + +- `fatal: ... does not appear to be a git repository`: push to correct remote (`git remote -v`) +- `A pull request already exists`: open existing PR URL instead of creating a new one +- Permission errors on upstream repo: create PR from your fork branch (`owner:branch`) + +## References + +- [`docs/contributing/commit-process.md`](../../../../../docs/contributing/commit-process.md) +- [`docs/contributing/pr-review-guide.md`](../../../../../docs/contributing/pr-review-guide.md) +- Existing branch skill: `.github/skills/dev/git-workflow/create-feature-branch/skill.md` diff --git a/.github/skills/dev/git-workflow/release-new-version/skill.md b/.github/skills/dev/git-workflow/release-new-version/skill.md new file mode 100644 index 00000000..48e99b5e --- /dev/null +++ b/.github/skills/dev/git-workflow/release-new-version/skill.md @@ -0,0 +1,102 @@ +--- +name: release-new-version +description: Guide for releasing a new version of the deployer using the standard branch/tag workflow. Covers version bump, signed release commit, pushing main, creating signed tag, creating release branch, and verifying Docker + crate publication workflows. Use when asked to "release", "cut a version", "publish a new version", or "create release vX.Y.Z". +metadata: + author: torrust + version: "1.0" +--- + +# Release New Version + +This skill provides the canonical workflow to release a new version of the Torrust Tracker Deployer. + +Primary reference: [`docs/release-process.md`](../../../../../docs/release-process.md) + +## Release Order (Mandatory) + +Execute these steps in order: + +1. Update versions in manifests +2. Create release commit +3. Push release commit to `main` +4. Create and push signed tag `vX.Y.Z` +5. Create and push release branch `releases/vX.Y.Z` +6. Verify release workflows +7. Create GitHub release + +Do not reorder these steps. + +## Version and Naming Rules + +- Git tag: `vX.Y.Z` +- Release branch: `releases/vX.Y.Z` +- Docker release tag: `X.Y.Z` (no `v` prefix) +- Crate version: `X.Y.Z` + +## Pre-Flight Checklist + +Before starting: + +- [ ] Clean working tree (`git status`) +- [ ] Up to date with `origin/main` +- [ ] GitHub environment `dockerhub-torrust` configured +- [ ] GitHub environment `crates-io` configured with `CARGO_REGISTRY_TOKEN` +- [ ] Releaser has permissions for `main`, tags, and release branches + +## Commands + +### 1) Update versions + +Update `version` in: + +- `Cargo.toml` +- `packages/sdk/Cargo.toml` + +### 2) Commit and push + +```bash +git add Cargo.toml packages/sdk/Cargo.toml +git commit -S -m "release: version vX.Y.Z" +git push origin main +``` + +### 3) Tag and release branch + +```bash +git tag -s -a vX.Y.Z -m "Release vX.Y.Z" +git push origin vX.Y.Z + +git checkout -b releases/vX.Y.Z +git push origin releases/vX.Y.Z +``` + +### 4) Verify workflows + +- Container workflow: publishes Docker image from release branch +- Publish Crate workflow: publishes `torrust-tracker-deployer-sdk` + +Workflow files: + +- `.github/workflows/container.yaml` +- `.github/workflows/publish-crate.yaml` + +### 5) Create GitHub release + +Create the release manually from tag `vX.Y.Z` after both workflows pass. + +## Failure Handling + +- Docker failed, crate not started: fix Docker workflow and rerun on same release branch +- Docker passed, crate failed before upload: fix issue and rerun crate workflow on same release branch +- Crate already published: do not republish same version; cut a patch release +- Ref already exists (tag/branch): stop and investigate partial release state before continuing + +## Quick Validation + +```bash +# Verify refs exist remotely +git ls-remote --tags origin vX.Y.Z +git ls-remote --heads origin releases/vX.Y.Z +``` + +For full operational guidance, troubleshooting, and rollback/yank policy, use [`docs/release-process.md`](../../../../../docs/release-process.md). diff --git a/.github/workflows/container.yaml b/.github/workflows/container.yaml index 1f76a66b..45884475 100644 --- a/.github/workflows/container.yaml +++ b/.github/workflows/container.yaml @@ -4,13 +4,15 @@ # Following patterns from torrust/torrust-tracker container.yaml workflow. # # Triggers: -# - Push to main/develop branches +# - Push to main/develop/releases/** branches # - Pull requests to main/develop # - Manual dispatch # # Publishing: -# - Images are pushed to Docker Hub on push to main/develop (not PRs) -# - Requires Docker Hub credentials in repository secrets +# - Images are pushed to Docker Hub on push to main/develop/release branches (not PRs) +# - Release branches (releases/vX.Y.Z) publish versioned Docker tags (X.Y.Z) +# - Release Docker tags use bare semver without the v prefix +# - Requires Docker Hub credentials in the dockerhub-torrust GitHub Environment name: Container @@ -19,6 +21,7 @@ on: branches: - "develop" - "main" + - "releases/**/*" paths: - "src/**" - "Cargo.toml" @@ -100,6 +103,7 @@ jobs: outputs: continue: ${{ steps.check.outputs.continue }} type: ${{ steps.check.outputs.type }} + version: ${{ steps.check.outputs.version }} steps: - name: Check Context @@ -108,10 +112,15 @@ jobs: if [[ "${{ github.repository }}" == "torrust/torrust-tracker-deployer" ]]; then if [[ "${{ github.event_name }}" == "push" ]]; then if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then - echo "type=production" >> $GITHUB_OUTPUT + echo "type=main" >> $GITHUB_OUTPUT echo "continue=true" >> $GITHUB_OUTPUT elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then - echo "type=development" >> $GITHUB_OUTPUT + echo "type=develop" >> $GITHUB_OUTPUT + echo "continue=true" >> $GITHUB_OUTPUT + elif [[ $(echo "${{ github.ref }}" | grep -P '^refs/heads/releases/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$') ]]; then + version=$(echo "${{ github.ref }}" | sed -n -E 's/^refs\/heads\/releases\///p') + echo "version=$version" >> $GITHUB_OUTPUT + echo "type=release" >> $GITHUB_OUTPUT echo "continue=true" >> $GITHUB_OUTPUT fi fi @@ -122,11 +131,11 @@ jobs: echo "continue=false" >> $GITHUB_OUTPUT fi - publish_development: - name: Publish (Development) + publish: + name: Publish (${{ needs.context.outputs.type }}) environment: dockerhub-torrust needs: context - if: needs.context.outputs.continue == 'true' && needs.context.outputs.type == 'development' + if: needs.context.outputs.continue == 'true' runs-on: ubuntu-latest timeout-minutes: 30 @@ -134,48 +143,34 @@ jobs: - name: Checkout uses: actions/checkout@v5 - - name: Docker Meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ env.DOCKER_HUB_USERNAME }}/tracker-deployer - tags: | - type=ref,event=branch - type=sha,prefix=dev- - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ env.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and Push - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/deployer/Dockerfile - target: release - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - publish_production: - name: Publish (Production) - environment: dockerhub-torrust - needs: context - if: needs.context.outputs.continue == 'true' && needs.context.outputs.type == 'production' - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout - uses: actions/checkout@v5 + - name: Configure Docker Tag Strategy + id: tag_config + run: | + if [[ "${{ needs.context.outputs.type }}" == "develop" ]]; then + { + echo "tags<<EOF" + echo "type=ref,event=branch" + echo "type=sha,prefix=dev-" + echo "EOF" + } >> "$GITHUB_OUTPUT" + elif [[ "${{ needs.context.outputs.type }}" == "main" ]]; then + { + echo "tags<<EOF" + echo "type=raw,value=latest" + echo "type=ref,event=branch" + echo "type=sha" + echo "EOF" + } >> "$GITHUB_OUTPUT" + elif [[ "${{ needs.context.outputs.type }}" == "release" ]]; then + { + echo "tags<<EOF" + echo "type=semver,value=${{ needs.context.outputs.version }},pattern={{version}}" + echo "EOF" + } >> "$GITHUB_OUTPUT" + else + echo "Unsupported publish type: ${{ needs.context.outputs.type }}" >&2 + exit 1 + fi - name: Docker Meta id: meta @@ -183,10 +178,7 @@ jobs: with: images: | ${{ env.DOCKER_HUB_USERNAME }}/tracker-deployer - tags: | - type=raw,value=latest - type=ref,event=branch - type=sha + tags: ${{ steps.tag_config.outputs.tags }} - name: Login to Docker Hub uses: docker/login-action@v3 @@ -208,3 +200,10 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + - name: Inspect Published Image + if: needs.context.outputs.type == 'release' + run: | + version=$(echo "${{ needs.context.outputs.version }}" | sed 's/^v//') + docker pull ${{ env.DOCKER_HUB_USERNAME }}/tracker-deployer:"$version" + docker image inspect ${{ env.DOCKER_HUB_USERNAME }}/tracker-deployer:"$version" diff --git a/.github/workflows/publish-crate.yaml b/.github/workflows/publish-crate.yaml new file mode 100644 index 00000000..6a5da8ce --- /dev/null +++ b/.github/workflows/publish-crate.yaml @@ -0,0 +1,137 @@ +# Crate publication workflow for Torrust Tracker Deployer SDK +# +# This workflow publishes the SDK crate when a release branch is pushed. +# Trigger branch format: releases/vX.Y.Z + +name: Publish Crate + +on: + push: + branches: + - "releases/**/*" + paths: + - "Cargo.toml" + - "Cargo.lock" + - "packages/sdk/**" + - ".github/workflows/publish-crate.yaml" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + CRATE_NAME: torrust-tracker-deployer-sdk + +jobs: + publish_sdk: + name: Publish SDK Crate + environment: crates-io + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout Repository + uses: actions/checkout@v5 + + - name: Setup Rust Toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Enable Workflow Cache + uses: Swatinem/rust-cache@v2 + + - name: Extract Release Version + id: release + run: | + if [[ "${{ github.ref_name }}" =~ ^releases/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + version="${GITHUB_REF_NAME#releases/v}" + echo "version=$version" >> "$GITHUB_OUTPUT" + else + echo "Invalid release branch name: ${{ github.ref_name }}" >&2 + echo "Expected format: releases/vX.Y.Z" >&2 + exit 1 + fi + + - name: Verify Release Version Matches Cargo Manifests + run: | + root_version=$(grep '^version = ' Cargo.toml | head -1 | sed -E 's/version = "([^"]+)"/\1/') + sdk_version=$(grep '^version = ' packages/sdk/Cargo.toml | head -1 | sed -E 's/version = "([^"]+)"/\1/') + release_version="${{ steps.release.outputs.version }}" + + if [[ "$root_version" != "$release_version" ]]; then + echo "Root Cargo.toml version mismatch: $root_version != $release_version" >&2 + exit 1 + fi + + if [[ "$sdk_version" != "$release_version" ]]; then + echo "SDK Cargo.toml version mismatch: $sdk_version != $release_version" >&2 + exit 1 + fi + + - name: Verify SDK Metadata + run: | + for field in description license repository readme; do + if ! grep -q "^$field = " packages/sdk/Cargo.toml; then + echo "Missing required field in packages/sdk/Cargo.toml: $field" >&2 + exit 1 + fi + done + + - name: Run SDK Tests + run: cargo test -p ${{ env.CRATE_NAME }} + + - name: Inspect Packaged Files + run: cargo package --list -p ${{ env.CRATE_NAME }} + + - name: Dry Run Publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --dry-run -p ${{ env.CRATE_NAME }} + + - name: Check If Version Already Published + run: | + set +e + http_status=$(curl -s -o /tmp/crate-version.json -w "%{http_code}" "https://crates.io/api/v1/crates/${{ env.CRATE_NAME }}/${{ steps.release.outputs.version }}") + set -e + + if [[ "$http_status" == "200" ]]; then + echo "Crate version already published: ${{ env.CRATE_NAME }} ${{ steps.release.outputs.version }}" >&2 + echo "Do not republish. Cut a follow-up patch release instead." >&2 + exit 1 + fi + + - name: Publish Crate + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p ${{ env.CRATE_NAME }} + + - name: Verify Crate Is Available + run: | + for attempt in 1 2 3 4 5; do + status=$(curl -s -o /tmp/crate-publish-check.json -w "%{http_code}" "https://crates.io/api/v1/crates/${{ env.CRATE_NAME }}/${{ steps.release.outputs.version }}") + if [[ "$status" == "200" ]]; then + echo "Crate is available on crates.io" + exit 0 + fi + echo "Waiting for crates.io index update (attempt $attempt/5)..." + sleep 10 + done + + echo "Crate was published but not visible yet. Check crates.io manually." >&2 + exit 1 + + - name: Verify docs.rs Build + run: | + docs_url="https://docs.rs/${{ env.CRATE_NAME }}/${{ steps.release.outputs.version }}" + for attempt in 1 2 3 4 5 6; do + status=$(curl -s -o /tmp/docs-rs-check.html -w "%{http_code}" "$docs_url") + if [[ "$status" == "200" ]]; then + echo "docs.rs page is available: $docs_url" + exit 0 + fi + echo "Waiting for docs.rs build (attempt $attempt/6)..." + sleep 20 + done + + echo "docs.rs page is not available yet: $docs_url" >&2 + echo "The crate may still be building on docs.rs; verify manually later." >&2 + exit 1 diff --git a/AGENTS.md b/AGENTS.md index 65b4490d..e645b3a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -203,8 +203,10 @@ Available skills: | Handling user output | `.github/skills/dev/cli/handle-user-output/skill.md` | | Implementing domain types | `.github/skills/dev/rust-code-quality/implement-domain-types/skill.md` | | Installing system dependencies | `.github/skills/usage/operations/install-system-dependencies/skill.md` | +| Opening pull requests | `.github/skills/dev/git-workflow/open-pull-request/skill.md` | | Organizing Rust modules | `.github/skills/dev/rust-code-quality/organize-rust-modules/skill.md` | | Placing code in DDD layers | `.github/skills/dev/rust-code-quality/place-code-in-ddd-layers/skill.md` | +| Releasing a new version | `.github/skills/dev/git-workflow/release-new-version/skill.md` | | Regenerating CLI docs | `.github/skills/dev/cli/regenerate-cli-docs/skill.md` | | Rendering tracker artifacts | `.github/skills/usage/operations/render-tracker-artifacts/skill.md` | | Reviewing pull requests | `.github/skills/dev/git-workflow/review-pr/skill.md` | diff --git a/README.md b/README.md index 1bbdbf55..b36b2363 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ [![Cargo Security Audit](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/cargo-security-audit.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/cargo-security-audit.yml) [![Docker Security Scan](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml) [![Container](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/container.yaml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/container.yaml) +[![Publish Crate](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/publish-crate.yaml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/publish-crate.yaml) [![Backup Container](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/backup-container.yaml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/backup-container.yaml) [![Code Statistics](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/code-statistics.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/code-statistics.yml) diff --git a/docs/README.md b/docs/README.md index 31384576..d7537948 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ Welcome to the Torrust Tracker Deployer documentation! This index helps you quic | Place code in the correct layer | [`contributing/ddd-layer-placement.md`](contributing/ddd-layer-placement.md) - **CRITICAL** decision flowchart | | Follow development principles | [`development-principles.md`](development-principles.md) - Observability, Testability, User Friendliness, Actionability | | Commit code | [`contributing/commit-process.md`](contributing/commit-process.md) - Pre-commit checks, conventional commits | +| Cut a release | [`release-process.md`](release-process.md) - Standard release sequence, branch/tag rules, Docker + crate publication | | Handle errors properly | [`contributing/error-handling.md`](contributing/error-handling.md) - Explicit enums, actionable messages | | Handle output properly | [`contributing/output-handling.md`](contributing/output-handling.md) - **CRITICAL** UserOutput, never `println!` | | Organize Rust code | [`contributing/module-organization.md`](contributing/module-organization.md) - **CRITICAL** import conventions | @@ -83,6 +84,7 @@ docs/ | Write unit tests | [`contributing/testing/unit-testing.md`](contributing/testing/unit-testing.md) | | Understand a decision | [`decisions/README.md`](decisions/README.md) | | Plan a new feature | [`features/README.md`](features/README.md) | +| Perform a release | [`release-process.md`](release-process.md) | | Fix external tool issues | [`external-issues/README.md`](external-issues/README.md) | | Work with templates | [`contributing/templates/`](contributing/templates/) | | Handle errors properly | [`contributing/error-handling.md`](contributing/error-handling.md) | diff --git a/docs/issues/448-release-process-branch-tag-docker-crate.md b/docs/issues/448-release-process-branch-tag-docker-crate.md index 5be5971c..723ac23e 100644 --- a/docs/issues/448-release-process-branch-tag-docker-crate.md +++ b/docs/issues/448-release-process-branch-tag-docker-crate.md @@ -25,20 +25,20 @@ The initial release process should include these mandatory steps in order: 3. Push the release commit to `main` 4. Create the release tag and push it 5. Create the release branch and push it -6. Create the GitHub release from the tag -7. Let GitHub Actions publish release artifacts: +6. Let GitHub Actions publish release artifacts: - Docker image for the release branch - Crate for the release branch +7. Create the GitHub release from the tag ## Goals -- [ ] Define a single documented release workflow with explicit step order -- [ ] Make branch and tag conventions consistent across releases -- [ ] Ensure Docker image publication is triggered from release branches -- [ ] Ensure crate publication is triggered from release branches -- [ ] Define validation and rollback guidance for failed release steps -- [ ] Keep the first version of the process intentionally simpler than the tracker repository -- [ ] Avoid duplicate Docker release tags for the same version +- [x] Define a single documented release workflow with explicit step order +- [x] Make branch and tag conventions consistent across releases +- [x] Ensure Docker image publication is triggered from release branches +- [x] Ensure crate publication is triggered from release branches +- [x] Define validation and rollback guidance for failed release steps +- [x] Keep the first version of the process intentionally simpler than the tracker repository +- [x] Avoid duplicate Docker release tags for the same version ## 🏗️ Architecture Requirements @@ -48,21 +48,21 @@ The initial release process should include these mandatory steps in order: ### Module Structure Requirements -- [ ] Keep process documentation in `docs/` -- [ ] Keep automation in `.github/workflows/` and/or `scripts/` -- [ ] Keep branch and tag naming rules explicit and testable -- [ ] Keep artifact version alignment across Git tag, Docker image tag, and crate version +- [x] Keep process documentation in `docs/` +- [x] Keep automation in `.github/workflows/` and/or `scripts/` +- [x] Keep branch and tag naming rules explicit and testable +- [x] Keep artifact version alignment across Git tag, Docker image tag, and crate version ### Architectural Constraints -- [ ] Release order must be deterministic and documented -- [ ] Tag format must be clearly defined as `vX.Y.Z` -- [ ] Release branch format must be clearly defined and compatible with workflow triggers -- [ ] Docker publish step must support reproducible release tagging without overloading `main` publish behavior -- [ ] Docker release tags must not include the Git tag `v` prefix -- [ ] Crate publish step must define pre-checks and ownership requirements -- [ ] Docker Hub credentials must separate secrets from non-sensitive variables -- [ ] Workflow triggers and branch protections must align with allowed branches (`develop`, `main`, `releases/**/*`) +- [x] Release order must be deterministic and documented +- [x] Tag format must be clearly defined as `vX.Y.Z` +- [x] Release branch format must be clearly defined and compatible with workflow triggers +- [x] Docker publish step must support reproducible release tagging without overloading `main` publish behavior +- [x] Docker release tags must not include the Git tag `v` prefix +- [x] Crate publish step must define pre-checks and ownership requirements +- [x] Docker Hub credentials must separate secrets from non-sensitive variables +- [x] Workflow triggers and branch protections must align with allowed branches (`develop`, `main`, `releases/**/*`) ### Anti-Patterns to Avoid @@ -224,31 +224,34 @@ Define repository settings expectations that release automation depends on. ### Phase 1: Define the Manual Release Sequence (estimated time: 2-3 hours) -- [ ] Task 1.1: Document the simplified release steps from version bump through GitHub release creation -- [ ] Task 1.2: Define version, tag, and release branch naming conventions -- [ ] Task 1.3: Specify which `Cargo.toml` files must be updated for each release -- [ ] Task 1.4: Add a pre-flight checklist for environments, permissions, and clean git state +- [x] Task 1.1: Document the simplified release steps from version bump through GitHub release creation +- [x] Task 1.2: Define version, tag, and release branch naming conventions +- [x] Task 1.3: Specify which `Cargo.toml` files must be updated for each release +- [x] Task 1.4: Add a pre-flight checklist for environments, permissions, and clean git state ### Phase 2: Docker Release Branch Publishing (estimated time: 1-2 hours) -- [ ] Task 2.1: Extend `container.yaml` to trigger on `releases/**/*` -- [ ] Task 2.2: Add release branch context detection and release image tags -- [ ] Task 2.3: Define image verification, credential, and rerun requirements -- [ ] Task 2.4: Ensure Docker Hub username/repository are configured as non-secret variables (token remains secret) +- [x] Task 2.1: Extend `container.yaml` to trigger on `releases/**/*` +- [x] Task 2.2: Add release branch context detection and release image tags +- [x] Task 2.3: Define image verification, credential, and rerun requirements +- [x] Task 2.4: Ensure Docker Hub username/repository are configured as non-secret variables (token remains secret) ### Phase 3: Crate Publishing Workflow (estimated time: 1-2 hours) -- [ ] Task 3.1: Create a dedicated workflow for publishing the SDK crate from `releases/**/*` -- [ ] Task 3.2: Define package inspection, dry-run, publish, and post-publish verification steps -- [ ] Task 3.3: Define dedicated environment and document cargo registry credentials and failure recovery rules -- [ ] Task 3.4: Add docs.rs post-publish verification guidance +- [x] Task 3.1: Create a dedicated workflow for publishing the SDK crate from `releases/**/*` +- [x] Task 3.2: Define package inspection, dry-run, publish, and post-publish verification steps +- [x] Task 3.3: Define dedicated environment and document cargo registry credentials and failure recovery rules +- [x] Task 3.4: Add docs.rs post-publish verification guidance ### Phase 4: Validation and Operational Guidance (estimated time: 2-4 hours) - [ ] Task 4.1: Validate the end-to-end release flow against a test version -- [ ] Task 4.2: Document how maintainers verify Docker image, crate publication, and GitHub release creation -- [ ] Task 4.3: Add troubleshooting notes for partial publication failures -- [ ] Task 4.4: Add explicit idempotency/re-run guidance and crate yank policy +- [x] Task 4.2: Document how maintainers verify Docker image, crate publication, and GitHub release creation +- [x] Task 4.3: Add troubleshooting notes for partial publication failures +- [x] Task 4.4: Add explicit idempotency/re-run guidance and crate yank policy + +> Note: The practical end-to-end validation for Task 4.1 is planned as the +> post-merge `0.1.0-beta` release run. ## Acceptance Criteria @@ -256,27 +259,27 @@ Define repository settings expectations that release automation depends on. **Quality Checks**: -- [ ] Pre-commit checks pass: `./scripts/pre-commit.sh` +- [x] Pre-commit checks pass: `./scripts/pre-commit.sh` **Task-Specific Criteria**: -- [ ] The documented release process follows this order: version update, release commit, push to `main`, tag push, release branch push, GitHub release creation, workflow-driven artifact publication -- [ ] The spec defines explicit finalization gates (main push, tag push, release branch push, Docker pass, crate pass, GitHub release) -- [ ] Branch naming and tag naming conventions are documented as `releases/vX.Y.Z` and `vX.Y.Z` -- [ ] `container.yaml` is specified to publish Docker images for release branches in addition to existing `main` and `develop` behavior -- [ ] The spec explicitly requires Docker release tags to use `X.Y.Z` and forbids `vX.Y.Z` image tags -- [ ] A separate crate publication workflow is specified for the SDK crate on `releases/**/*` -- [ ] The spec explicitly records the decision to keep Docker and crate publication in independent workflows -- [ ] Docker Hub configuration policy is explicit: token is secret, username/repository are variables -- [ ] Release workflow branch scope is explicit and aligned with `develop`, `main`, and `releases/**/*` -- [ ] Docker publish procedure includes verification and failure handling -- [ ] Crate publish procedure includes dry-run and post-publish verification -- [ ] Crate publish procedure includes package content inspection before publish -- [ ] Crate publish procedure includes docs.rs build verification after publish -- [ ] Pre-flight checks are documented for environments, secrets/variables, permissions, and git state -- [ ] Partial-failure and re-run rules are documented for Docker and crate workflows -- [ ] Crate rollback policy includes explicit yank criteria and patch-release follow-up -- [ ] Version consistency rules are documented across Git tags, Docker tags, and crate versions +- [x] The documented release process follows this order: version update, release commit, push to `main`, tag push, release branch push, workflow-driven artifact publication, GitHub release creation +- [x] The spec defines explicit finalization gates (main push, tag push, release branch push, Docker pass, crate pass, GitHub release) +- [x] Branch naming and tag naming conventions are documented as `releases/vX.Y.Z` and `vX.Y.Z` +- [x] `container.yaml` is specified to publish Docker images for release branches in addition to existing `main` and `develop` behavior +- [x] The spec explicitly requires Docker release tags to use `X.Y.Z` and forbids `vX.Y.Z` image tags +- [x] A separate crate publication workflow is specified for the SDK crate on `releases/**/*` +- [x] The spec explicitly records the decision to keep Docker and crate publication in independent workflows +- [x] Docker Hub configuration policy is explicit: token is secret, username/repository are variables +- [x] Release workflow branch scope is explicit and aligned with `develop`, `main`, and `releases/**/*` +- [x] Docker publish procedure includes verification and failure handling +- [x] Crate publish procedure includes dry-run and post-publish verification +- [x] Crate publish procedure includes package content inspection before publish +- [x] Crate publish procedure includes docs.rs build verification after publish +- [x] Pre-flight checks are documented for environments, secrets/variables, permissions, and git state +- [x] Partial-failure and re-run rules are documented for Docker and crate workflows +- [x] Crate rollback policy includes explicit yank criteria and patch-release follow-up +- [x] Version consistency rules are documented across Git tags, Docker tags, and crate versions ## Related Documentation diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 00000000..dbaa34ea --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,269 @@ +# Release Process + +This document defines the standard release process for the Torrust Tracker Deployer +repository. Following these steps ensures that releases are predictable, auditable, +and consistent across Git tags, Docker images, and published crates. + +## Overview + +Releasing consists of these mandatory steps, executed **in order**: + +1. Update version in all relevant `Cargo.toml` files +2. Commit the version bump (`release: version vX.Y.Z`) +3. Push the release commit to `main` +4. Create and push the annotated, signed release tag (`vX.Y.Z`) +5. Create and push the release branch (`releases/vX.Y.Z`) +6. Wait for GitHub Actions to publish release artifacts (Docker image, crate) +7. Create the GitHub release from the tag + +Do not skip or reorder steps. Each step is a prerequisite for the next. + +## Naming Conventions + +| Artifact | Convention | Example | +| ---------------- | ----------------------- | ----------------- | +| Git tag | `vX.Y.Z` | `v1.2.3` | +| Release branch | `releases/vX.Y.Z` | `releases/v1.2.3` | +| Docker image tag | `X.Y.Z` (no `v` prefix) | `1.2.3` | +| Crate version | `X.Y.Z` (no `v` prefix) | `1.2.3` | + +> **Important**: Docker release tags must use bare semver (`X.Y.Z`). Never publish +> release Docker tags with the `v` prefix (e.g., `v1.2.3`). + +## Files to Update for Each Release + +Update the `version` field in both files: + +- `Cargo.toml` (workspace root) — deployer binary version +- `packages/sdk/Cargo.toml` — `torrust-tracker-deployer-sdk` crate version + +Both files must contain the same non-prefixed semver version (e.g., `1.2.3`). + +## Pre-Flight Checklist + +Run these checks before starting any release actions: + +**Git state:** + +- [ ] You are on the `main` branch with a clean working tree (`git status`) +- [ ] The branch is up to date with `origin/main` (`git pull --ff-only`) + +**GitHub Environments:** + +- [ ] GitHub Environment `dockerhub-torrust` exists and contains: + - `DOCKER_HUB_ACCESS_TOKEN` — secret + - `DOCKER_HUB_USERNAME` — variable (value: `torrust`) +- [ ] GitHub Environment `crates-io` exists and contains: + - `CARGO_REGISTRY_TOKEN` — secret + +**Permissions:** + +- [ ] You have push access to `main`, and can push tags and release branches +- [ ] You have access to the `dockerhub-torrust` and `crates-io` environments + +**Crate metadata** (before first publish of each crate): + +- [ ] `packages/sdk/Cargo.toml` has `description`, `license`, `repository`, and `readme` + +## Release Steps + +### Step 1 — Update Versions + +Edit the `version` field in both Cargo.toml files: + +```bash +# Edit Cargo.toml and packages/sdk/Cargo.toml +# Change: version = "X.Y.Z-dev" (or current dev version) +# To: version = "X.Y.Z" +``` + +Verify the workspace compiles and tests pass after the version change: + +```bash +cargo build +cargo test +``` + +### Step 2 — Create the Release Commit + +Stage only the version changes and create a signed commit: + +```bash +git add Cargo.toml packages/sdk/Cargo.toml +git commit -S -m "release: version vX.Y.Z" +``` + +The commit subject must follow the pattern `release: version vX.Y.Z` so releases +are easily identifiable in the git log. + +### Step 3 — Push to `main` + +```bash +git push origin main +``` + +Wait for the CI pipeline on `main` to pass before continuing. + +### Step 4 — Create and Push the Release Tag + +Create an annotated, signed tag from the release commit: + +```bash +git tag -s -a vX.Y.Z -m "Release vX.Y.Z" +git push origin vX.Y.Z +``` + +Verify the tag: + +```bash +git tag -v vX.Y.Z +``` + +### Step 5 — Create and Push the Release Branch + +Create the release branch from the same commit (already at `HEAD` of `main`): + +```bash +git checkout -b releases/vX.Y.Z +git push origin releases/vX.Y.Z +``` + +This push triggers the GitHub Actions workflows that publish the Docker image and +the crate. + +### Step 6 — Wait for CI Artifacts + +Monitor the following workflows in GitHub Actions: + +- **Container** workflow — publishes the Docker image tagged `X.Y.Z` to Docker Hub +- **Publish Crate** workflow — publishes `torrust-tracker-deployer-sdk` to crates.io + +Both workflows must succeed before moving to step 7. See +[Finalization Gates](#finalization-gates) below. + +### Step 7 — Create the GitHub Release + +Once both workflows have passed: + +1. Go to **GitHub → Releases → Draft a new release** +2. Select tag `vX.Y.Z` +3. Write release notes (highlights, breaking changes, upgrade instructions) +4. Publish the release + +## Finalization Gates + +All of the following must be confirmed before marking the release as complete: + +- [ ] Release commit is on `main` and CI passed +- [ ] Tag `vX.Y.Z` is pushed and signed +- [ ] Branch `releases/vX.Y.Z` is pushed +- [ ] Container workflow completed successfully (Docker image `X.Y.Z` published) +- [ ] Publish Crate workflow completed successfully (crate `X.Y.Z` on crates.io) +- [ ] GitHub release created from tag `vX.Y.Z` + +## Docker Image Verification + +After the Container workflow completes: + +```bash +# Pull and inspect the published image +docker pull torrust/tracker-deployer:X.Y.Z +docker image inspect torrust/tracker-deployer:X.Y.Z + +# Confirm the version and tools +docker run --rm torrust/tracker-deployer:X.Y.Z --version || true +docker run --rm --entrypoint tofu torrust/tracker-deployer:X.Y.Z version +``` + +## Crate Verification + +After the Publish Crate workflow completes: + +```bash +# Verify the crate is visible on crates.io +# (indexing may take a few minutes after publish) +curl -sf "https://crates.io/api/v1/crates/torrust-tracker-deployer-sdk/X.Y.Z" | jq '.version.num' + +# Verify docs.rs build +# https://docs.rs/torrust-tracker-deployer-sdk/X.Y.Z +``` + +It is normal for the crates.io index and docs.rs build to take a few minutes. +Check the GitHub release notes for links once propagation is complete. + +## Failure Handling and Recovery + +### Partial-Failure Action Matrix + +| Failure point | Action | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| Docker failed, crate not started | Fix the Docker workflow and re-run the Container workflow on the same release branch | +| Docker passed, crate failed before upload | Fix the issue and re-run the Publish Crate workflow on the same release branch | +| Crate published, later step (e.g., GitHub release) failed | Do not republish. Proceed with follow-up patch release if the crate artifact is broken | + +### Re-Run Rules + +**Docker publication** is safely re-runnable for the same release branch. Pushing the +same Docker tag twice with identical content is idempotent. + +**Crate publication** must detect previously published versions: + +- `cargo publish` will fail with a clear error if the version is already on crates.io +- Do not attempt to republish the same version; instead, cut a patch release + +**Tag and branch creation** must verify that refs do not already exist: + +```bash +# Check before creating the tag +git ls-remote --tags origin vX.Y.Z + +# Check before creating the release branch +git ls-remote --heads origin releases/vX.Y.Z +``` + +If a ref already exists, **do not force-push**. Investigate the previous state and +determine whether the release partially succeeded. + +### Crate Rollback and Yank Policy + +Yanking a published crate is a last resort, not a routine operation. + +Use `cargo yank` **only** for: + +- A critical security vulnerability in the published version +- A broken build that prevents dependents from compiling +- Corruption that makes the crate entirely unusable + +```bash +# Yank a specific version (prevents new Cargo.lock pins; existing users keep it) +cargo yank --version X.Y.Z torrust-tracker-deployer-sdk +``` + +After yanking, cut a patch release (`X.Y.Z+1`) with a fix and document the +remediation in its release notes. + +Never yank for minor issues. Prefer a follow-up patch release instead. + +### Tag and Branch Rollback + +If the release commit has not been pushed to `main` yet, you can reset locally: + +```bash +# Delete the local tag +git tag -d vX.Y.Z + +# Delete the local branch +git branch -d releases/vX.Y.Z +``` + +Once a tag or branch is pushed and CI has run, **do not delete** the remote ref +without coordinating with maintainers. Deleting a published release ref can break +CI re-runs and audit trails. + +## Related Documentation + +- [Branching conventions](contributing/branching.md) +- [Commit process](contributing/commit-process.md) +- [Docker workflow](../.github/workflows/container.yaml) +- [Crate publish workflow](../.github/workflows/publish-crate.yaml) +- [Roadmap](roadmap.md) diff --git a/packages/sdk/Cargo.toml b/packages/sdk/Cargo.toml index fe0c4266..b3cb538e 100644 --- a/packages/sdk/Cargo.toml +++ b/packages/sdk/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" description = "Programmatic SDK for the Torrust Tracker Deployer" license = "MIT" +repository = "https://github.com/torrust/torrust-tracker-deployer" +readme = "README.md" [[example]] name = "sdk_basic_usage"