Implement network isomorphism solver using Links Theory #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| workflow_dispatch: | |
| inputs: | |
| release_mode: | |
| description: 'Manual release mode' | |
| required: true | |
| type: choice | |
| default: 'instant' | |
| options: | |
| - instant | |
| - changelog-pr | |
| bump_type: | |
| description: 'Version bump type' | |
| required: true | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| description: | |
| description: 'Release description (optional)' | |
| required: false | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUSTFLAGS: -Dwarnings | |
| CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} | |
| jobs: | |
| # === DETECT CHANGES - determines which jobs should run === | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'workflow_dispatch' | |
| outputs: | |
| rs-changed: ${{ steps.changes.outputs.rs-changed }} | |
| toml-changed: ${{ steps.changes.outputs.toml-changed }} | |
| mjs-changed: ${{ steps.changes.outputs.mjs-changed }} | |
| docs-changed: ${{ steps.changes.outputs.docs-changed }} | |
| workflow-changed: ${{ steps.changes.outputs.workflow-changed }} | |
| any-code-changed: ${{ steps.changes.outputs.any-code-changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20.x' | |
| - name: Detect changes | |
| id: changes | |
| env: | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: node scripts/detect-code-changes.mjs | |
| # === CHANGELOG CHECK - only runs on PRs with code changes === | |
| # Docs-only PRs (./docs folder, markdown files) don't require changelog fragments | |
| changelog: | |
| name: Changelog Fragment Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for changelog fragments | |
| run: | | |
| # Get list of fragment files (excluding README and template) | |
| FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" 2>/dev/null | wc -l) | |
| # Get changed files in PR | |
| CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) | |
| # Check if any source files changed (excluding docs and config) | |
| SOURCE_CHANGED=$(echo "$CHANGED_FILES" | grep -E "^(src/|tests/|scripts/|Cargo\.toml)" | wc -l) | |
| if [ "$SOURCE_CHANGED" -gt 0 ] && [ "$FRAGMENTS" -eq 0 ]; then | |
| echo "::error::No changelog fragment found. Please add a changelog entry in changelog.d/" | |
| echo "" | |
| echo "To create a changelog fragment:" | |
| echo " Create a new .md file in changelog.d/ with your changes" | |
| echo "" | |
| echo "See changelog.d/README.md for more information." | |
| exit 1 | |
| fi | |
| echo "Changelog check passed" | |
| # === LINT AND FORMAT CHECK === | |
| # Lint runs independently of changelog check - it's a fast check that should always run | |
| # See: https://github.com/link-assistant/hive-mind/pull/1024 for why this dependency was removed | |
| lint: | |
| name: Lint and Format Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| # Note: always() is required because detect-changes is skipped on workflow_dispatch, | |
| # and without always(), this job would also be skipped even though its condition includes workflow_dispatch. | |
| # See: https://github.com/actions/runner/issues/491 | |
| if: | | |
| always() && !cancelled() && ( | |
| github.event_name == 'push' || | |
| github.event_name == 'workflow_dispatch' || | |
| needs.detect-changes.outputs.rs-changed == 'true' || | |
| needs.detect-changes.outputs.toml-changed == 'true' || | |
| needs.detect-changes.outputs.mjs-changed == 'true' || | |
| needs.detect-changes.outputs.docs-changed == 'true' || | |
| needs.detect-changes.outputs.workflow-changed == 'true' | |
| ) | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20.x' | |
| - name: Cache cargo registry | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo- | |
| - name: Check formatting | |
| run: cargo fmt --all -- --check | |
| - name: Run Clippy | |
| run: cargo clippy --all-targets --all-features | |
| - name: Check file size limit | |
| run: node scripts/check-file-size.mjs | |
| # === TEST === | |
| # Test runs independently of changelog check | |
| test: | |
| name: Test (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: [detect-changes, changelog] | |
| # Run if: push event, OR changelog succeeded, OR changelog was skipped (docs-only PR) | |
| if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo registry | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo- | |
| - name: Run tests | |
| run: cargo test --all-features --verbose | |
| - name: Run doc tests | |
| run: cargo test --doc --verbose | |
| # === BUILD === | |
| # Build package - only runs if lint and test pass | |
| build: | |
| name: Build Package | |
| runs-on: ubuntu-latest | |
| needs: [lint, test] | |
| if: always() && !cancelled() && needs.lint.result == 'success' && needs.test.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo registry | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-build- | |
| - name: Build release | |
| run: cargo build --release --verbose | |
| - name: Check package | |
| run: cargo package --list | |
| # === AUTO RELEASE === | |
| # Automatic release on push to main using changelog fragments | |
| # This job automatically bumps version based on fragments in changelog.d/ | |
| auto-release: | |
| name: Auto Release | |
| needs: [lint, test, build] | |
| # Note: always() ensures consistent behavior with other jobs that depend on jobs using always(). | |
| if: | | |
| always() && !cancelled() && | |
| github.event_name == 'push' && | |
| github.ref == 'refs/heads/main' && | |
| needs.build.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20.x' | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Determine bump type from changelog fragments | |
| id: bump_type | |
| run: node scripts/get-bump-type.mjs | |
| - name: Check if version already released or no fragments | |
| id: check | |
| run: | | |
| # Check if there are changelog fragments | |
| if [ "${{ steps.bump_type.outputs.has_fragments }}" != "true" ]; then | |
| # No fragments - check if current version tag exists | |
| CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) | |
| if git rev-parse "v$CURRENT_VERSION" >/dev/null 2>&1; then | |
| echo "No changelog fragments and v$CURRENT_VERSION already released" | |
| echo "should_release=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "No changelog fragments but v$CURRENT_VERSION not yet released" | |
| echo "should_release=true" >> $GITHUB_OUTPUT | |
| echo "skip_bump=true" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| echo "Found changelog fragments, proceeding with release" | |
| echo "should_release=true" >> $GITHUB_OUTPUT | |
| echo "skip_bump=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Collect changelog and bump version | |
| id: version | |
| if: steps.check.outputs.should_release == 'true' && steps.check.outputs.skip_bump != 'true' | |
| run: | | |
| node scripts/version-and-commit.mjs \ | |
| --bump-type "${{ steps.bump_type.outputs.bump_type }}" | |
| - name: Get current version | |
| id: current_version | |
| if: steps.check.outputs.should_release == 'true' | |
| run: | | |
| CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) | |
| echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| - name: Build release | |
| if: steps.check.outputs.should_release == 'true' | |
| run: cargo build --release | |
| - name: Publish to Crates.io | |
| if: steps.check.outputs.should_release == 'true' | |
| id: publish-crate | |
| run: | | |
| PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/') | |
| PACKAGE_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" | |
| echo "=== Attempting to publish to crates.io ===" | |
| # Try to publish and capture the result | |
| set +e # Don't exit on error | |
| cargo publish --token ${{ secrets.CARGO_TOKEN }} --allow-dirty 2>&1 | tee publish_output.txt | |
| PUBLISH_EXIT_CODE=$? | |
| set -e # Re-enable exit on error | |
| if [ $PUBLISH_EXIT_CODE -eq 0 ]; then | |
| echo "Successfully published $PACKAGE_NAME@$PACKAGE_VERSION to crates.io" | |
| echo "publish_result=success" >> $GITHUB_OUTPUT | |
| elif grep -q "already uploaded" publish_output.txt || grep -q "already exists" publish_output.txt; then | |
| echo "Version $PACKAGE_VERSION already exists on crates.io - this is OK" | |
| echo "publish_result=already_exists" >> $GITHUB_OUTPUT | |
| else | |
| echo "Failed to publish for unknown reason" | |
| cat publish_output.txt | |
| echo "publish_result=failed" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Report crates.io publish status | |
| if: steps.check.outputs.should_release == 'true' | |
| run: | | |
| if [ "${{ steps.publish-crate.outputs.publish_result }}" = "success" ]; then | |
| echo "Package was successfully published to crates.io" | |
| elif [ "${{ steps.publish-crate.outputs.publish_result }}" = "already_exists" ]; then | |
| echo "Package version already exists on crates.io - no action needed" | |
| else | |
| echo "Publishing to crates.io failed - please check the logs" | |
| fi | |
| - name: Create GitHub Release | |
| if: steps.check.outputs.should_release == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/') | |
| node scripts/create-github-release.mjs \ | |
| --release-version "${{ steps.current_version.outputs.version }}" \ | |
| --repository "${{ github.repository }}" \ | |
| --crates-io-url "https://crates.io/crates/$PACKAGE_NAME" | |
| # === MANUAL INSTANT RELEASE === | |
| # Manual release via workflow_dispatch - only after CI passes | |
| manual-release: | |
| name: Instant Release | |
| needs: [lint, test, build] | |
| # Note: always() is required to evaluate the condition when dependencies use always(). | |
| # The build job ensures lint and test passed before this job runs. | |
| if: | | |
| always() && !cancelled() && | |
| github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.release_mode == 'instant' && | |
| needs.build.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20.x' | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Collect changelog fragments | |
| run: | | |
| # Check if there are any fragments to collect | |
| FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" 2>/dev/null | wc -l) | |
| if [ "$FRAGMENTS" -gt 0 ]; then | |
| echo "Found $FRAGMENTS changelog fragment(s), collecting..." | |
| node scripts/collect-changelog.mjs | |
| else | |
| echo "No changelog fragments found, skipping collection" | |
| fi | |
| - name: Version and commit | |
| id: version | |
| run: | | |
| node scripts/version-and-commit.mjs \ | |
| --bump-type "${{ github.event.inputs.bump_type }}" \ | |
| --description "${{ github.event.inputs.description }}" | |
| - name: Build release | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| run: cargo build --release | |
| - name: Publish to Crates.io | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| id: publish-crate | |
| run: | | |
| PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/') | |
| PACKAGE_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" | |
| echo "=== Attempting to publish to crates.io ===" | |
| # Try to publish and capture the result | |
| set +e # Don't exit on error | |
| cargo publish --token ${{ secrets.CARGO_TOKEN }} --allow-dirty 2>&1 | tee publish_output.txt | |
| PUBLISH_EXIT_CODE=$? | |
| set -e # Re-enable exit on error | |
| if [ $PUBLISH_EXIT_CODE -eq 0 ]; then | |
| echo "Successfully published $PACKAGE_NAME@$PACKAGE_VERSION to crates.io" | |
| echo "publish_result=success" >> $GITHUB_OUTPUT | |
| elif grep -q "already uploaded" publish_output.txt || grep -q "already exists" publish_output.txt; then | |
| echo "Version $PACKAGE_VERSION already exists on crates.io - this is OK" | |
| echo "publish_result=already_exists" >> $GITHUB_OUTPUT | |
| else | |
| echo "Failed to publish for unknown reason" | |
| cat publish_output.txt | |
| echo "publish_result=failed" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Report crates.io publish status | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| run: | | |
| if [ "${{ steps.publish-crate.outputs.publish_result }}" = "success" ]; then | |
| echo "Package was successfully published to crates.io" | |
| elif [ "${{ steps.publish-crate.outputs.publish_result }}" = "already_exists" ]; then | |
| echo "Package version already exists on crates.io - no action needed" | |
| else | |
| echo "Publishing to crates.io failed - please check the logs" | |
| fi | |
| - name: Create GitHub Release | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/') | |
| node scripts/create-github-release.mjs \ | |
| --release-version "${{ steps.version.outputs.new_version }}" \ | |
| --repository "${{ github.repository }}" \ | |
| --crates-io-url "https://crates.io/crates/$PACKAGE_NAME" | |
| # === MANUAL CHANGELOG PR === | |
| changelog-pr: | |
| name: Create Changelog PR | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changelog-pr' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20.x' | |
| - name: Create changelog fragment | |
| run: | | |
| BUMP_TYPE="${{ github.event.inputs.bump_type }}" | |
| DESCRIPTION="${{ github.event.inputs.description }}" | |
| TIMESTAMP=$(date +%Y%m%d%H%M%S) | |
| FRAGMENT_FILE="changelog.d/${TIMESTAMP}-manual-${BUMP_TYPE}.md" | |
| # Determine changelog category based on bump type | |
| case "$BUMP_TYPE" in | |
| major) | |
| CATEGORY="### Breaking Changes" | |
| ;; | |
| minor) | |
| CATEGORY="### Added" | |
| ;; | |
| patch) | |
| CATEGORY="### Fixed" | |
| ;; | |
| esac | |
| # Create changelog fragment with frontmatter | |
| mkdir -p changelog.d | |
| cat > "$FRAGMENT_FILE" << EOF | |
| --- | |
| bump: $BUMP_TYPE | |
| --- | |
| $CATEGORY | |
| EOF | |
| if [ -n "$DESCRIPTION" ]; then | |
| echo "- ${DESCRIPTION}" >> "$FRAGMENT_FILE" | |
| else | |
| echo "- Manual ${BUMP_TYPE} release" >> "$FRAGMENT_FILE" | |
| fi | |
| echo "Created changelog fragment: $FRAGMENT_FILE" | |
| cat "$FRAGMENT_FILE" | |
| - name: Create Pull Request | |
| uses: peter-evans/create-pull-request@v7 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| commit-message: 'chore: add changelog for manual ${{ github.event.inputs.bump_type }} release' | |
| branch: changelog-manual-release-${{ github.run_id }} | |
| delete-branch: true | |
| title: 'chore: manual ${{ github.event.inputs.bump_type }} release' | |
| body: | | |
| ## Manual Release Request | |
| This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release. | |
| ### Release Details | |
| - **Type:** ${{ github.event.inputs.bump_type }} | |
| - **Description:** ${{ github.event.inputs.description || 'Manual release' }} | |
| - **Triggered by:** @${{ github.actor }} | |
| ### Next Steps | |
| 1. Review the changelog fragment in this PR | |
| 2. Merge this PR to main | |
| 3. The automated release workflow will publish to crates.io and create a GitHub release |