name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
name: Rust (${{ matrix.toolchain }})
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
toolchain: [stable, 1.88.0]
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Install Rust
run: rustup toolchain install ${{ matrix.toolchain }} --profile minimal --component clippy --component rustfmt
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.toolchain }}
- name: Check
env:
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
run: make check
- name: Package
if: matrix.toolchain == 'stable'
env:
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
run: cargo package --locked
release:
name: Publish crate and GitHub release
needs: check
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: macos-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Install Rust
run: rustup toolchain install stable --profile minimal
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
key: release
- name: Detect release version
id: version
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
python3 - <<'PY'
import os
import subprocess
import tomllib
from pathlib import Path
current = tomllib.loads(Path("Cargo.toml").read_text())["package"]["version"]
before_sha = os.environ["BEFORE_SHA"]
previous = ""
if (
subprocess.run(
["git", "cat-file", "-e", f"{before_sha}^{{commit}}"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
):
previous_manifest = subprocess.check_output(
["git", "show", f"{before_sha}:Cargo.toml"],
text=True,
)
previous = tomllib.loads(previous_manifest)["package"]["version"]
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"current={current}\n")
fh.write(f"previous={previous}\n")
fh.write(f"changed={str(current != previous).lower()}\n")
PY
- name: Inspect release state
id: release_state
env:
VERSION: ${{ steps.version.outputs.current }}
PREVIOUS_VERSION: ${{ steps.version.outputs.previous }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ github.token }}
run: |
python3 - <<'PY'
import io
import json
import os
import subprocess
import sys
import tarfile
import urllib.error
import urllib.request
version = os.environ["VERSION"]
previous_version = os.environ.get("PREVIOUS_VERSION", "")
github_sha = os.environ["GITHUB_SHA"]
repository = os.environ["GITHUB_REPOSITORY"]
github_token = os.environ.get("GITHUB_TOKEN", "")
tag = f"v{version}"
def git_output(*args: str) -> str:
return subprocess.check_output(["git", *args], text=True).strip()
def set_output(name: str, value: str) -> None:
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"{name}={value}\n")
tag_exists = (
subprocess.run(
["git", "rev-parse", "-q", "--verify", f"refs/tags/{tag}"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)
tag_commit = git_output("rev-list", "-n", "1", tag) if tag_exists else ""
published = False
published_sha = ""
crate_metadata_request = urllib.request.Request(
f"https://crates.io/api/v1/crates/monitr/{version}",
headers={"User-Agent": "monitr-release-workflow"},
)
try:
with urllib.request.urlopen(crate_metadata_request):
published = True
except urllib.error.HTTPError as exc:
if exc.code != 404:
raise
if published:
crate_url = f"https://static.crates.io/crates/monitr/monitr-{version}.crate"
crate_request = urllib.request.Request(
crate_url, headers={"User-Agent": "monitr-release-workflow"}
)
with urllib.request.urlopen(crate_request) as response:
crate_bytes = response.read()
with tarfile.open(fileobj=io.BytesIO(crate_bytes), mode="r:gz") as archive:
try:
with archive.extractfile(
f"monitr-{version}/.cargo_vcs_info.json"
) as vcs_info_file:
vcs_info = json.load(vcs_info_file)
except Exception:
vcs_info = {}
published_sha = vcs_info.get("git", {}).get("sha1", "")
release_exists = False
release_url = f"https://api.github.com/repos/{repository}/releases/tags/{tag}"
release_headers = {
"Accept": "application/vnd.github+json",
"User-Agent": "monitr-release-workflow",
}
if github_token:
release_headers["Authorization"] = f"Bearer {github_token}"
release_request = urllib.request.Request(release_url, headers=release_headers)
try:
with urllib.request.urlopen(release_request):
release_exists = True
except urllib.error.HTTPError as exc:
if exc.code != 404:
raise
if published and published_sha and tag_exists and tag_commit != published_sha:
print(
f"::error::{tag} points to {tag_commit}, but crates.io {version} was "
f"published from {published_sha}.",
file=sys.stderr,
)
sys.exit(1)
if (
published
and published_sha
and published_sha != github_sha
and previous_version != version
):
print(
f"::error::monitr {version} is already published on crates.io from "
f"{published_sha}, but this push is {github_sha}. Bump Cargo.toml "
"before publishing another release.",
file=sys.stderr,
)
sys.exit(1)
if not published:
sync_needed = True
publish_needed = True
reason = "current-version-unpublished"
elif published_sha == github_sha:
sync_needed = (not tag_exists) or (not release_exists)
publish_needed = False
reason = (
"repair-github-metadata"
if sync_needed
else "release-already-synced"
)
else:
sync_needed = False
publish_needed = False
reason = "version-already-published-from-earlier-commit"
set_output("tag", tag)
set_output("tag_exists", str(tag_exists).lower())
set_output("tag_commit", tag_commit)
set_output("published", str(published).lower())
set_output("published_sha", published_sha)
set_output("release_exists", str(release_exists).lower())
set_output("sync_needed", str(sync_needed).lower())
set_output("publish_needed", str(publish_needed).lower())
set_output("reason", reason)
PY
- name: Skip release when current version is already synced
if: steps.release_state.outputs.sync_needed != 'true'
run: echo "Skipping release sync for ${{ steps.version.outputs.current }} (${{ steps.release_state.outputs.reason }})."
- name: Verify package
if: steps.release_state.outputs.sync_needed == 'true'
env:
RUSTUP_TOOLCHAIN: stable
run: cargo package --locked
- name: Ensure crates.io token is configured
if: steps.release_state.outputs.publish_needed == 'true'
env:
REPO_CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
REPO_CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
run: |
token="${REPO_CRATES_IO_TOKEN:-${REPO_CARGO_REGISTRY_TOKEN}}"
if [ -z "${token}" ]; then
echo "::error::Set the CARGO_REGISTRY_TOKEN or CRATES_IO_TOKEN repository secret before publishing."
exit 1
fi
echo "::add-mask::${token}"
echo "CARGO_REGISTRY_TOKEN=${token}" >> "${GITHUB_ENV}"
- name: Publish crate
if: steps.release_state.outputs.publish_needed == 'true'
env:
RUSTUP_TOOLCHAIN: stable
run: cargo publish --locked
- name: Create GitHub release
if: steps.release_state.outputs.sync_needed == 'true' && steps.release_state.outputs.release_exists != 'true'
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
tag_name: ${{ steps.release_state.outputs.tag }}
target_commitish: ${{ github.sha }}