chore: migrate to workspace structure and 3-file CI pipeline (#170)

## Summary

- Migrates flat crate structure to mandatory workspace layout
(`crates/cull-gmail/`)
- Replaces old 4-workflow flag-based CI with the 3-file pipeline model
at toolkit 4.9.6
- Fixes pre-existing broken doctest in `rules.rs`

## Changes

### Workspace migration
- `Cargo.toml` → workspace manifest with `[workspace.package]` and
`[workspace.dependencies]`
- `crates/cull-gmail/Cargo.toml` — crate manifest inheriting from
workspace
- `src/`, `tests/`, `CHANGELOG.md` moved to `crates/cull-gmail/`
- `docs/lib/` moved to `crates/cull-gmail/docs/lib/` (required for
`include_str!` to work with `cargo package`)
- `crates/cull-gmail/release.toml` — crate-specific config, tag format
`cull-gmail-v{{version}}`, **PRLOG replacements removed**
- `crates/cull-gmail/release-hook.sh` — updated paths for workspace
layout
- `release.toml` (workspace) — shared signing/branch settings only

### PRLOG
- Added `## [Unreleased]` section at top
- Updated reference links to use `cull-gmail-v*` tag format

### CI files
- `config.yml` — validation-only at toolkit 4.9.6 (no
`trigger_pipeline`)
- `update_prlog.yml` (new) — pr-merged event trigger
- `release.yml` — standard toolkit jobs: `calculate_versions` →
`release_crate` (with `build_binary: true`) → `release_prlog`

### Anchor tag
- `cull-gmail-v0.1.4` created at `v0.1.4` commit — gives nextsv a
baseline for crate-prefixed version calculation

### Bug fix
- Fixed doctest in `rules::Rules::get_rules_by_label_for_action`: added
missing `EolAction` import and corrected method name

## Post-merge setup required (CircleCI project settings)
- `update_prlog.yml` must be set as the trigger for the "pull_request
merged" event
- `release.yml` must be set as a manual trigger pipeline

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Jeremiah Russell
2026-03-13 09:21:54 +00:00
committed by GitHub
46 changed files with 241 additions and 924 deletions

View File

@@ -1,55 +1,16 @@
version: 2.1 version: 2.1
parameters: parameters:
fingerprint:
type: string
default: SHA256:OkxsH8Z6Iim6WDJBaII9eTT9aaO1f3eDc6IpsgYYPVg
min_rust_version: min_rust_version:
type: string type: string
default: "1.88" default: "1.88"
release_flag:
type: boolean
default: false
description: "If true, the release workflow will be executed."
success_flag:
type: boolean
default: false
description: "If true, the success pipeline will be executed."
validation_flag:
type: boolean
default: false
description: "If true, the validation pipeline will be executed."
orbs: orbs:
toolkit: jerus-org/circleci-toolkit@4.4.2 toolkit: jerus-org/circleci-toolkit@4.9.6
# Custom executors removed - using toolkit rolling executors instead
workflows: workflows:
check_last_commit:
when:
and:
- not:
equal: [scheduled_pipeline, << pipeline.trigger_source >>]
- not: << pipeline.parameters.success_flag >>
- not: << pipeline.parameters.release_flag >>
- not: << pipeline.parameters.validation_flag >>
jobs:
- toolkit/choose_pipeline:
name: choose pipeline based on committer
context: bot-check
validation: validation:
when:
and:
- not:
equal: [scheduled_pipeline, << pipeline.trigger_source >>]
- not: << pipeline.parameters.success_flag >>
- << pipeline.parameters.validation_flag >>
- not: << pipeline.parameters.release_flag >>
jobs: jobs:
# Signature verification for trusted PRs (with write access for comments)
- toolkit/verify_commit_signatures: - toolkit/verify_commit_signatures:
name: verify_commit_signatures_trusted name: verify_commit_signatures_trusted
context: bot-check context: bot-check
@@ -60,121 +21,70 @@ workflows:
ignore: ignore:
- main - main
- /pull\/[0-9]+/ - /pull\/[0-9]+/
# Signature verification for forked PRs (read-only, no comments)
- toolkit/verify_commit_signatures: - toolkit/verify_commit_signatures:
name: verify_commit_signatures_forked name: verify_commit_signatures_forked
post_comment: false post_comment: false
update_pcu: false update_pcu: false
filters: filters:
branches: branches:
only: /pull\/[0-9]+/ only:
- toolkit/label: - /pull\/[0-9]+/
- toolkit/required_builds_rolling:
min_rust_version: << pipeline.parameters.min_rust_version >> min_rust_version: << pipeline.parameters.min_rust_version >>
context: pcu-app
update_pcu: true - toolkit/optional_builds:
min_rust_version: << pipeline.parameters.min_rust_version >>
filters:
branches:
ignore: main
- toolkit/test_doc_build:
min_rust_version: << pipeline.parameters.min_rust_version >>
filters:
branches:
ignore: main
- toolkit/idiomatic_rust:
min_rust_version: << pipeline.parameters.min_rust_version >>
filters:
branches:
ignore: main
- toolkit/common_tests_rolling:
min_rust_version: << pipeline.parameters.min_rust_version >>
- toolkit/security:
name: security audit only
sonarcloud: false
# RUSTSEC-2025-0066: google-apis-common unmaintained — core transitive
# dependency of google-gmail1; no maintained Gmail API alternative exists.
ignore_advisories: RUSTSEC-2025-0066
filters: filters:
branches: branches:
only: only:
- main - main
- toolkit/required_builds: - /pull\/[0-9]+/
min_rust_version: << pipeline.parameters.min_rust_version >>
- toolkit/optional_builds:
min_rust_version: << pipeline.parameters.min_rust_version >>
- toolkit/test_doc_build:
min_rust_version: << pipeline.parameters.min_rust_version >>
- toolkit/common_tests:
min_rust_version: << pipeline.parameters.min_rust_version >>
test_runner: nextest
nextest_profile: ci
post-steps:
- store_test_results:
path: target/nextest/ci/junit.xml
- toolkit/idiomatic_rust:
min_rust_version: << pipeline.parameters.min_rust_version >>
- toolkit/security:
name: security audit only
sonarcloud: false
ignore_advisories: RUSTSEC-2025-0066
filters:
branches:
only: /pull\/[0-9]+/
- toolkit/security: - toolkit/security:
name: security with sonarcloud name: security with sonarcloud
context: SonarCloud context: SonarCloud
# RUSTSEC-2025-0066: google-apis-common unmaintained — core transitive
# dependency of google-gmail1; no maintained Gmail API alternative exists.
ignore_advisories: RUSTSEC-2025-0066 ignore_advisories: RUSTSEC-2025-0066
filters: filters:
branches: branches:
ignore: ignore:
- /pull\/[0-9]+/ - /pull\/[0-9]+/
- main - main
- toolkit/update_prlog:
- toolkit/code_coverage:
min_rust_version: << pipeline.parameters.min_rust_version >>
package: cull-gmail
context: SonarCloud
filters: filters:
branches: branches:
ignore: ignore:
- /pull\/[0-9]+/ - /pull\/[0-9]+/
- main - main
requires:
- verify_commit_signatures_trusted
- toolkit/required_builds
- toolkit/test_doc_build
- toolkit/idiomatic_rust
- security audit only
- security with sonarcloud
- toolkit/common_tests
context:
- release
- bot-check
ssh_fingerprint: << pipeline.parameters.fingerprint >>
min_rust_version: << pipeline.parameters.min_rust_version >>
on_success:
when:
and:
- not:
equal: [scheduled_pipeline, << pipeline.trigger_source >>]
- << pipeline.parameters.success_flag >>
- not: << pipeline.parameters.validation_flag >>
- not: << pipeline.parameters.release_flag >>
jobs:
- toolkit/end_success
release:
when:
and:
- or:
- and:
- equal: [scheduled_pipeline, << pipeline.trigger_source >>]
- equal: ["release check", << pipeline.schedule.name >>]
- << pipeline.parameters.release_flag >>
- not: << pipeline.parameters.success_flag >>
- not: << pipeline.parameters.validation_flag >>
jobs:
- toolkit/save_next_version:
min_rust_version: << pipeline.parameters.min_rust_version >>
- toolkit/make_release:
requires:
- toolkit/save_next_version
pre-steps:
- attach_workspace:
at: /tmp/workspace
- run:
name: Set SEMVER based on next-version file
command: |
set +ex
export SEMVER=$(cat /tmp/workspace/next-version)
echo $SEMVER
echo "export SEMVER=$SEMVER" >> "$BASH_ENV"
context:
- release
- bot-check
ssh_fingerprint: << pipeline.parameters.fingerprint >>
min_rust_version: << pipeline.parameters.min_rust_version >>
when_get_version: false
- toolkit/no_release:
min_rust_version: << pipeline.parameters.min_rust_version >>
requires:
- toolkit/save_next_version:
- failed

View File

@@ -1,419 +1,21 @@
version: 2.1 version: 2.1
parameters: parameters:
fingerprint: cull_gmail_version:
type: string
default: SHA256:OkxsH8Z6Iim6WDJBaII9eTT9aaO1f3eDc6IpsgYYPVg
min_rust_version:
type: string
default: "1.88"
# Version override for crate release (used when nextsv cannot calculate)
# Set to empty string "" to use nextsv auto-detection
crate_version_override:
type: string type: string
default: "" default: ""
# Version override for workspace/PRLOG release description: "Override cull-gmail crate version (empty = nextsv auto-detect)"
# Set to empty string "" to use nextsv auto-detection workspace_version:
workspace_version_override:
type: string type: string
default: "" default: ""
description: "Override workspace v* version (empty = nextsv auto-detect)"
orbs: orbs:
toolkit: jerus-org/circleci-toolkit@4.4.2 toolkit: jerus-org/circleci-toolkit@4.9.6
# Commands designed for future migration to circleci-toolkit
# These extend existing toolkit patterns with backward-compatible parameters
commands:
# New command: Check if version exists on crates.io
# Used for recovery scenarios where publish succeeded but workflow failed
check_crates_io_version:
description: >
Check if a version already exists on crates.io.
Sets SKIP_PUBLISH=true if version exists, false otherwise.
Used for recovery scenarios where crates.io publish succeeded but workflow failed afterward.
parameters:
package:
type: string
description: "Crate name on crates.io"
steps:
- run:
name: install kdeets
command: cargo install kdeets
- run:
name: Check crates.io for << parameters.package >>
command: |
set -eo pipefail
# Use SEMVER or NEXT_VERSION (whichever is set)
VERSION="${SEMVER:-${NEXT_VERSION:-none}}"
if [ "$VERSION" = "none" ]; then
echo "No version to check"
echo "export SKIP_PUBLISH=false" >> "$BASH_ENV"
exit 0
fi
USER_AGENT="circleci-toolkit/1.0 (https://github.com/jerus-org/circleci-toolkit)"
most_recent_published="$(kdeets crate -br cull-gmail)"
if [[ ${most_recent_published} == ${VERSION} ]]; then
echo "Version ${VERSION} exists on crates.io - will skip publish"
echo "export SKIP_PUBLISH=true" >> "$BASH_ENV"
else
echo "Version ${VERSION} not found on crates.io - will publish"
echo "export SKIP_PUBLISH=false" >> "$BASH_ENV"
fi
# New command: Check if release tag already exists
# Used for recovery scenarios where tag was created but workflow failed
check_tag_exists:
description: >
Check if the release tag already exists.
Sets SKIP_RELEASE=true if tag exists, false otherwise.
Used for recovery scenarios where release partially succeeded.
parameters:
package:
type: string
description: "Package name (used to construct tag name)"
steps:
- run:
name: Check if tag exists for << parameters.package >>
command: |
set -eo pipefail
# Use SEMVER or NEXT_VERSION (whichever is set)
VERSION="${SEMVER:-${NEXT_VERSION:-none}}"
if [ "$VERSION" = "none" ]; then
echo "No version to check"
echo "export SKIP_RELEASE=false" >> "$BASH_ENV"
exit 0
fi
if [ "" = "<< parameters.package >>" ]; then
TAG="v${VERSION}"
else
TAG="<< parameters.package >>-v${VERSION}"
fi
# Fetch tags from remote
git fetch --tags
if git tag -l "$TAG" | grep -q .; then
echo "Tag ${TAG} already exists - will skip release"
echo "export SKIP_RELEASE=true" >> "$BASH_ENV"
else
echo "Tag ${TAG} not found - will proceed with release"
echo "export SKIP_RELEASE=false" >> "$BASH_ENV"
fi
# Enhanced make_cargo_release with conditional publish support
# Backward compatible: publishes by default unless SKIP_PUBLISH=true or publish=false
# Also respects SKIP_RELEASE=true to skip entirely when tag already exists
make_cargo_release:
description: >
Make a release using cargo release.
Enhanced version that respects SKIP_PUBLISH environment variable for recovery scenarios.
When SKIP_PUBLISH=true, adds --no-publish flag to skip crates.io publish.
The publish parameter controls default behavior; SKIP_PUBLISH overrides it at runtime.
parameters:
package:
type: string
default: ""
description: "Package to release"
verbosity:
type: string
default: "-vv"
description: "Verbosity for cargo release"
publish:
type: boolean
default: true
description: "If true, the release will be published to crates.io"
no_push:
type: boolean
default: false
description: "Whether cargo release should push the changes"
steps:
- run:
name: List changes using cargo release
command: |
set -exo pipefail
cargo release changes
- run:
name: Execute cargo release
command: |
set -exo pipefail
# Check if release should be skipped (tag already exists)
if [ "$SKIP_RELEASE" = "true" ]; then
echo "Skipping release (tag already exists)"
exit 0
fi
# Use SEMVER or NEXT_VERSION
VERSION="${SEMVER:-${NEXT_VERSION:-none}}"
if [ "$VERSION" = "none" ]; then
echo "No version to release - skipping"
exit 0
fi
echo "Releasing version: $VERSION"
# Build cargo release arguments
release_args="--execute --no-confirm --sign-tag"
if [ "<< parameters.package >>" != "" ]; then
release_args="$release_args --package << parameters.package >>"
fi
if [ "<< parameters.no_push >>" = "true" ]; then
release_args="$release_args --no-push"
fi
# Handle publish: parameter controls default, SKIP_PUBLISH overrides at runtime
if [ "<< parameters.publish >>" = "false" ]; then
release_args="$release_args --no-publish"
echo "Publishing disabled by parameter"
elif [ "$SKIP_PUBLISH" = "true" ]; then
release_args="$release_args --no-publish"
echo "Skipping publish (version already on crates.io)"
fi
# Map verbosity
case "<< parameters.verbosity >>" in
"-vvv"|"-vvvv")
release_args="-vv $release_args"
;;
esac
cargo release $release_args "$VERSION"
# Enhanced make_github_release with package support
make_github_release:
description: >
Create a GitHub release using the pcu utility.
When package is provided, uses 'pcu release package' which derives the correct tag prefix.
When package is empty, uses 'pcu release version' with the specified prefix.
parameters:
prefix:
type: string
default: "v"
description: "Tag prefix for the release (used when package is empty)"
package:
type: string
default: ""
description: "Package name - when provided, derives tag prefix automatically"
verbosity:
type: string
default: "-vv"
description: "Verbosity for pcu command"
update_prlog:
type: boolean
default: false
description: "Update PRLOG when creating the release"
steps:
- run:
name: Create GitHub release
command: |
set -exo pipefail
# Use SEMVER or NEXT_VERSION
VERSION="${SEMVER:-${NEXT_VERSION:-none}}"
if [ "$VERSION" = "none" ]; then
echo "No version to release - skipping GitHub release"
exit 0
fi
# Determine the tag name
if [ "<< parameters.package >>" != "" ]; then
TAG="<< parameters.package >>-v${VERSION}"
else
TAG="<< parameters.prefix >>${VERSION}"
fi
# Check if GitHub release already exists using API
# Extract owner/repo from git remote
REPO_URL=$(git remote get-url origin)
REPO_PATH=$(echo "$REPO_URL" | sed -E 's|.*github\.com[:/]||' | sed 's|\.git$||')
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${REPO_PATH}/releases/tags/${TAG}")
if [ "$HTTP_STATUS" = "200" ]; then
echo "GitHub release ${TAG} already exists - skipping"
exit 0
fi
echo "GitHub release ${TAG} not found (HTTP ${HTTP_STATUS}) - will create"
pcu_args="<< parameters.verbosity >> release"
if [ "<< parameters.package >>" != "" ]; then
pcu_args="$pcu_args package << parameters.package >>"
else
pcu_args="$pcu_args --prefix << parameters.prefix >> version"
fi
if [ "<< parameters.update_prlog >>" = "true" ]; then
pcu_args="$pcu_args --update-prlog"
fi
pcu $pcu_args ${VERSION}
# Build a release binary using cargo.
# Requires VERSION environment variable (skips if "none").
build_release_binary:
description: >
Build a release-optimised binary using cargo build --release.
Expects VERSION to be set in BASH_ENV; skips when VERSION is "none".
steps:
- run:
name: Build release binary
command: |
set -eo pipefail
if [ "$VERSION" = "none" ]; then
echo "No version to release - skipping"
exit 0
fi
cargo build --release
# Package a binary from target/release into a .tar.gz archive.
# Sets ASSET_NAME in BASH_ENV for use by upload_release_asset.
package_binary:
description: >
Package a binary from target/release into a .tar.gz archive named
<binary_name>-<target>.tar.gz. Sets ASSET_NAME in BASH_ENV.
parameters:
binary_name:
type: string
description: "Name of the binary in target/release"
target:
type: string
default: x86_64-unknown-linux-gnu
description: "Rust target triple for the archive name"
steps:
- run:
name: Package binary as tar.gz
command: |
set -eo pipefail
if [ "$VERSION" = "none" ]; then
echo "No version to release - skipping"
exit 0
fi
ASSET_NAME="<< parameters.binary_name >>-<< parameters.target >>.tar.gz"
tar czf "$ASSET_NAME" -C target/release "<< parameters.binary_name >>"
echo "Created $ASSET_NAME ($(du -h "$ASSET_NAME" | cut -f1))"
echo "export ASSET_NAME=$ASSET_NAME" >> "$BASH_ENV"
# Upload a binary asset to a GitHub release.
# Adapted from toolkit/upload_release_asset with release_tag parameter
# so it works in pipelines where CIRCLE_TAG is not set.
# TODO: Replace with toolkit/upload_release_asset once it accepts a
# release_tag parameter (circleci-toolkit#333).
upload_release_asset:
description: >
Upload a binary asset to a GitHub release. Accepts a release_tag
parameter instead of relying on CIRCLE_TAG, making it usable in
scheduled/manual pipelines. Requires GITHUB_TOKEN environment variable.
parameters:
asset_path:
type: string
description: "Path to the asset file to upload"
asset_name:
type: string
default: ""
description: "Name for the asset in the release (defaults to filename)"
release_tag:
type: string
description: "Git tag identifying the GitHub release"
github_token_var:
type: env_var_name
default: GITHUB_TOKEN
description: "Environment variable containing the GitHub token"
steps:
- run:
name: Upload asset to GitHub release
command: |
set -eo pipefail
if [ "$VERSION" = "none" ]; then
echo "No version to release - skipping"
exit 0
fi
ASSET_PATH="<< parameters.asset_path >>"
ASSET_NAME="<< parameters.asset_name >>"
TOKEN="${<< parameters.github_token_var >>}"
TAG="<< parameters.release_tag >>"
if [ -z "${ASSET_NAME}" ]; then
ASSET_NAME="$(basename "${ASSET_PATH}")"
fi
if [ ! -f "${ASSET_PATH}" ]; then
echo "ERROR: Asset file not found: ${ASSET_PATH}" >&2
exit 1
fi
# Derive repo slug from git remote
REPO_URL=$(git remote get-url origin)
REPO_SLUG=$(echo "$REPO_URL" | sed -E 's|.*github\.com[:/]||' | sed 's|\.git$||')
echo "Repository: ${REPO_SLUG}"
echo "Looking up release for tag ${TAG}..."
RELEASE_RESPONSE=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer ${TOKEN}" \
"https://api.github.com/repos/${REPO_SLUG}/releases/tags/${TAG}")
HTTP_CODE=$(echo "${RELEASE_RESPONSE}" | tail -1)
RELEASE_BODY=$(echo "${RELEASE_RESPONSE}" | sed '$d')
if [ "${HTTP_CODE}" != "200" ]; then
echo "ERROR: GitHub API returned HTTP ${HTTP_CODE}" >&2
echo "${RELEASE_BODY}" | jq -r '.message // .' >&2
exit 1
fi
RELEASE_ID=$(echo "${RELEASE_BODY}" | jq -r '.id')
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
echo "ERROR: Could not find GitHub release for tag ${TAG}" >&2
exit 1
fi
echo "Uploading ${ASSET_NAME} to release ${RELEASE_ID}..."
UPLOAD_RESPONSE=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/${REPO_SLUG}/releases/${RELEASE_ID}/assets?name=${ASSET_NAME}" \
--data-binary "@${ASSET_PATH}")
HTTP_CODE=$(echo "${UPLOAD_RESPONSE}" | tail -1)
UPLOAD_BODY=$(echo "${UPLOAD_RESPONSE}" | sed '$d')
if [ "${HTTP_CODE}" != "201" ]; then
echo "ERROR: Upload failed with HTTP ${HTTP_CODE}" >&2
echo "${UPLOAD_BODY}" | jq -r '.message // .' >&2
exit 1
fi
echo "Upload complete: ${ASSET_NAME}"
jobs: jobs:
tools: tools:
executor: executor: toolkit/rust_env_rolling
name: toolkit/rust_env_rolling
steps: steps:
- run: - run:
name: Verify tools name: Verify tools
@@ -423,314 +25,39 @@ jobs:
pcu --version pcu --version
cargo release --version cargo release --version
jq --version jq --version
rsign --version
# Calculate and display versions for approval
# Persists calculated versions to workspace for downstream jobs
calculate-versions:
parameters:
crate_version:
type: string
default: ""
description: "Override version for crate release (empty = auto-detect)"
workspace_version:
type: string
default: ""
description: "Override version for workspace/PRLOG release (empty = auto-detect)"
executor:
name: toolkit/rust_env_rolling
steps:
- checkout
- run:
name: Calculate release versions
command: |
set -eo pipefail
echo "=============================================="
echo " RELEASE VERSION CALCULATION"
echo "=============================================="
echo ""
# Check for version override (passed from workflow)
VERSION_OVERRIDE="<< parameters.crate_version >>"
# Calculate crate version
echo "--- Crate: cull-gmail ---"
if [ -n "$VERSION_OVERRIDE" ]; then
CRATE_VERSION="$VERSION_OVERRIDE"
echo "Result: Will release version $CRATE_VERSION (OVERRIDE)"
echo "Note: Version explicitly set via crate_version parameter"
NEXTSV_VERSION=$(nextsv -bn calculate 2>/dev/null || echo "none")
echo " (nextsv would have calculated: $NEXTSV_VERSION)"
else
CRATE_VERSION=$(nextsv -bn calculate 2>/dev/null || echo "")
if [ -z "$CRATE_VERSION" ]; then
CRATE_VERSION="none"
echo "Result: No crate release needed"
echo "Reason: No changes to crates/cull-gmail/ or its dependencies"
else
echo "Result: Will release version $CRATE_VERSION"
echo "Changes detected in crate scope (code, deps, or Cargo.lock)"
fi
fi
echo ""
# Calculate workspace version
echo "--- Workspace (PRLOG) ---"
WORKSPACE_OVERRIDE="<< parameters.workspace_version >>"
if [ -n "$WORKSPACE_OVERRIDE" ]; then
WORKSPACE_VERSION="$WORKSPACE_OVERRIDE"
echo "Result: Will release version $WORKSPACE_VERSION (OVERRIDE)"
echo "Note: Version explicitly set via workspace_version parameter"
NEXTSV_WS=$(nextsv -bn calculate --prefix "v" 2>/dev/null || echo "none")
echo " (nextsv would have calculated: $NEXTSV_WS)"
else
WORKSPACE_VERSION=$(nextsv -bn calculate --prefix "v" 2>/dev/null || echo "")
if [ -z "$WORKSPACE_VERSION" ]; then
WORKSPACE_VERSION="none"
echo "Result: No workspace release needed"
echo "Reason: No changes since last v* tag"
else
echo "Result: Will release version $WORKSPACE_VERSION"
echo "Changes detected in workspace scope"
fi
fi
echo ""
echo "=============================================="
echo " SUMMARY"
echo "=============================================="
echo "Crate (cull-gmail): $CRATE_VERSION"
echo "Workspace (PRLOG): $WORKSPACE_VERSION"
echo "=============================================="
echo ""
# Validation rules
echo "--- Validation ---"
if [ "$CRATE_VERSION" != "none" ] && [ "$WORKSPACE_VERSION" = "none" ]; then
echo "WARNING: Crate release without workspace release is unusual"
echo " Workspace should increment when crate changes"
fi
if [ "$CRATE_VERSION" = "none" ] && [ "$WORKSPACE_VERSION" = "none" ]; then
echo "INFO: No releases needed - workflow will skip release steps"
fi
echo ""
echo "Please review the versions above and approve to proceed."
# Persist versions to workspace for downstream jobs
mkdir -p /tmp/release-versions
echo "$CRATE_VERSION" > /tmp/release-versions/crate-version
echo "$WORKSPACE_VERSION" > /tmp/release-versions/workspace-version
echo "Versions persisted to workspace for release jobs"
- persist_to_workspace:
root: /tmp
paths:
- release-versions
# Release a single crate
# Reads version from workspace (calculated by calculate-versions job)
release-crate:
parameters:
package:
type: string
default: ""
executor:
name: toolkit/rust_env_rolling
steps:
- checkout
- attach_workspace:
at: /tmp
- add_ssh_keys:
fingerprints:
- << pipeline.parameters.fingerprint >>
- run:
name: Remove original SSH key from agent
command: |
ssh-add -l
# GitHub App integration doesn't create id_rsa.pub, handle gracefully
if [ -f ~/.ssh/id_rsa.pub ]; then
ssh-add -d ~/.ssh/id_rsa.pub
else
echo "No id_rsa.pub found (GitHub App integration) - skipping removal"
fi
ssh-add -l
- toolkit/gpg_key
- toolkit/git_config
# Step 1: Load version from workspace (calculated by calculate-versions)
- run:
name: Load version from workspace
command: |
set -eo pipefail
VERSION_FILE="/tmp/release-versions/crate-version"
if [ -f "$VERSION_FILE" ]; then
VERSION=$(cat "$VERSION_FILE")
echo "Loaded version from workspace: $VERSION"
echo "export NEXT_VERSION=$VERSION" >> "$BASH_ENV"
echo "export SEMVER=$VERSION" >> "$BASH_ENV"
else
echo "ERROR: Version file not found at $VERSION_FILE"
echo "This job requires calculate-versions to run first"
exit 1
fi
# Step 2: Check crates.io for recovery scenarios
- check_crates_io_version:
package: << parameters.package >>
# Step 3: Check if tag already exists for recovery scenarios
- check_tag_exists:
package: << parameters.package >>
# Step 4: Run cargo release (respects SKIP_PUBLISH and SKIP_RELEASE)
- make_cargo_release:
package: << parameters.package >>
verbosity: "-vv"
# Step 5: Update pcu to latest version (command not yet in toolkit release)
- run:
name: Update to latest pcu
command: |
cargo install --force --git https://github.com/jerus-org/pcu --branch main
# Step 6: Create GitHub release
- make_github_release:
package: << parameters.package >>
verbosity: "-vv"
# # Release PRLOG/workspace
# # Reads version from workspace (calculated by calculate-versions job)
# release-prlog:
# executor:
# name: toolkit/rust_env_rolling
# steps:
# - checkout
# - attach_workspace:
# at: /tmp
# - add_ssh_keys:
# fingerprints:
# - << pipeline.parameters.fingerprint >>
# - run:
# name: Remove original SSH key from agent
# command: |
# ssh-add -l
# # GitHub App integration doesn't create id_rsa.pub, handle gracefully
# if [ -f ~/.ssh/id_rsa.pub ]; then
# ssh-add -d ~/.ssh/id_rsa.pub
# else
# echo "No id_rsa.pub found (GitHub App integration) - skipping removal"
# fi
# ssh-add -l
# - toolkit/gpg_key
# - toolkit/git_config
# - run:
# name: Release PRLOG
# command: |
# set -exo pipefail
# chmod +x scripts/*.sh
# # Load version from workspace (calculated by calculate-versions)
# VERSION_FILE="/tmp/release-versions/workspace-version"
# if [ -f "$VERSION_FILE" ]; then
# VERSION=$(cat "$VERSION_FILE")
# echo "Loaded workspace version from workspace: $VERSION"
# else
# echo "ERROR: Version file not found at $VERSION_FILE"
# echo "This job requires calculate-versions to run first"
# exit 1
# fi
# if [ "$VERSION" = "none" ]; then
# echo "No PRLOG release needed"
# exit 0
# fi
# # Check if tag already exists
# TAG="v${VERSION}"
# git fetch --tags origin main
# # Pull latest main (release-crate may have pushed commits)
# git pull --rebase origin main
# if git tag -l "$TAG" | grep -q .; then
# echo "Tag $TAG already exists - updating PRLOG without new tag"
# # Update PRLOG.md with the release date but don't create new tag
# ./scripts/release-prlog.sh "$VERSION" --no-tag 2>/dev/null || \
# ./scripts/release-prlog.sh "$VERSION"
# git push origin main || echo "No changes to push"
# else
# echo "Creating new release for version $VERSION"
# ./scripts/release-prlog.sh "$VERSION"
# git push origin main --tags
# fi
# Build release binary and upload to GitHub release for cargo-binstall
release-binary:
executor:
name: toolkit/rust_env_rolling
steps:
- checkout
- attach_workspace:
at: /tmp
- run:
name: Load version from workspace
command: |
set -eo pipefail
VERSION_FILE="/tmp/release-versions/crate-version"
if [ -f "$VERSION_FILE" ]; then
VERSION=$(cat "$VERSION_FILE")
echo "Loaded version from workspace: $VERSION"
echo "export VERSION=$VERSION" >> "$BASH_ENV"
else
echo "ERROR: Version file not found at $VERSION_FILE"
exit 1
fi
- build_release_binary
- package_binary:
binary_name: cull-gmail
- upload_release_asset:
asset_path: cull-gmail-x86_64-unknown-linux-gnu.tar.gz
release_tag: v${VERSION}
workflows: workflows:
release: release:
jobs: jobs:
- tools - tools
# Calculate and display versions for review - toolkit/calculate_versions:
# Pass pipeline parameters to job for version overrides name: calculate-versions
- calculate-versions:
requires: [tools] requires: [tools]
crate_version: << pipeline.parameters.crate_version_override >> crates: "cull-gmail:cull-gmail-v"
workspace_version: << pipeline.parameters.workspace_version_override >> crate_version_overrides: "cull-gmail:<< pipeline.parameters.cull_gmail_version >>"
workspace_version_override: << pipeline.parameters.workspace_version >>
# Manual approval gate - review calculated versions before release
- approve-release: - approve-release:
type: approval type: approval
requires: [calculate-versions] requires: [calculate-versions]
# Release cull-gmail crate - toolkit/release_crate:
# Version is read from workspace (set by calculate-versions)
# Pipeline parameter crate_version_override controls the override
# Set parameter to "" to resume nextsv auto-detection
- release-crate:
name: release-cull-gmail name: release-cull-gmail
requires: [approve-release] requires: [approve-release]
package: cull-gmail
crate_tag_prefix: cull-gmail-v
build_binary: true
binary_name: cull-gmail
context: context:
- release - release
- bot-check - bot-check
- pcu-app
# Build and upload release binary for cargo-binstall - toolkit/release_prlog:
- release-binary:
requires: [release-cull-gmail] requires: [release-cull-gmail]
context: context:
- release - release
- bot-check - bot-check
- pcu-app
# Release PRLOG (after crate released)
- toolkit/make_release:
requires: [release-cull-gmail]
context:
- release
- bot-check
ssh_fingerprint: << pipeline.parameters.fingerprint >>
min_rust_version: << pipeline.parameters.min_rust_version >>
when_cargo_release: false
when_use_workspace: false
pcu_update_prlog: true
remove_ssh_key: false

