Skip to content

Commit 42e3f13

Browse files
corvid-agentclaude
andcommitted
feat: add CI/CD workflows and release binary publishing
- Add CI workflow (test, clippy, fmt on ubuntu/macos/windows) - Add release workflow (cross-compile binaries on tag push for linux/macos/windows x86_64 + linux/macos aarch64) - Fix all clippy warnings (collapsible ifs, unnecessary filter_map, manual prefix strip) - Run cargo fmt across all source files - Update README with install instructions (pre-built binaries, cargo install from git) - Fix repository URL in Cargo.toml Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 410b98d commit 42e3f13

19 files changed

Lines changed: 311 additions & 161 deletions

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
test:
14+
runs-on: ${{ matrix.os }}
15+
strategy:
16+
matrix:
17+
os: [ubuntu-latest, macos-latest, windows-latest]
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: dtolnay/rust-toolchain@stable
21+
- uses: Swatinem/rust-cache@v2
22+
- run: cargo build --verbose
23+
- run: cargo test --verbose
24+
- run: cargo clippy -- -D warnings
25+
26+
fmt:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
- uses: dtolnay/rust-toolchain@stable
31+
with:
32+
components: rustfmt
33+
- run: cargo fmt --check

.github/workflows/release.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
build:
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
matrix:
16+
include:
17+
- os: ubuntu-latest
18+
target: x86_64-unknown-linux-gnu
19+
artifact: specsync-linux-x86_64
20+
- os: ubuntu-latest
21+
target: aarch64-unknown-linux-gnu
22+
artifact: specsync-linux-aarch64
23+
- os: macos-latest
24+
target: x86_64-apple-darwin
25+
artifact: specsync-macos-x86_64
26+
- os: macos-latest
27+
target: aarch64-apple-darwin
28+
artifact: specsync-macos-aarch64
29+
- os: windows-latest
30+
target: x86_64-pc-windows-msvc
31+
artifact: specsync-windows-x86_64.exe
32+
steps:
33+
- uses: actions/checkout@v4
34+
35+
- uses: dtolnay/rust-toolchain@stable
36+
with:
37+
targets: ${{ matrix.target }}
38+
39+
- name: Install cross-compilation tools (Linux aarch64)
40+
if: matrix.target == 'aarch64-unknown-linux-gnu'
41+
run: |
42+
sudo apt-get update
43+
sudo apt-get install -y gcc-aarch64-linux-gnu
44+
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
45+
46+
- uses: Swatinem/rust-cache@v2
47+
48+
- name: Build
49+
run: cargo build --release --target ${{ matrix.target }}
50+
51+
- name: Package (Unix)
52+
if: runner.os != 'Windows'
53+
run: |
54+
cp target/${{ matrix.target }}/release/specsync ${{ matrix.artifact }}
55+
chmod +x ${{ matrix.artifact }}
56+
tar czf ${{ matrix.artifact }}.tar.gz ${{ matrix.artifact }}
57+
58+
- name: Package (Windows)
59+
if: runner.os == 'Windows'
60+
run: |
61+
cp target/${{ matrix.target }}/release/specsync.exe ${{ matrix.artifact }}
62+
Compress-Archive -Path ${{ matrix.artifact }} -DestinationPath ${{ matrix.artifact }}.zip
63+
64+
- name: Upload artifact
65+
uses: actions/upload-artifact@v4
66+
with:
67+
name: ${{ matrix.artifact }}
68+
path: |
69+
${{ matrix.artifact }}.tar.gz
70+
${{ matrix.artifact }}.zip
71+
72+
release:
73+
needs: build
74+
runs-on: ubuntu-latest
75+
steps:
76+
- uses: actions/checkout@v4
77+
78+
- name: Download all artifacts
79+
uses: actions/download-artifact@v4
80+
with:
81+
path: artifacts
82+
83+
- name: Create release
84+
uses: softprops/action-gh-release@v2
85+
with:
86+
generate_release_notes: true
87+
files: |
88+
artifacts/**/*.tar.gz
89+
artifacts/**/*.zip

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "1.0.0"
44
edition = "2024"
55
description = "Bidirectional spec-to-code validation — language-agnostic, blazing fast"
66
license = "MIT"
7-
repository = "https://github.com/corvid-agent/specsync"
7+
repository = "https://github.com/CorvidLabs/spec-sync"
88
keywords = ["spec", "documentation", "validation", "coverage"]
99
categories = ["development-tools", "command-line-utilities"]
1010

