Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions .github/workflows/bump-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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:
Expand All @@ -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
9 changes: 5 additions & 4 deletions .github/workflows/ci-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/publish-docker-hub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/publish-ghcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 9 additions & 26 deletions .github/workflows/sync-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`).
Loading