From 4fdc0d43ab92001a0d12e47388b6c50e3a4cff19 Mon Sep 17 00:00:00 2001 From: Anthony Oteri <4360016+anthonyoteri@users.noreply.github.com> Date: Wed, 13 May 2026 14:16:08 -0400 Subject: [PATCH] ci: overhaul CI/CD pipeline and add tooling configs - Add ci.yml: test matrix (ubuntu/macos/windows), lint, conventional commits check (cocogitto), cargo-deny, MSRV, and semver jobs - Add release.yml: tag-triggered publish to crates.io + GitHub release - Remove stale rust.yml and rust-clippy.yml workflows - Add deny.toml for cargo-deny license/advisory/ban/source checks - Add cog.toml for cocogitto conventional commits and changelog generation - Add Justfile with test, check, fmt, commits, release, push-tag targets - Add CHANGELOG.md seed file - Add PULL_REQUEST_TEMPLATE.md with conventional commits checklist - Update dependabot.yml: weekly schedule with grouped patch/minor updates --- .github/PULL_REQUEST_TEMPLATE.md | 18 +++ .github/dependabot.yml | 18 ++- .github/workflows/ci.yml | 121 ++++++++++++++++++ .github/workflows/release.yml | 200 +++++++++++++++++------------- .github/workflows/rust-clippy.yml | 51 -------- .github/workflows/rust.yml | 56 --------- CHANGELOG.md | 5 + Justfile | 168 +++++++++++++++++++++++++ cog.toml | 32 +++++ deny.toml | 44 +++++++ 10 files changed, 521 insertions(+), 192 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/rust-clippy.yml delete mode 100644 .github/workflows/rust.yml create mode 100644 CHANGELOG.md create mode 100644 Justfile create mode 100644 cog.toml create mode 100644 deny.toml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..b5b1758 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## Summary + + + +## Checklist + +- [ ] Commits follow the [Conventional Commits](https://www.conventionalcommits.org/) spec + - `feat:` new feature + - `fix:` bug fix + - `docs:` documentation only + - `chore:` maintenance / tooling + - `ci:` CI/CD changes + - `refactor:` code change that neither fixes a bug nor adds a feature + - `test:` adding or correcting tests + - `perf:` performance improvement +- [ ] `cargo test` passes locally +- [ ] `cargo clippy --all-targets -- -W clippy::pedantic` passes with no warnings +- [ ] `cargo fmt --check` passes diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1116676..5383919 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,9 +3,23 @@ updates: - package-ecosystem: "cargo" directory: "/" schedule: - interval: "daily" + interval: "weekly" + groups: + patch-updates: + update-types: + - "patch" + minor-updates: + update-types: + - "minor" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" + groups: + patch-updates: + update-types: + - "patch" + minor-updates: + update-types: + - "minor" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..10cbd82 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: CI + +on: + push: + branches: ["master"] + pull_request: + workflow_call: + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +jobs: + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --all-targets + + - name: Run tests + run: cargo test + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain with clippy and rustfmt + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --check --all + + - name: Clippy + run: cargo clippy --all-targets -- -W clippy::pedantic + + commits: + name: Conventional commits + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + # Full history is required for cog check to validate all commits. + fetch-depth: 0 + + - name: Check conventional commits + uses: cocogitto/cocogitto-action@v3 + with: + check: true + # On PRs check only the commits introduced by the PR. + # On pushes to master check only commits since the previous HEAD. + from: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + + deny: + name: Cargo deny + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Check licenses, advisories, and bans + uses: EmbarkStudios/cargo-deny-action@v2 + + msrv: + name: MSRV (1.80) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust 1.80 + uses: dtolnay/rust-toolchain@1.80 + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Check MSRV builds + run: cargo check + + semver: + name: Semver compatibility + runs-on: ubuntu-latest + # Only meaningful on PRs — compares the PR branch against the published + # crate version to catch accidental breaking API changes. + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Check semver compatibility + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + package: dredge-tool diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b06dd3..c2587e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,113 +3,147 @@ name: Release on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+*" + - "v[0-9]*.[0-9]*.[0-9]*" + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" jobs: - create_release: - name: Create release + # --------------------------------------------------------------------------- + # Gate: all CI checks must pass before we publish anything. + # --------------------------------------------------------------------------- + ci: + name: CI checks + uses: ./.github/workflows/ci.yml + + # --------------------------------------------------------------------------- + # Publish to crates.io and create a GitHub release. + # --------------------------------------------------------------------------- + publish: + name: Publish runs-on: ubuntu-latest + needs: ci permissions: contents: write - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} - steps: - - name: Create release - id: create_release - uses: ncipollo/release-action@v1.21.0 - build-docs: - name: Build documentation - needs: create_release - runs-on: ubuntu-latest - permissions: - contents: write steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@v4 with: - toolchain: stable - components: rust-docs + fetch-depth: 0 - - name: Cache build dependencies + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies uses: Swatinem/rust-cache@v2 - - name: Run cargo doc - run: - cargo doc - --bin=dredge - --no-deps - --all-features - --document-private-items - --release - - - name: Archive the Docs - run: - tar --directory target/doc -czf - dredge-${{ github.ref_name}}-docs.tar.gz - dredge - - - name: Upload documentation assets - uses: shogo82148/actions-upload-release-asset@v1.10.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Install cocogitto + uses: cocogitto/cocogitto-action@v3 with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: dredge-${{ github.ref_name }}-docs.tar.gz - asset_path: dredge-${{ github.ref_name }}-docs.tar.gz - asset_content_type: application/gzip + check: false - release_assets: - name: Release assets - needs: create_release - runs-on: ${{ matrix.config.os }} + - name: Generate release notes for this tag + id: changelog + run: | + VERSION="${GITHUB_REF_NAME#v}" + # Write release notes outside the repo so cargo publish does not + # see an untracked file and refuse to run. + cog changelog --at "$GITHUB_REF_NAME" > /tmp/release_notes.md || \ + echo "No structured changelog available for $GITHUB_REF_NAME." > /tmp/release_notes.md + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Publish dredge-tool to crates.io + run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + - name: Create GitHub release + run: | + gh release create "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --notes-file /tmp/release_notes.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # --------------------------------------------------------------------------- + # Build release binaries and attach them to the GitHub release. + # Runs in parallel across platforms after the release is created. + # --------------------------------------------------------------------------- + build-binaries: + name: Build binary (${{ matrix.upload_name }}) + needs: publish + runs-on: ${{ matrix.os }} permissions: contents: write + strategy: + fail-fast: false matrix: - config: + include: - os: ubuntu-latest - platform: linux - arch: x86_64 - ext: '' - - os: macos-latest - platform: macos - arch: x86_64 - ext: '' - - os: windows-latest - platform: win - arch: x86_64 - ext: .exe - steps: - - name: Checkout code - uses: actions/checkout@v4 + artifact: dredge + upload_name: dredge-linux-x86_64.tar.gz + target: x86_64-unknown-linux-musl + artifact_dir: target/x86_64-unknown-linux-musl/release - - name: Install Rust Toolchain + - os: macos-latest + artifact: dredge + upload_name: dredge-macos-aarch64.tar.gz + target: "" + artifact_dir: target/release + + - os: macos-latest + artifact: dredge + upload_name: dredge-macos-x86_64.tar.gz + target: x86_64-apple-darwin + artifact_dir: target/x86_64-apple-darwin/release + + - os: windows-latest + artifact: dredge.exe + upload_name: dredge-windows-x86_64.exe + target: "" + artifact_dir: target/release + + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable with: - toolchain: stable + targets: ${{ matrix.target }} - - name: Cache build dependencies + - name: Install musl tools (Linux only) + if: matrix.target == 'x86_64-unknown-linux-musl' + run: sudo apt-get install -y musl-tools + + - name: Cache dependencies uses: Swatinem/rust-cache@v2 - - name: Run cargo build - run: cargo build --release + - name: Build release binary + run: > + cargo build --release + ${{ matrix.target != '' && format('--target {0}', matrix.target) || '' }} - - name: Create release assets - run: - tar --directory target/release -czf - dredge-${{ github.ref_name }}-${{ matrix.config.platform }}.${{ matrix.config.arch }}.tar.gz - dredge${{ matrix.config.ext }} + - name: Strip binary (Unix only) + if: runner.os != 'Windows' + run: strip ${{ matrix.artifact_dir }}/${{ matrix.artifact }} - - name: Upload release assets - uses: shogo82148/actions-upload-release-asset@v1.10.1 + - name: Ad-hoc sign binary (macOS only) + if: runner.os == 'macOS' + run: codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime ${{ matrix.artifact_dir }}/${{ matrix.artifact }} + + - name: Package binary (Unix) + if: runner.os != 'Windows' + shell: bash + run: tar -czf "${{ matrix.upload_name }}" -C "${{ matrix.artifact_dir }}" "${{ matrix.artifact }}" + + - name: Copy binary (Windows) + if: runner.os == 'Windows' + shell: bash + run: cp "${{ matrix.artifact_dir }}/${{ matrix.artifact }}" "${{ matrix.upload_name }}" + + - name: Upload to GitHub release + run: gh release upload "${{ github.ref_name }}" "${{ matrix.upload_name }}" --clobber env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: dredge-${{ github.ref_name }}-${{ matrix.config.platform }}.${{ matrix.config.arch }}.tar.gz - asset_path: dredge-${{ github.ref_name }}-${{ matrix.config.platform }}.${{ matrix.config.arch }}.tar.gz - asset_content_type: application/gzip + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml deleted file mode 100644 index b9036e4..0000000 --- a/.github/workflows/rust-clippy.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Rust Clippy Analysis - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: "18 21 * * 6" - -env: - CARGO_TERM_COLOR: always - -jobs: - rust-clippy: - name: Run rust-clippy analysis - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - actions: read - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - components: clippy - - - name: Cache build dependencies - uses: Swatinem/rust-cache@v2 - - - name: Install clippy-sarif - run: cargo install clippy-sarif sarif-fmt - - - name: Run rust-clippy - run: - cargo clippy - --all-features - --tests - --message-format=json - | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt - continue-on-error: true - - - name: Upload analysis results ot GitHub - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: rust-clippy-results.sarif - wait-for-processing: true \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 3e6b402..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Rust - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - rust-fmt-check: - name: Run Rust-fmt check - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - components: rustfmt - - - name: Cache build dependencies - uses: Swatinem/rust-cache@v2 - - - name: Run rust-fmt --check - run: cargo fmt --check --all - - test: - name: Run unit and integration tests - strategy: - matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.platform }} - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - components: rustfmt - - - name: Cache build dependencies - uses: Swatinem/rust-cache@v2 - - - name: Run cargo-test - run: cargo test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8a520c7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This changelog is generated by [cocogitto](https://github.com/oknozor/cocogitto). diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..aa137d8 --- /dev/null +++ b/Justfile @@ -0,0 +1,168 @@ +# Justfile for dredge +# Install just: https://just.systems + +# Default: list available recipes +default: + @just --list + +# Run the full test suite +test: + cargo test + +# Check formatting and lints (mirrors CI) +check: + cargo fmt --check + cargo clippy --all-targets -- -W clippy::pedantic + +# Auto-format the source +fmt: + cargo fmt + +# Validate conventional commit history +commits: + cog check + +# --------------------------------------------------------------------------- +# Release +# --------------------------------------------------------------------------- +# +# Two-step process to work with branch protection on master: +# +# Step 1 — just release +# Runs pre-flight checks, calls `cog bump --auto` on a local release +# branch, opens a PR. Review and merge the PR normally. +# +# Step 2 — just push-tag +# After the PR is merged, pulls master, tags the merge commit with the +# version from Cargo.toml, and pushes the tag. +# The tag push triggers release.yml which publishes to crates.io. +# +# Prerequisites: +# - `cocogitto` installed: cargo install cocogitto +# - `gh` CLI installed and authenticated: https://cli.github.com +# --------------------------------------------------------------------------- + +# Step 1: open a release PR. +# +# Runs pre-flight checks, bumps the version on a release branch, +# generates CHANGELOG.md, and opens a pull request against master. +# After the PR is merged, run `just push-tag` to trigger the publish. +release: + #!/usr/bin/env bash + set -euo pipefail + + # Guard: must be on master. + branch=$(git rev-parse --abbrev-ref HEAD) + if [[ "$branch" != "master" ]]; then + echo "error: releases must be cut from master (currently on '$branch')" >&2 + exit 1 + fi + + # Guard: working tree must be clean. + if ! git diff --quiet || ! git diff --cached --quiet; then + echo "error: working tree has uncommitted changes; commit or stash them first" >&2 + exit 1 + fi + + # Guard: local master must not be behind origin. + git fetch --quiet origin master + if [[ $(git rev-list --count HEAD..origin/master) -gt 0 ]]; then + echo "error: local master is behind origin/master; run 'git pull' first" >&2 + exit 1 + fi + + # Run the full test suite before touching anything. + echo "==> Running tests..." + cargo test + + # Check formatting and lints. + echo "==> Checking formatting and lints..." + cargo fmt --check + cargo clippy --all-targets -- -W clippy::pedantic + + # Determine the next version without making any changes yet. + # cog bump --auto --dry-run prints e.g. "v0.1.0" to stdout. + echo "==> Determining next version..." + next_version=$(cog bump --auto --dry-run) + echo " Next version: ${next_version}" + + # Create and switch to a release branch. + release_branch="release/${next_version}" + git checkout -b "${release_branch}" + + # Bump version, generate CHANGELOG.md, commit, and create the local tag. + # cog bump --auto: + # - Updates the version field in Cargo.toml + # - Writes CHANGELOG.md + # - Creates a commit "chore(version): bump to vX.Y.Z" + # - Creates an annotated tag vX.Y.Z (local only until push-tag) + echo "==> Bumping version with cog..." + cog bump --auto + + # Push the release branch (not the tag — that comes after PR merge). + echo "==> Pushing release branch..." + git push -u origin "${release_branch}" + + # Open the pull request. + echo "==> Opening pull request..." + gh pr create \ + --title "chore(release): ${next_version}" \ + --body "$(cat CHANGELOG.md)" \ + --base master \ + --head "${release_branch}" + + echo "" + echo "==> Release PR opened." + echo " Review and merge the PR, then run:" + echo "" + echo " just push-tag" + echo "" + echo " to push the tag and trigger the crates.io publish." + +# Step 2: tag master HEAD and push the tag. +# +# Run this after the release PR has been merged into master. +# Pulls the latest master, reads the version from Cargo.toml, creates an +# annotated tag on the current HEAD, and pushes it to trigger release.yml. +push-tag: + #!/usr/bin/env bash + set -euo pipefail + + # Must be on master. + branch=$(git rev-parse --abbrev-ref HEAD) + if [[ "$branch" != "master" ]]; then + echo "error: push-tag must be run from master (currently on '$branch')" >&2 + exit 1 + fi + + # Pull so we are at the merge commit. + echo "==> Pulling latest master..." + git pull --ff-only origin master + + # Derive the version from Cargo.toml (set by `cog bump --auto`). + version=$(grep '^version' Cargo.toml | head -1 \ + | sed 's/version = "\(.*\)"/\1/') + if [[ -z "$version" ]]; then + echo "error: could not read version from Cargo.toml" >&2 + exit 1 + fi + tag="v${version}" + echo "==> Tagging HEAD as ${tag}..." + + # Guard: tag must not already exist on origin. + if git ls-remote --tags origin "${tag}" | grep -q "refs/tags/${tag}$"; then + echo "error: tag ${tag} already exists on origin" >&2 + exit 1 + fi + + # Delete stale local tag if present (leftover from the release branch). + git tag -d "${tag}" 2>/dev/null || true + + # Create a fresh annotated tag on the current (merged) HEAD. + git tag -a "${tag}" -m "chore(release): ${tag}" + + echo "==> Pushing tag ${tag}..." + git push origin "${tag}" + + echo "==> Done. Monitor the release workflow at:" + echo " https://github.com/anthonyoteri/dredge/actions" diff --git a/cog.toml b/cog.toml new file mode 100644 index 0000000..65ebe34 --- /dev/null +++ b/cog.toml @@ -0,0 +1,32 @@ +# Cocogitto configuration +# https://docs.cocogitto.io/config/ + +ignore_merge_commits = true +tag_prefix = "v" + +# Update Cargo.toml version before committing the bump. +# The .bak suffix is needed for macOS compatibility with sed -i. +pre_bump_hooks = [ + "sed -i.bak 's/^version = \"[^\"]*\"/version = \"{{version}}\"/' Cargo.toml && rm -f Cargo.toml.bak", +] + +[changelog] +path = "CHANGELOG.md" +template = "remote" +remote = "github.com" +repository = "dredge" +owner = "anthonyoteri" +authors = [ + { username = "anthonyoteri", signature = "Anthony Oteri" }, +] + +[commit_types] +feat = { changelog_title = "Features" } +fix = { changelog_title = "Bug Fixes" } +perf = { changelog_title = "Performance" } +refactor = { changelog_title = "Refactoring" } +docs = { changelog_title = "Documentation" } +test = { changelog_title = "Tests" } +chore = { changelog_title = "Miscellaneous" } +ci = { changelog_title = "CI", omit_from_changelog = true } +style = { changelog_title = "Style", omit_from_changelog = true } diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..26a56a3 --- /dev/null +++ b/deny.toml @@ -0,0 +1,44 @@ +# cargo-deny configuration +# https://embarkstudios.github.io/cargo-deny/ + +[graph] +# Check all targets, not just the host. +all-features = true + +# --------------------------------------------------------------------------- +# Licenses +# --------------------------------------------------------------------------- +[licenses] +# Minimum confidence threshold for license detection (0.0 – 1.0). +confidence-threshold = 0.8 + +# Licenses we explicitly permit across the entire dependency tree. +allow = [ + "MIT", + "Apache-2.0", + "Unicode-3.0", +] + +# --------------------------------------------------------------------------- +# Security advisories +# --------------------------------------------------------------------------- +[advisories] +# Deny crates with known security vulnerabilities. +version = 2 + +# --------------------------------------------------------------------------- +# Crate bans +# --------------------------------------------------------------------------- +[bans] +# Deny multiple versions of the same crate (can cause bloat and confusion). +multiple-versions = "warn" +# Deny wildcard dependencies. +wildcards = "deny" + +# --------------------------------------------------------------------------- +# Crate sources +# --------------------------------------------------------------------------- +[sources] +# Only allow crates from crates.io. +unknown-registry = "deny" +unknown-git = "deny"