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
37 changes: 37 additions & 0 deletions .github/scripts/cargo-package-field.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 2 ]; then
echo "Usage: $0 <Cargo.toml path> <field>" >&2
exit 2
fi

manifest_path="$1"
field="$2"

if [ ! -f "$manifest_path" ]; then
echo "Cargo manifest not found: $manifest_path" >&2
exit 1
fi

value="$(
awk -v requested_field="$field" '
/^\[package\][[:space:]]*$/ { in_package = 1; next }
/^\[[^]]+\][[:space:]]*$/ {
if (in_package) exit
}
in_package && $0 ~ ("^[[:space:]]*" requested_field "[[:space:]]*=") {
if (match($0, /"[^"]+"/)) {
print substr($0, RSTART + 1, RLENGTH - 2)
exit
}
}
' "$manifest_path"
)"

if [ -z "$value" ]; then
echo "Could not read package.$field from $manifest_path" >&2
exit 1
fi

printf '%s\n' "$value"
21 changes: 21 additions & 0 deletions .github/scripts/changelog-section.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 2 ]; then
echo "Usage: $0 <changelog path> <version>" >&2
exit 2
fi

changelog_path="$1"
version="$2"

if [ ! -f "$changelog_path" ]; then
echo "Changelog not found: $changelog_path" >&2
exit 1
fi

awk -v requested_version="$version" '
$0 ~ ("^## \\[" requested_version "\\]") { in_section = 1; next }
in_section && /^## \[/ { exit }
in_section { print }
' "$changelog_path"
98 changes: 98 additions & 0 deletions .github/workflows/publish-crate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Publish Crate

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: write

concurrency:
group: publish-crate
cancel-in-progress: false

jobs:
publish:
name: Publish to crates.io and create release
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable

- name: Read crate metadata
id: meta
run: |
echo "crate_name=$(.github/scripts/cargo-package-field.sh Cargo.toml name)" >> "$GITHUB_OUTPUT"
echo "version=$(.github/scripts/cargo-package-field.sh Cargo.toml version)" >> "$GITHUB_OUTPUT"
echo "tag=v$(.github/scripts/cargo-package-field.sh Cargo.toml version)" >> "$GITHUB_OUTPUT"

- name: Check crates.io for existing version
id: crates
env:
CRATE_NAME: ${{ steps.meta.outputs.crate_name }}
VERSION: ${{ steps.meta.outputs.version }}
run: |
STATUS="$(curl -sS -o /tmp/crates-version.json -w '%{http_code}' "https://crates.io/api/v1/crates/${CRATE_NAME}/${VERSION}")"
if [ "$STATUS" = "200" ]; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "Version ${VERSION} is already published on crates.io."
elif [ "$STATUS" = "404" ]; then
echo "published=false" >> "$GITHUB_OUTPUT"
echo "Version ${VERSION} is not published yet."
else
echo "Unexpected crates.io response code: $STATUS" >&2
cat /tmp/crates-version.json >&2 || true
exit 1
fi

- name: Publish crate
if: steps.crates.outputs.published != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --locked

- name: Ensure git tag exists
id: tag
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists on origin."
else
git tag "${TAG}" "${GITHUB_SHA}"
git push origin "${TAG}"
echo "Created and pushed ${TAG}."
fi
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.tag }}
VERSION: ${{ steps.meta.outputs.version }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release ${TAG} already exists."
exit 0
fi

NOTES_FILE="$(mktemp)"
{
echo "## micro-moka ${VERSION}"
echo
.github/scripts/changelog-section.sh CHANGELOG.md "$VERSION"
} > "$NOTES_FILE"

gh release create "$TAG" \
--title "micro-moka ${VERSION}" \
--notes-file "$NOTES_FILE"
72 changes: 72 additions & 0 deletions .github/workflows/release-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Release Check

on:
pull_request:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
release-readiness:
name: Validate release readiness
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable

- name: Read crate metadata (head)
id: head
run: |
echo "crate_name=$(.github/scripts/cargo-package-field.sh Cargo.toml name)" >> "$GITHUB_OUTPUT"
echo "version=$(.github/scripts/cargo-package-field.sh Cargo.toml version)" >> "$GITHUB_OUTPUT"

- name: Read base version
id: base
run: |
BASE_REF="${{ github.base_ref || 'main' }}"
git fetch --no-tags origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}"
git show "origin/${BASE_REF}:Cargo.toml" > /tmp/base-Cargo.toml
echo "version=$(.github/scripts/cargo-package-field.sh /tmp/base-Cargo.toml version)" >> "$GITHUB_OUTPUT"