View File

@@ -0,0 +1,30 @@
version: 2.1
parameters:
update_pcu:
type: boolean
default: false
description: "If true, pcu is updated from its main github branch before running."
orbs:
toolkit: jerus-org/circleci-toolkit@4.9.6
workflows:
update_prlog:
jobs:
- toolkit/update_prlog:
name: update-prlog-on-main
context:
- release
- bot-check
- pcu-app
min_rust_version: "1.88"
target_branch: "main"
pcu_from_merge: --from-merge
update_pcu: << pipeline.parameters.update_pcu >>
pcu_verbosity: "-vvv"
- toolkit/label:
min_rust_version: "1.88"
context: pcu-app
requires:
- update-prlog-on-main

View File

@@ -1,73 +1,49 @@
[package] [workspace]
name = "cull-gmail" members = ["crates/cull-gmail"]
description = "Cull emails from a gmail account using the gmail API" resolver = "2"
version = "0.1.4"
authors = ["Jeremiah Russell <jrussell@jerus.ie>"] [workspace.package]
edition = "2024" edition = "2024"
authors = ["Jeremiah Russell <jrussell@jerus.ie>"]
rust-version = "1.88" rust-version = "1.88"
license = "MIT"
repository = "https://github.com/jerus-org/cull-gmail"
keywords = ["gmail", "gmail-api", "management"] keywords = ["gmail", "gmail-api", "management"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/jerus-org/cull-gmail"
include = [
"**/*.rs",
"Cargo.toml",
"README.md",
"LICENSE-MIT",
"LICENSE-APACHE",
"CHANGELOG.md",
"docs",
]
[dependencies] [workspace.dependencies]
base64 = "0.22.1"
chrono = "0.4.43" chrono = "0.4.43"
clap = { version = "4.5.58", features = ["derive"] } clap = { version = "4.5.58", features = ["derive"] }
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] } clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
config = { version = "0.15.19", default-features = false, features = [ config = { version = "0.15.19", default-features = false, features = ["json", "toml"] }
"json", dialoguer = "0.12.0"
"toml",
] }
env_logger = "0.11.9" env_logger = "0.11.9"
flate2 = "1.1.9"
futures = "0.3.31"
google-gmail1 = { version = "7.0.0", default-features = false, features = ["yup-oauth2", "aws-lc-rs"] } google-gmail1 = { version = "7.0.0", default-features = false, features = ["yup-oauth2", "aws-lc-rs"] }
hyper-rustls = { version = "0.27.7", features = ["http1"] } hyper-rustls = { version = "0.27.7", features = ["http1"] }
indicatif = "0.18.3"
lazy-regex = "3.6.0" lazy-regex = "3.6.0"
log = "0.4.29" log = "0.4.29"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
thiserror = "2.0.18" thiserror = "2.0.18"
tokio = { version = "1.49.0", features = [ tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "process"] }
"macros",
"rt-multi-thread",
"process",
] }
toml = "1.0.0" toml = "1.0.0"
base64 = "0.22.1"
flate2 = "1.1.9"
dialoguer = "0.12.0"
indicatif = "0.18.3"
[dev-dependencies] # dev-dependencies
httpmock = "0.8.3"
tokio-test = "0.4.5"
temp-env = "0.3.6"
tempfile = "3.25.0"
futures = "0.3.31"
assert_cmd = "2.1.2" assert_cmd = "2.1.2"
assert_fs = "1.1.3" assert_fs = "1.1.3"
httpmock = "0.8.3"
predicates = "3.1.4" predicates = "3.1.4"
temp-env = "0.3.6"
tempfile = "3.25.0"
tokio-test = "0.4.5"
[lints.clippy] [workspace.lints.clippy]
uninlined-format-args = "warn" uninlined-format-args = "warn"
unnecessary_semicolon = "warn" unnecessary_semicolon = "warn"
[lints.rust] [workspace.lints.rust]
dead-code = "allow" # allow temporarily while developing initial code dead-code = "allow"
[lib]
name = "cull_gmail"
path = "src/lib.rs"
[[bin]]
name = "cull-gmail"
path = "src/cli/main.rs"

