Skip to content

Commit 22b9031

Browse files
konardclaude
andcommitted
Apply Rust CI/CD best practices from rust-ai-driven-development-pipeline-template
- Add pre-commit hook configuration for Rust code quality checks - Add CONTRIBUTING.md with development setup and guidelines - Add file size check (1000 lines max) to Rust lint job - Add crates.io publishing support to auto and manual release workflows - Add changelog-pr mode for manual releases via workflow_dispatch - Add CARGO_TOKEN environment variable for crates.io publishing Fixes #9 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 54639a0 commit 22b9031

File tree

3 files changed

+538
-5
lines changed

3 files changed

+538
-5
lines changed

.github/workflows/ci.yml

Lines changed: 236 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ on:
1717
- js
1818
- rust
1919
- both
20+
release_mode:
21+
description: 'Manual release mode'
22+
required: true
23+
type: choice
24+
default: 'instant'
25+
options:
26+
- instant
27+
- changelog-pr
2028
bump_type:
2129
description: 'Version bump type'
2230
required: true
@@ -37,6 +45,7 @@ concurrency:
3745
env:
3846
CARGO_TERM_COLOR: always
3947
RUSTFLAGS: -Dwarnings
48+
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
4049

4150
jobs:
4251
# === DETECT CHANGES ===
@@ -268,6 +277,10 @@ jobs:
268277
working-directory: rust
269278
run: cargo clippy --all-targets --all-features
270279

280+
- name: Check file size limit
281+
working-directory: rust
282+
run: node ../scripts/rust/check-file-size.mjs
283+
271284
# === RUST TEST ===
272285
rust-test:
273286
name: Rust Test (${{ matrix.os }})
@@ -459,17 +472,63 @@ jobs:
459472
working-directory: rust
460473
run: cargo build --release
461474

475+
- name: Publish to Crates.io
476+
if: steps.check.outputs.should_release == 'true'
477+
id: publish-crate
478+
working-directory: rust
479+
run: |
480+
PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/')
481+
PACKAGE_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
482+
echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION"
483+
484+
echo "=== Attempting to publish to crates.io ==="
485+
486+
# Try to publish and capture the result
487+
set +e # Don't exit on error
488+
cargo publish --token ${{ secrets.CARGO_TOKEN }} --allow-dirty 2>&1 | tee publish_output.txt
489+
PUBLISH_EXIT_CODE=$?
490+
set -e # Re-enable exit on error
491+
492+
if [ $PUBLISH_EXIT_CODE -eq 0 ]; then
493+
echo "Successfully published $PACKAGE_NAME@$PACKAGE_VERSION to crates.io"
494+
echo "publish_result=success" >> $GITHUB_OUTPUT
495+
elif grep -q "already uploaded" publish_output.txt || grep -q "already exists" publish_output.txt; then
496+
echo "Version $PACKAGE_VERSION already exists on crates.io - this is OK"
497+
echo "publish_result=already_exists" >> $GITHUB_OUTPUT
498+
else
499+
echo "Failed to publish for unknown reason"
500+
cat publish_output.txt
501+
echo "publish_result=failed" >> $GITHUB_OUTPUT
502+
exit 1
503+
fi
504+
505+
- name: Report crates.io publish status
506+
if: steps.check.outputs.should_release == 'true'
507+
run: |
508+
if [ "${{ steps.publish-crate.outputs.publish_result }}" = "success" ]; then
509+
echo "Package was successfully published to crates.io"
510+
elif [ "${{ steps.publish-crate.outputs.publish_result }}" = "already_exists" ]; then
511+
echo "Package version already exists on crates.io - no action needed"
512+
else
513+
echo "Publishing to crates.io failed - please check the logs"
514+
fi
515+
462516
- name: Create GitHub Release
463517
if: steps.check.outputs.should_release == 'true'
464518
env:
465519
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
466520
working-directory: rust
467-
run: node ../scripts/rust/create-github-release.mjs --release-version "${{ steps.current_version.outputs.version }}" --repository "${{ github.repository }}"
521+
run: |
522+
PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/')
523+
node ../scripts/rust/create-github-release.mjs \
524+
--release-version "${{ steps.current_version.outputs.version }}" \
525+
--repository "${{ github.repository }}" \
526+
--crates-io-url "https://crates.io/crates/$PACKAGE_NAME"
468527
469-
# === MANUAL RELEASE ===
528+
# === MANUAL INSTANT RELEASE ===
470529
manual-release:
471-
name: Manual Release (${{ github.event.inputs.release_target }})
472-
if: github.event_name == 'workflow_dispatch'
530+
name: Manual Instant Release (${{ github.event.inputs.release_target }})
531+
if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant'
473532
runs-on: ubuntu-latest
474533
permissions:
475534
contents: write
@@ -522,6 +581,19 @@ jobs:
522581
run: node ../scripts/js/create-github-release.mjs --release-version "${{ steps.js_publish.outputs.published_version }}" --repository "${{ github.repository }}"
523582