README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,39 @@ Language is auto-detected from file extensions. The same spec format works for a
3232

3333
## Install
3434

35+
### Pre-built binaries (recommended)
36+
37+
Download the latest binary from [GitHub Releases](https://github.com/CorvidLabs/spec-sync/releases):
38+
3539
```bash
36-
# From source
37-
cargo install --path .
40+
# macOS (Apple Silicon)
41+
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-macos-aarch64.tar.gz | tar xz
42+
sudo mv specsync-macos-aarch64 /usr/local/bin/specsync
43+
44+
# macOS (Intel)
45+
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-macos-x86_64.tar.gz | tar xz
46+
sudo mv specsync-macos-x86_64 /usr/local/bin/specsync
47+
48+
# Linux (x86_64)
49+
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-linux-x86_64.tar.gz | tar xz
50+
sudo mv specsync-linux-x86_64 /usr/local/bin/specsync
3851

39-
# Or build release binary
40-
cargo build --release
41-
# Binary at: target/release/specsync
52+
# Linux (aarch64)
53+
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-linux-aarch64.tar.gz | tar xz
54+
sudo mv specsync-linux-aarch64 /usr/local/bin/specsync
55+
```
56+
57+
Windows: download `specsync-windows-x86_64.exe.zip` from the releases page.
58+
59+
### From source
60+
61+
```bash
62+
cargo install --git https://github.com/CorvidLabs/spec-sync
63+
64+
# Or clone and build
65+
git clone https://github.com/CorvidLabs/spec-sync.git
66+
cd spec-sync
67+
cargo install --path .
4268
```
4369

4470
## Quick start
@@ -187,7 +213,21 @@ Flags:
187213
## CI integration
188214

189215
```yaml
190-
# GitHub Actions
216+
# GitHub Actions — install from release binary
217+
- name: Install specsync
218+
run: |
219+
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-linux-x86_64.tar.gz | tar xz
220+
sudo mv specsync-linux-x86_64 /usr/local/bin/specsync
221+
222+
- name: Spec check
223+
run: specsync check --strict --require-coverage 100
224+
```
225+
226+
```yaml
227+
# Or install from source (slower but always up to date)
228+
- name: Install specsync
229+
run: cargo install --git https://github.com/CorvidLabs/spec-sync
230+
191231
- name: Spec check
192232
run: specsync check --strict --require-coverage 100
193233
```

src/exports/csharp.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use regex::Regex;
22
use std::sync::LazyLock;
33

4-
static COMMENT_SINGLE: LazyLock<Regex> =
5-
LazyLock::new(|| Regex::new(r"//.*$").unwrap());
4+
static COMMENT_SINGLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"//.*$").unwrap());
65

7-
static COMMENT_MULTI: LazyLock<Regex> =
8-
LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
6+
static COMMENT_MULTI: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
97

108
/// C# public/internal types: class, struct, interface, enum, record, delegate
119
static CS_TYPE: LazyLock<Regex> = LazyLock::new(|| {

src/exports/dart.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
use regex::Regex;
22
use std::sync::LazyLock;
33

4-
static COMMENT_SINGLE: LazyLock<Regex> =
5-
LazyLock::new(|| Regex::new(r"//.*$").unwrap());
4+
static COMMENT_SINGLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"//.*$").unwrap());
65

7-
static COMMENT_MULTI: LazyLock<Regex> =
8-
LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
6+
static COMMENT_MULTI: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
97

108
/// Dart top-level declarations: class, mixin, enum, extension, typedef
119
/// In Dart, anything NOT prefixed with _ is public.
1210
static DART_TYPE: LazyLock<Regex> = LazyLock::new(|| {
13-
Regex::new(
14-
r"(?m)^(?:abstract\s+)?(?:class|mixin|enum|extension|typedef)\s+([A-Z]\w*)",
15-
)
16-
.unwrap()
11+
Regex::new(r"(?m)^(?:abstract\s+)?(?:class|mixin|enum|extension|typedef)\s+([A-Z]\w*)").unwrap()
1712
});
1813