View File

@@ -5,6 +5,8 @@ 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/) 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.4] - 2026-02-14 ## [0.1.4] - 2026-02-14
### Changed ### Changed
@@ -477,8 +479,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#161]: https://github.com/jerus-org/cull-gmail/pull/161 [#161]: https://github.com/jerus-org/cull-gmail/pull/161
[#162]: https://github.com/jerus-org/cull-gmail/pull/162 [#162]: https://github.com/jerus-org/cull-gmail/pull/162
[#163]: https://github.com/jerus-org/cull-gmail/pull/163 [#163]: https://github.com/jerus-org/cull-gmail/pull/163
[0.1.4]: https://github.com/jerus-org/cull-gmail/compare/v0.1.3...v0.1.4 [Unreleased]: https://github.com/jerus-org/cull-gmail/compare/cull-gmail-v0.1.4...HEAD
[0.1.3]: https://github.com/jerus-org/cull-gmail/compare/v0.1.2...v0.1.3 [0.1.4]: https://github.com/jerus-org/cull-gmail/compare/cull-gmail-v0.1.3...cull-gmail-v0.1.4
[0.1.3]: https://github.com/jerus-org/cull-gmail/compare/cull-gmail-v0.1.2...cull-gmail-v0.1.3
[0.1.2]: https://github.com/jerus-org/cull-gmail/compare/v0.1.1...v0.1.2 [0.1.2]: https://github.com/jerus-org/cull-gmail/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/jerus-org/cull-gmail/compare/v0.1.0...v0.1.1 [0.1.1]: https://github.com/jerus-org/cull-gmail/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/jerus-org/cull-gmail/compare/v0.0.16...v0.1.0 [0.1.0]: https://github.com/jerus-org/cull-gmail/compare/v0.0.16...v0.1.0

View File

@@ -0,0 +1,62 @@
[package]
name = "cull-gmail"
description = "Cull emails from a gmail account using the gmail API"
version = "0.1.4"
edition.workspace = true
authors.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
readme = "README.md"
include = [
"**/*.rs",
"Cargo.toml",
"README.md",
"LICENSE-MIT",
"LICENSE-APACHE",
"CHANGELOG.md",
"docs",
]
[dependencies]
base64.workspace = true
chrono.workspace = true
clap.workspace = true
clap-verbosity-flag.workspace = true
config.workspace = true
dialoguer.workspace = true
env_logger.workspace = true
flate2.workspace = true
google-gmail1.workspace = true
hyper-rustls.workspace = true
indicatif.workspace = true
lazy-regex.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true
[dev-dependencies]
assert_cmd.workspace = true
assert_fs.workspace = true
futures.workspace = true
httpmock.workspace = true
predicates.workspace = true
temp-env.workspace = true
tempfile.workspace = true
tokio-test.workspace = true
[lints]
workspace = true
[lib]
name = "cull_gmail"
path = "src/lib.rs"
[[bin]]
name = "cull-gmail"
path = "src/cli/main.rs"

View File

@@ -0,0 +1,16 @@
#!/bin/sh
# Build an updated README
cat ../../docs/readme/head.md > ../../README.md
# shellcheck disable=SC2129
cat ../../docs/main.md >> ../../README.md
cat ../../docs/lib.md >> ../../README.md
cat ../../docs/readme/tail.md >> ../../README.md
# Build Changelog
gen-changelog generate \
--display-summaries \
--name "CHANGELOG.md" \
--package "cull-gmail" \
--repository-dir "../.." \
--next-version "${NEW_VERSION:-${SEMVER}}"

View File

@@ -0,0 +1,7 @@
pre-release-commit-message = "chore: Release cull-gmail v{{version}}"
tag-message = "{{tag_name}}"
tag-name = "cull-gmail-v{{version}}"
pre-release-hook = ["./release-hook.sh"]
pre-release-replacements = [
{ file = "../../docs/lib.md", search = "cull-gmail = \"\\d+\\.\\d+\\.\\d+\"", replace = "cull-gmail = \"{{version}}\"", exactly = 1 },
]

View File

@@ -361,13 +361,13 @@ impl Rules {
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use cull_gmail::{Rules, Retention, MessageAge}; /// use cull_gmail::{Rules, Retention, MessageAge, EolAction};
/// ///
/// let mut rules = Rules::new(); /// let mut rules = Rules::new();
/// let retention = Retention::new(MessageAge::Days(30), false); /// let retention = Retention::new(MessageAge::Days(30), false);
/// rules.add_rule(retention, Some("test"), false); /// rules.add_rule(retention, Some("test"), false);
/// ///
/// let label_map = rules.get_rules_by_label(EolAction::Trash); /// let label_map = rules.get_rules_by_label_for_action(EolAction::Trash);
/// if let Some(rule) = label_map.get("test") { /// if let Some(rule) = label_map.get("test") {
/// println!("Rule for 'test' label: {}", rule.describe()); /// println!("Rule for 'test' label: {}", rule.describe());
/// } /// }

View File

@@ -29,41 +29,10 @@ mod test_utils {
let config_dir = temp_dir.path().join(".config").join("cull-gmail"); let config_dir = temp_dir.path().join(".config").join("cull-gmail");
fs::create_dir_all(&config_dir)?; fs::create_dir_all(&config_dir)?;
// Get the path to the compiled binary - try multiple locations // Use CARGO_BIN_EXE_cull-gmail, set by Cargo at compile time to the
let binary_path = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { // actual binary path. Works correctly in workspaces and with tools
// Running under cargo test - try release first, then debug // that override the target directory (e.g. cargo llvm-cov).
let release_binary = PathBuf::from(&manifest_dir) let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_cull-gmail"));
.join("target")
.join("release")
.join("cull-gmail");
if release_binary.exists() {
release_binary
} else {
PathBuf::from(&manifest_dir)
.join("target")
.join("debug")
.join("cull-gmail")
}
} else if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
// CI environments may set CARGO_TARGET_DIR
let release_binary = PathBuf::from(&target_dir)
.join("release")
.join("cull-gmail");
if release_binary.exists() {
release_binary
} else {
PathBuf::from(&target_dir).join("debug").join("cull-gmail")
}
} else {
// Fallback for other scenarios
std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("cull-gmail")
};
// Validate that the binary exists // Validate that the binary exists
if !binary_path.exists() { if !binary_path.exists() {

26
deny.toml Normal file
View File

@@ -0,0 +1,26 @@
# https://embarkstudios.github.io/cargo-deny/
[advisories]
ignore = [
# google-apis-common 8.0.0: project unmaintained (RUSTSEC-2025-0066).
# Core transitive dependency of google-gmail1 which is the only available
# Rust client for the Gmail API. No alternative available upstream.
{ id = "RUSTSEC-2025-0066", reason = "transitive via google-gmail1; no maintained alternative for Gmail API access" },
]
[licenses]
allow = [
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"MIT",
"Unicode-3.0",
]
[bans]
multiple-versions = "allow"
[sources]
unknown-registry = "deny"
unknown-git = "deny"

View File

@@ -1,14 +1,5 @@
pre-release-replacements = [
{ file = "docs/lib.md", search = """cull-gmail = "\\d+.\\d+.\\d+"""", replace = "{{crate_name}} = \"{{version}}\"", exactly = 1 },
{ file = "PRLOG.md", search = "## \\[Unreleased\\]", replace = "## [{{version}}] - {{date}}", exactly = 1 },
{ file = "PRLOG.md", search = "\\[Unreleased\\]:", replace = "[{{version}}]:", exactly = 1 },
{ file = "PRLOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", exactly = 1 },
]
pre-release-commit-message = "chore: Release {{crate_name}} v{{version}}"
tag-message = "{{tag_name}}"
tag-name = "{{prefix}}v{{version}}"
sign-tag = true sign-tag = true
sign-commit = true sign-commit = true
consolidate-commits = false consolidate-commits = true
allow-branch = ["main"] allow-branch = ["main"]
pre-release-hook = ["./release-hook.sh"] pre-release-replacements = []