524583
# Rust Manual Release
584+
- name: Rust Collect changelog fragments
585+
if: github.event.inputs.release_target == 'rust' || github.event.inputs.release_target == 'both'
586+
working-directory: rust
587+
run: |
588+
# Check if there are any fragments to collect
589+
FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" 2>/dev/null | wc -l)
590+
if [ "$FRAGMENTS" -gt 0 ]; then
591+
echo "Found $FRAGMENTS changelog fragment(s), collecting..."
592+
node ../scripts/rust/collect-changelog.mjs
593+
else
594+
echo "No changelog fragments found, skipping collection"
595+
fi
596+
525597
- name: Rust Version and commit
526598
if: github.event.inputs.release_target == 'rust' || github.event.inputs.release_target == 'both'
527599
id: rust_version
@@ -533,9 +605,168 @@ jobs:
533605
working-directory: rust
534606
run: cargo build --release
535607

608+
- name: Rust Publish to Crates.io
609+
if: (github.event.inputs.release_target == 'rust' || github.event.inputs.release_target == 'both') && (steps.rust_version.outputs.version_committed == 'true' || steps.rust_version.outputs.already_released == 'true')
610+
id: rust_publish_crate
611+
working-directory: rust
612+
run: |
613+
PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/')
614+
PACKAGE_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
615+
echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION"
616+
617+
echo "=== Attempting to publish to crates.io ==="
618+
619+
# Try to publish and capture the result
620+
set +e # Don't exit on error
621+
cargo publish --token ${{ secrets.CARGO_TOKEN }} --allow-dirty 2>&1 | tee publish_output.txt
622+
PUBLISH_EXIT_CODE=$?
623+
set -e # Re-enable exit on error
624+
625+
if [ $PUBLISH_EXIT_CODE -eq 0 ]; then
626+
echo "Successfully published $PACKAGE_NAME@$PACKAGE_VERSION to crates.io"
627+
echo "publish_result=success" >> $GITHUB_OUTPUT
628+
elif grep -q "already uploaded" publish_output.txt || grep -q "already exists" publish_output.txt; then
629+
echo "Version $PACKAGE_VERSION already exists on crates.io - this is OK"
630+
echo "publish_result=already_exists" >> $GITHUB_OUTPUT
631+
else
632+
echo "Failed to publish for unknown reason"
633+
cat publish_output.txt
634+
echo "publish_result=failed" >> $GITHUB_OUTPUT
635+
exit 1
636+
fi
637+
638+
- name: Rust Report crates.io publish status
639+
if: (github.event.inputs.release_target == 'rust' || github.event.inputs.release_target == 'both') && (steps.rust_version.outputs.version_committed == 'true' || steps.rust_version.outputs.already_released == 'true')
640+
run: |
641+
if [ "${{ steps.rust_publish_crate.outputs.publish_result }}" = "success" ]; then
642+
echo "Package was successfully published to crates.io"
643+
elif [ "${{ steps.rust_publish_crate.outputs.publish_result }}" = "already_exists" ]; then
644+
echo "Package version already exists on crates.io - no action needed"
645+
else
646+
echo "Publishing to crates.io failed - please check the logs"
647+
fi
648+
536649
- name: Rust Create GitHub Release
537650
if: (github.event.inputs.release_target == 'rust' || github.event.inputs.release_target == 'both') && (steps.rust_version.outputs.version_committed == 'true' || steps.rust_version.outputs.already_released == 'true')
538651
env:
539652
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
540653
working-directory: rust
541-
run: node ../scripts/rust/create-github-release.mjs --release-version "${{ steps.rust_version.outputs.new_version }}" --repository "${{ github.repository }}"
654+
run: |
655+
PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/')
656+
node ../scripts/rust/create-github-release.mjs \
657+
--release-version "${{ steps.rust_version.outputs.new_version }}" \
658+
--repository "${{ github.repository }}" \
659+
--crates-io-url "https://crates.io/crates/$PACKAGE_NAME"
660+
661+
# === MANUAL CHANGELOG PR ===
662+
changelog-pr:
663+
name: Create Changelog PR (${{ github.event.inputs.release_target }})
664+
if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changelog-pr'
665+
runs-on: ubuntu-latest
666+
permissions:
667+
contents: write
668+
pull-requests: write
669+
steps:
670+
- uses: actions/checkout@v4
671+
with:
672+
fetch-depth: 0
673+
674+
- name: Setup Node.js
675+
uses: actions/setup-node@v4
676+
with:
677+
node-version: '20.x'
678+
679+
- name: Create JS changelog fragment
680+
if: github.event.inputs.release_target == 'js' || github.event.inputs.release_target == 'both'
681+
working-directory: js
682+
run: |
683+
BUMP_TYPE="${{ github.event.inputs.bump_type }}"
684+
DESCRIPTION="${{ github.event.inputs.description }}"
685+
TIMESTAMP=$(date +%Y%m%d%H%M%S)
686+
FRAGMENT_FILE=".changeset/${TIMESTAMP}-manual-${BUMP_TYPE}.md"
687+
688+
# Create changeset file
689+
mkdir -p .changeset
690+
cat > "$FRAGMENT_FILE" << EOF
691+
---
692+
"agent-commander": $BUMP_TYPE
693+
---
694+
695+
EOF
696+
697+
if [ -n "$DESCRIPTION" ]; then
698+
echo "${DESCRIPTION}" >> "$FRAGMENT_FILE"
699+
else
700+
echo "Manual ${BUMP_TYPE} release" >> "$FRAGMENT_FILE"
701+
fi
702+
703+
echo "Created changeset fragment: $FRAGMENT_FILE"
704+
cat "$FRAGMENT_FILE"
705+
706+
- name: Create Rust changelog fragment
707+
if: github.event.inputs.release_target == 'rust' || github.event.inputs.release_target == 'both'
708+
working-directory: rust
709+
run: |
710+
BUMP_TYPE="${{ github.event.inputs.bump_type }}"
711+
DESCRIPTION="${{ github.event.inputs.description }}"
712+
TIMESTAMP=$(date +%Y%m%d%H%M%S)
713+
FRAGMENT_FILE="changelog.d/${TIMESTAMP}-manual-${BUMP_TYPE}.md"
714+
715+
# Determine changelog category based on bump type
716+
case "$BUMP_TYPE" in
717+
major)
718+
CATEGORY="### Breaking Changes"
719+
;;
720+
minor)
721+
CATEGORY="### Added"
722+
;;
723+
patch)
724+
CATEGORY="### Fixed"
725+
;;
726+
esac
727+
728+
# Create changelog fragment with frontmatter
729+
mkdir -p changelog.d
730+
cat > "$FRAGMENT_FILE" << EOF
731+
---
732+
bump: $BUMP_TYPE
733+
---
734+
735+
$CATEGORY
736+
737+
EOF
738+
739+
if [ -n "$DESCRIPTION" ]; then
740+
echo "- ${DESCRIPTION}" >> "$FRAGMENT_FILE"
741+
else
742+
echo "- Manual ${BUMP_TYPE} release" >> "$FRAGMENT_FILE"
743+
fi
744+
745+
echo "Created changelog fragment: $FRAGMENT_FILE"
746+
cat "$FRAGMENT_FILE"
747+
748+
- name: Create Pull Request
749+
uses: peter-evans/create-pull-request@v7
750+
with:
751+
token: ${{ secrets.GITHUB_TOKEN }}
752+
commit-message: 'chore: add changelog for manual ${{ github.event.inputs.bump_type }} release (${{ github.event.inputs.release_target }})'
753+
branch: changelog-manual-release-${{ github.run_id }}
754+
delete-branch: true
755+
title: 'chore: manual ${{ github.event.inputs.bump_type }} release (${{ github.event.inputs.release_target }})'
756+
body: |
757+
## Manual Release Request
758+
759+
This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release.
760+
761+
### Release Details
762+
- **Target:** ${{ github.event.inputs.release_target }}
763+
- **Type:** ${{ github.event.inputs.bump_type }}
764+
- **Description:** ${{ github.event.inputs.description || 'Manual release' }}
765+
- **Triggered by:** @${{ github.actor }}
766+
767+
### Next Steps
768+
1. Review the changelog fragment(s) in this PR
769+
2. Merge this PR to main
770+
3. The automated release workflow will:
771+
- For JS: Publish to npm and create a GitHub release
772+
- For Rust: Publish to crates.io and create a GitHub release

.pre-commit-config.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v5.0.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-added-large-files
9+
- id: check-merge-conflict
10+
- id: check-toml
11+
- id: debug-statements
12+
13+
- repo: local
14+
hooks:
15+
- id: cargo-fmt
16+
name: cargo fmt
17+
entry: cargo fmt --manifest-path rust/Cargo.toml --all --
18+
language: system
19+
types: [rust]
20+
pass_filenames: false
21+
22+
- id: cargo-clippy
23+
name: cargo clippy
24+
entry: cargo clippy --manifest-path rust/Cargo.toml --all-targets --all-features -- -D warnings
25+
language: system
26+
types: [rust]
27+
pass_filenames: false
28+
29+
- id: cargo-test
30+
name: cargo test
31+
entry: cargo test --manifest-path rust/Cargo.toml
32+
language: system
33+
types: [rust]
34+
pass_filenames: false

0 commit comments

Comments
 (0)