diff --git a/.github/workflows/bump-and-release.yml b/.github/workflows/bump-and-release.yml index 95e124d..45cb655 100644 --- a/.github/workflows/bump-and-release.yml +++ b/.github/workflows/bump-and-release.yml @@ -46,23 +46,26 @@ jobs: - name: Configure git run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config user.name "nerjs" + git config user.email "nerjs.stap@gmail.com" - name: Compute next version id: ver + env: + BUMP_LEVEL: ${{ inputs.version }} + IS_BETA: ${{ inputs.beta }} run: | set -euo pipefail CURRENT=$(cargo read-manifest | jq -r .version) BASE_CURRENT=${CURRENT%%-*} IFS=. read MAJ MIN PAT <<< "$BASE_CURRENT" - case "${{ inputs.version }}" in + case "$BUMP_LEVEL" in patch) NEXT_BASE="$MAJ.$MIN.$((PAT+1))" ;; minor) NEXT_BASE="$MAJ.$((MIN+1)).0" ;; major) NEXT_BASE="$((MAJ+1)).0.0" ;; esac git fetch --tags - if [ "${{ inputs.beta }}" = "true" ]; then + if [ "$IS_BETA" = "true" ]; then N=$(git tag --list "v${NEXT_BASE}-beta.*" | sed "s/.*-beta\.//" | sort -n | tail -n1) N=$((${N:-0}+1)) NEXT="${NEXT_BASE}-beta.${N}" @@ -73,22 +76,28 @@ jobs: echo "next=$NEXT" >> "$GITHUB_OUTPUT" - name: Commit + env: + NEXT_VERSION: ${{ steps.ver.outputs.next }} + COMMIT_NOTE: ${{ inputs.commit_note }} run: | - VERSION="${{ steps.ver.outputs.next }}" - MSG="Bump version to v${VERSION}" - NOTE="${{ inputs.commit_note }}" - if [ -n "$NOTE" ]; then - MSG="${MSG}"$'\n\n'"${NOTE}" + set -euo pipefail + MSG="Bump version to v${NEXT_VERSION}" + if [ -n "$COMMIT_NOTE" ]; then + MSG="${MSG}"$'\n\n'"${COMMIT_NOTE}" fi git add Cargo.toml Cargo.lock git commit -m "$MSG" - name: Tag and push id: tag + env: + NEXT_VERSION: ${{ steps.ver.outputs.next }} + BRANCH: ${{ github.ref_name }} run: | - TAG="v${{ steps.ver.outputs.next }}" + set -euo pipefail + TAG="v${NEXT_VERSION}" git tag -a "$TAG" -m "$TAG" - git push origin "${{ github.ref_name }}" --follow-tags + git push origin "$BRANCH" --follow-tags echo "tag=$TAG" >> "$GITHUB_OUTPUT" release: @@ -103,9 +112,12 @@ jobs: - name: Create GitHub Release env: GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + TAG: ${{ needs.bump.outputs.tag }} + IS_BETA: ${{ inputs.beta }} run: | + set -euo pipefail FLAGS="--generate-notes" - if [ "${{ inputs.beta }}" = "true" ]; then + if [ "$IS_BETA" = "true" ]; then FLAGS="$FLAGS --prerelease" fi - gh release create "${{ needs.bump.outputs.tag }}" $FLAGS + gh release create "$TAG" $FLAGS diff --git a/.github/workflows/ci-deps.yml b/.github/workflows/ci-deps.yml index 8072b6d..1d19d61 100644 --- a/.github/workflows/ci-deps.yml +++ b/.github/workflows/ci-deps.yml @@ -3,19 +3,20 @@ name: CI (deps) on: workflow_dispatch: push: - branches: - - dependencies - - deps-sync + branches: [dependencies] pull_request: branches: [dependencies] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check-test: - if: ${{ !contains(github.event.head_commit.message || github.event.pull_request.title || '', '[skip-CI]') }} + if: ${{ !contains(github.event.head_commit.message || '', '[skip-CI]') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1329425..c555ae6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,16 @@ on: pull_request: branches: [master] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: fmt: - if: ${{ !contains(github.event.head_commit.message || github.event.pull_request.title || '', '[skip-CI]') }} + if: ${{ !contains(github.event.head_commit.message || '', '[skip-CI]') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -51,6 +54,8 @@ jobs: name: test (${{ matrix.env.name }} / ${{ matrix.rust }}) runs-on: ubuntu-latest container: ${{ matrix.env.container }} + env: + RUSTUP_TOOLCHAIN: ${{ matrix.rust }} steps: - name: Prep (debian) if: matrix.env.name == 'debian' diff --git a/.github/workflows/publish-docker-hub.yml b/.github/workflows/publish-docker-hub.yml index e961978..55eb0c2 100644 --- a/.github/workflows/publish-docker-hub.yml +++ b/.github/workflows/publish-docker-hub.yml @@ -15,21 +15,21 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ inputs.image }} tags: | type=raw,value=${{ github.ref_name }} type=raw,value=latest,enable=${{ !contains(github.ref_name, '-beta') }} - - uses: docker/build-push-action@v5 + - uses: docker/build-push-action@v7 with: context: . push: true diff --git a/.github/workflows/publish-ghcr.yml b/.github/workflows/publish-ghcr.yml index 5bbe478..518a530 100644 --- a/.github/workflows/publish-ghcr.yml +++ b/.github/workflows/publish-ghcr.yml @@ -14,22 +14,22 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository }} tags: | type=raw,value=${{ github.ref_name }} type=raw,value=latest,enable=${{ !contains(github.ref_name, '-beta') }} - - uses: docker/build-push-action@v5 + - uses: docker/build-push-action@v7 with: context: . push: true diff --git a/.github/workflows/sync-dependencies.yml b/.github/workflows/sync-dependencies.yml index 5767a5f..75f0eb0 100644 --- a/.github/workflows/sync-dependencies.yml +++ b/.github/workflows/sync-dependencies.yml @@ -22,43 +22,26 @@ jobs: fetch-depth: 0 token: ${{ secrets.RELEASE_TOKEN }} - - name: Configure git - run: | - git config user.name "nerjs" - git config user.email "nerjs.stap@gmail.com" - - - name: Sync or prepare deps-sync - id: sync + - name: Ensure dependencies branch and open PR if drift + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | set -euo pipefail if ! git ls-remote --heads origin dependencies | grep -q dependencies; then git push origin master:dependencies - echo "result=created" >> "$GITHUB_OUTPUT" exit 0 fi - git fetch origin dependencies:dependencies - git checkout dependencies - if git merge --ff-only origin/master; then - git push origin dependencies - echo "result=ff" >> "$GITHUB_OUTPUT" + git fetch origin dependencies + if git diff --quiet origin/master origin/dependencies; then + echo "master and dependencies trees match; nothing to sync" exit 0 fi - git checkout -B deps-sync origin/master - git push --force-with-lease origin deps-sync - echo "result=needs-pr" >> "$GITHUB_OUTPUT" - - - name: Ensure PR deps-sync -> dependencies - if: steps.sync.outputs.result == 'needs-pr' - env: - GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} - run: | - set -euo pipefail - existing=$(gh pr list --base dependencies --head deps-sync --state open --json number --jq '.[0].number // empty') + existing=$(gh pr list --base dependencies --head master --state open --json number --jq '.[0].number // empty') if [ -z "$existing" ]; then gh pr create \ - --base dependencies --head deps-sync \ + --base dependencies --head master \ --title "Sync master into dependencies" \ - --body "Auto-generated by sync-dependencies.yml. Resolve conflicts on deps-sync, then merge into dependencies." + --body "Auto-generated by sync-dependencies.yml. Merge to bring master changes into dependencies." else echo "PR #$existing already open" fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..be4db2e --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# rust-template + +GitHub template repository for new Rust projects. Provides CI, dependabot, +release tagging and three publish workflows out of the box. + +## Use it + +1. Click "Use this template" -> "Create a new repository" on GitHub. +2. Rename the package in `Cargo.toml` and update the binary path in `Dockerfile` + (the `target/release/rust-template` line). +3. Add the secrets listed in [Secrets](#secrets). +4. Push to `master`. The first `sync-dependencies` run creates the + `dependencies` branch. + +## Branches + +| Branch | Role | +|----------------|------------------------------------------------------------------| +| `master` | Main line. Protected by required PR. | +| `dependencies` | Receives dependabot PRs and accumulates updates. | +| `deps-sync` | Throwaway branch for resolving `master` -> `dependencies` conflicts. | + +Dependabot opens PRs into `dependencies`, not `master`, so dependency churn +stays out of the main line until a human merges it. + +`sync-dependencies.yml` runs on each push to `master` and fast-forwards +`dependencies` when possible. On conflict it force-pushes a fresh snapshot of +`master` to `deps-sync` and opens (or reuses) a PR `deps-sync -> dependencies` +for manual conflict resolution. + +## Workflows + +| File | Trigger | Purpose | +|-------------------------------------|------------------------------------|---------------------------------------------------------------| +| `ci.yml` | push/PR on master, manual | fmt, clippy, test matrix (rust x distro), release build | +| `ci-deps.yml` | push/PR on dependencies, deps-sync | cargo check + cargo test | +| `sync-dependencies.yml` | push on master, manual | sync `master` into `dependencies` | +| `bump-and-release.yml` | manual | bump version, tag, optional GitHub Release | +| `publish-crates.yml` | manual on a tag | publish to crates.io | +| `publish-docker-hub.yml` | manual on a tag | build and push image to Docker Hub, multi-arch | +| `publish-ghcr.yml` | manual on a tag | build and push image to ghcr.io, multi-arch | + +Commit-message or PR-title marker `[skip-CI]` skips `ci.yml` and `ci-deps.yml`. + +### Test matrix + +`ci.yml` runs `cargo test --all-features` across: + +- rust: `stable`, `beta` +- env: `ubuntu-latest`, `debian:12`, `archlinux:latest`, `rust:1-alpine` + +8 jobs total, `fail-fast: false`. Distros run via `container:`. + +### Bump and release + +Manual trigger from any branch (selected via "Use workflow from"). Inputs: + +| Input | Type | Default | Effect | +|-----------------|---------|---------|---------------------------------------------------------| +| `version` | choice | patch | bump level: patch / minor / major | +| `beta` | boolean | false | append `-beta.N` suffix; N is auto-incremented per base | +| `force_release` | boolean | false | create GitHub Release even for a beta tag | +| `commit_note` | string | "" | optional text appended to the bump commit message | + +The job updates `Cargo.toml`, commits, tags `vX.Y.Z[-beta.N]`, pushes to the +selected branch. A second job creates a GitHub Release unless the tag is beta +and `force_release` is false. Beta releases are marked `--prerelease`. + +### Publish + +Each publish workflow is triggered manually with "Use workflow from: +tags/vX.Y.Z". A guard rejects runs from a branch. Docker tags are derived from +the tag name; `latest` is set only for non-beta tags. + +## Dependabot + +Two ecosystems, both targeting `dependencies`: + +- `cargo` - weekly, max 5 open PRs, minor+patch grouped, major opens its own PR +- `github-actions` - weekly, max 5 open PRs, same grouping + +## Secrets + +Add these in repository settings before using the relevant workflow: + +| Secret | Used by | Notes | +|------------------------|--------------------------|------------------------------------------------------------------------------| +| `RELEASE_TOKEN` | bump, sync, release | PAT with `contents: write`, `pull-requests: write`. Required so tag pushes trigger downstream workflows; the default `GITHUB_TOKEN` does not. | +| `CARGO_REGISTRY_TOKEN` | publish-crates | crates.io API token | +| `DOCKERHUB_USERNAME` | publish-docker-hub | Docker Hub login | +| `DOCKERHUB_TOKEN` | publish-docker-hub | Docker Hub access token | + +GHCR uses the built-in `GITHUB_TOKEN` with `packages: write`. No extra secret. + +## Dockerfile + +Minimal alpine multi-stage. The binary path in the runtime stage is +`target/release/rust-template`. Rename it together with the package name in +`Cargo.toml`, otherwise the `COPY --from=builder` step fails. + +Crates with C dependencies under musl need extra apk packages in the builder +stage (for example `openssl-dev pkgconfig`).