1914
/// Dart top-level functions and variables (public = no underscore prefix)

src/exports/go.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use regex::Regex;
22
use std::sync::LazyLock;
33

4-
static COMMENT_SINGLE: LazyLock<Regex> =
5-
LazyLock::new(|| Regex::new(r"//.*$").unwrap());
4+
static COMMENT_SINGLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"//.*$").unwrap());
65

7-
static COMMENT_MULTI: LazyLock<Regex> =
8-
LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
6+
static COMMENT_MULTI: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
97

108
/// Go exports: func Name, type Name, var Name, const Name
119
/// In Go, anything starting with uppercase is exported.
@@ -14,9 +12,8 @@ static GO_DECL: LazyLock<Regex> = LazyLock::new(|| {
1412
});
1513

1614
/// Go method: func (receiver) Name(...)
17-
static GO_METHOD: LazyLock<Regex> = LazyLock::new(|| {
18-
Regex::new(r"(?m)^func\s+\([^)]+\)\s+([A-Z]\w*)").unwrap()
19-
});
15+
static GO_METHOD: LazyLock<Regex> =
16+
LazyLock::new(|| Regex::new(r"(?m)^func\s+\([^)]+\)\s+([A-Z]\w*)").unwrap());
2017

2118
/// Extract exported symbols from Go source code.
2219
/// In Go, any top-level identifier starting with an uppercase letter is exported.

src/exports/java.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use regex::Regex;
22
use std::sync::LazyLock;
33

4-
static COMMENT_SINGLE: LazyLock<Regex> =
5-
LazyLock::new(|| Regex::new(r"//.*$").unwrap());
4+
static COMMENT_SINGLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"//.*$").unwrap());
65

7-
static COMMENT_MULTI: LazyLock<Regex> =
8-
LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
6+
static COMMENT_MULTI: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
97

108
/// Java public declarations: public class, interface, enum, record, @interface (annotation)
119
static JAVA_TYPE: LazyLock<Regex> = LazyLock::new(|| {

src/exports/kotlin.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use regex::Regex;
22
use std::sync::LazyLock;
33

4-
static COMMENT_SINGLE: LazyLock<Regex> =
5-
LazyLock::new(|| Regex::new(r"//.*$").unwrap());
4+
static COMMENT_SINGLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"//.*$").unwrap());
65

7-
static COMMENT_MULTI: LazyLock<Regex> =
8-
LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
6+
static COMMENT_MULTI: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
97

108
/// Kotlin top-level declarations (everything not marked private/internal/protected is public by default).
119
/// We match: fun, class, object, interface, typealias, val, var, enum class, data class, sealed class, annotation class
@@ -18,9 +16,8 @@ static KT_DECL: LazyLock<Regex> = LazyLock::new(|| {
1816
});
1917

2018
/// Detect visibility — private/internal/protected lines should be excluded
21-
static PRIVATE_LINE: LazyLock<Regex> = LazyLock::new(|| {
22-
Regex::new(r"(?m)^[^\S\n]*(?:private|internal|protected)\s+").unwrap()
23-
});
19+
static PRIVATE_LINE: LazyLock<Regex> =
20+
LazyLock::new(|| Regex::new(r"(?m)^[^\S\n]*(?:private|internal|protected)\s+").unwrap());
2421

2522
/// Extract exported symbols from Kotlin source code.
2623
/// In Kotlin, everything is public by default unless marked private/internal/protected.
@@ -37,15 +34,14 @@ pub fn extract_exports(content: &str) -> Vec<String> {
3734
continue;
3835
}
3936

40-
if let Some(caps) = KT_DECL.captures(line) {
41-
if let Some(name) = caps.get(1) {
37+
if let Some(caps) = KT_DECL.captures(line)
38+
&& let Some(name) = caps.get(1) {
4239
// Skip companion objects (they're not standalone exports)
4340
if trimmed.starts_with("companion") {
4441
continue;
4542
}
4643
symbols.push(name.as_str().to_string());
4744
}
48-
}
4945
}
5046

5147
symbols

0 commit comments

Comments
 (0)