- name: Validate semantic version bump
env:
BASE_VERSION: ${{ steps.base.outputs.version }}
HEAD_VERSION: ${{ steps.head.outputs.version }}
run: |
echo "Base version: $BASE_VERSION"
echo "Head version: $HEAD_VERSION"

if [ "$BASE_VERSION" = "$HEAD_VERSION" ]; then
echo "Version must be bumped in Cargo.toml for merges into main." >&2
exit 1
fi

HIGHEST="$(printf '%s\n%s\n' "$BASE_VERSION" "$HEAD_VERSION" | sort -V | tail -n1)"
if [ "$HIGHEST" != "$HEAD_VERSION" ]; then
echo "Version must increase. Got $HEAD_VERSION <= $BASE_VERSION." >&2
exit 1
fi

- name: Validate CHANGELOG entry for target version
env:
TARGET_VERSION: ${{ steps.head.outputs.version }}
run: |
if ! grep -Eq "^## \\[${TARGET_VERSION}\\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$" CHANGELOG.md; then
echo "CHANGELOG.md must contain a section header like:" >&2
echo "## [${TARGET_VERSION}] - YYYY-MM-DD" >&2
exit 1
fi

- name: Cargo publish dry-run
run: cargo publish --dry-run --locked
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.5] - 2026-02-27

### Added

- Added automated release and publish workflows:
- PR-time release readiness checks (version bump, changelog entry, `cargo publish --dry-run`).
- Post-merge publish to crates.io, version tag creation, and GitHub release creation.

## [0.1.4] - 2026-02-27

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "micro-moka"
version = "0.1.4"
version = "0.1.5"
edition = "2018"
rust-version = "1.76"

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ to take a look at the [Quick Cache][quick-cache] crate.
- [Example: Basic Usage](#example-basic-usage)
- [Minimum Supported Rust Versions](#minimum-supported-rust-versions)
- [Developing Micro Moka](#developing-micro-moka)
- [Releasing](#releasing)
- [Credits](#credits)
- [Caffeine](#caffeine)
- [License](#license)
Expand Down Expand Up @@ -130,6 +131,14 @@ $ cargo +nightly -Z unstable-options --config 'build.rustdocflags="--cfg docsrs"
doc --no-deps
```

## Releasing

Releases are automated from merges into `main`.

- See [RELEASING.md](./RELEASING.md) for one-time setup.
- Every PR to `main` must bump `Cargo.toml` version and add the matching changelog section.
- On merge, GitHub Actions publishes to crates.io, creates `v<version>` tag, and creates a GitHub release.

## Credits

### Caffeine
Expand Down
35 changes: 35 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Releasing micro-moka

This repository is configured for release-on-merge to `main`.

## One-time setup

1. Add repository secret `CARGO_REGISTRY_TOKEN`.
2. In branch protection for `main`, require this check:
- `Validate release readiness`
3. Keep merge policy as pull-request merge into `main`.

## Per-PR requirements

Every PR targeting `main` must:

1. Bump `[package].version` in `Cargo.toml`.
2. Add the matching changelog header in `CHANGELOG.md`:

```md
## [x.y.z] - YYYY-MM-DD
```

3. Pass `Release Check` workflow (runs `cargo publish --dry-run --locked`).

## What happens on merge to `main`

`Publish Crate` workflow runs automatically:

1. Reads crate `name` and `version` from `Cargo.toml`.
2. Checks whether that version already exists on crates.io.
3. Publishes with `cargo publish --locked` if it is new.
4. Ensures git tag `v<version>` exists.
5. Creates a GitHub release from the matching changelog section.

The publish workflow is idempotent: re-runs will skip publishing if crates.io already has that version.