Skip to content

Commit 3e2266f

Browse files
committed
ci: Rust release pipeline과 vendor staging 추가
1 parent eb39dc3 commit 3e2266f

14 files changed

Lines changed: 370 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,15 @@ jobs:
5151

5252
- name: Setup Rust toolchain
5353
run: |
54-
rustup toolchain install 1.95.0 --profile minimal
54+
rustup toolchain install 1.95.0 --profile minimal --component clippy --component rustfmt
5555
rustup default 1.95.0
5656
57+
- name: Check Rust formatting
58+
run: cargo fmt --check
59+
60+
- name: Lint Rust workspace
61+
run: cargo clippy --workspace --all-targets -- -D warnings
62+
5763
- name: Run Rust workspace tests
5864
run: cargo test --workspace
5965

.github/workflows/release.yml

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ permissions:
1010
id-token: write
1111

1212
jobs:
13-
publish:
13+
prepare-release:
1414
if: github.repository == 'JeremyDev87/legolas'
1515
runs-on: ubuntu-latest
1616
timeout-minutes: 15
17+
outputs:
18+
package_name: ${{ steps.meta.outputs.name }}
19+
package_version: ${{ steps.meta.outputs.version }}
20+
tag_name: ${{ steps.meta.outputs.tag_name }}
1721

1822
steps:
1923
- name: Checkout code
@@ -32,6 +36,7 @@ jobs:
3236
run: |
3337
echo "name=$(node -p 'require(\"./package.json\").name')" >> "$GITHUB_OUTPUT"
3438
echo "version=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
39+
echo "tag_name=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
3540
3641
- name: Verify release tag matches package version
3742
run: |
@@ -73,6 +78,90 @@ jobs:
7378
GITHUB_REF: ${{ github.ref }}
7479
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7580

81+
rust-release-binaries:
82+
if: github.repository == 'JeremyDev87/legolas'
83+
needs: prepare-release
84+
runs-on: ${{ matrix.runs-on }}
85+
timeout-minutes: 20
86+
strategy:
87+
fail-fast: false
88+
matrix:
89+
include:
90+
- runs-on: ubuntu-latest
91+
target: x86_64-unknown-linux-gnu
92+
binary_name: legolas
93+
asset_name: legolas-x86_64-unknown-linux-gnu
94+
- runs-on: windows-latest
95+
target: x86_64-pc-windows-msvc
96+
binary_name: legolas.exe
97+
asset_name: legolas-x86_64-pc-windows-msvc.exe
98+
- runs-on: macos-13
99+
target: x86_64-apple-darwin
100+
binary_name: legolas
101+
asset_name: legolas-x86_64-apple-darwin
102+
- runs-on: macos-14
103+
target: aarch64-apple-darwin
104+
binary_name: legolas
105+
asset_name: legolas-aarch64-apple-darwin
106+
107+
steps:
108+
- name: Checkout code
109+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
110+
111+
- name: Setup Node.js
112+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
113+
with:
114+
node-version: "22"
115+
116+
- name: Setup Rust toolchain
117+
run: |
118+
rustup toolchain install 1.95.0 --profile minimal
119+
rustup default 1.95.0
120+
121+
- name: Build Rust release binary
122+
run: cargo build --release -p legolas-cli
123+
124+
- name: Stage vendor layout
125+
run: node ./scripts/stage-local-vendor-binary.mjs
126+
127+
- name: Verify vendor layout
128+
run: node ./scripts/verify-vendor-layout.mjs ${{ matrix.target }}
129+
130+
- name: Copy release asset
131+
shell: bash
132+
run: |
133+
mkdir -p release-assets
134+
cp "vendor/${{ matrix.target }}/${{ matrix.binary_name }}" "release-assets/${{ matrix.asset_name }}"
135+
136+
- name: Upload binary asset to release draft
137+
run: gh release upload "${{ needs.prepare-release.outputs.tag_name }}" "release-assets/${{ matrix.asset_name }}" --clobber
138+
env:
139+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
140+
141+
publish:
142+
if: github.repository == 'JeremyDev87/legolas'
143+
needs: [prepare-release, rust-release-binaries]
144+
runs-on: ubuntu-latest
145+
timeout-minutes: 15
146+
147+
steps:
148+
- name: Checkout code
149+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
150+
with:
151+
fetch-depth: 0
152+
153+
- name: Setup Node.js
154+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
155+
with:
156+
node-version: "22"
157+
registry-url: "https://registry.npmjs.org"
158+
159+
- name: Read package metadata
160+
id: meta
161+
run: |
162+
echo "name=$(node -p 'require(\"./package.json\").name')" >> "$GITHUB_OUTPUT"
163+
echo "version=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
164+
76165
- name: Run tests
77166
run: npm test
78167

@@ -152,7 +241,7 @@ jobs:
152241

153242
- name: Publish GitHub release
154243
run: |
155-
TAG_NAME="${GITHUB_REF#refs/tags/}"
244+
TAG_NAME="${{ needs.prepare-release.outputs.tag_name }}"
156245
RELEASE_ID="$(gh release view "$TAG_NAME" --json id --jq .id)"
157246
158247
gh api \
@@ -163,6 +252,5 @@ jobs:
163252
-f make_latest=true \
164253
-f name="${TAG_NAME}"
165254
env:
166-
GITHUB_REF: ${{ github.ref }}
167255
GITHUB_REPOSITORY: ${{ github.repository }}
168256
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

crates/legolas-cli/tests/cli_contract.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,11 @@ fn matches_validation_error_oracles() {
121121
.expect("run command");
122122

123123
assert!(!output.status.success(), "expected failure for {oracle}");
124-
assert_eq!(output.status.code(), Some(1), "expected exit code 1 for {oracle}");
124+
assert_eq!(
125+
output.status.code(),
126+
Some(1),
127+
"expected exit code 1 for {oracle}"
128+
);
125129
assert_eq!(String::from_utf8(output.stdout).expect("stdout"), "");
126130
assert_eq!(
127131
support::normalize_cli_output(&String::from_utf8(output.stderr).expect("stderr")),

crates/legolas-cli/tests/support/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use std::path::PathBuf;
21
use serde_json::Value;
2+
use std::path::PathBuf;
33

4+
#[allow(dead_code)]
45
pub fn workspace_root() -> PathBuf {
56
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
67
.parent()
@@ -9,15 +10,18 @@ pub fn workspace_root() -> PathBuf {
910
.to_path_buf()
1011
}
1112

13+
#[allow(dead_code)]
1214
pub fn fixture_path(relative_path: &str) -> PathBuf {
1315
workspace_root().join(relative_path)
1416
}
1517

18+
#[allow(dead_code)]
1619
pub fn read_oracle(relative_path: &str) -> String {
1720
std::fs::read_to_string(workspace_root().join("tests/oracles").join(relative_path))
1821
.expect("read oracle")
1922
}
2023

24+
#[allow(dead_code)]
2125
pub fn normalize_cli_output(output: &str) -> String {
2226
to_posix(output.to_string()).replace(
2327
&to_posix(
@@ -29,12 +33,14 @@ pub fn normalize_cli_output(output: &str) -> String {
2933
)
3034
}
3135

36+
#[allow(dead_code)]
3237
pub fn normalize_analysis_json_output(output: &str) -> Value {
3338
let mut analysis = serde_json::from_str::<Value>(output).expect("parse analysis json");
3439
normalize_analysis_value(&mut analysis);
3540
analysis
3641
}
3742

43+
#[allow(dead_code)]
3844
fn normalize_analysis_value(analysis: &mut Value) {
3945
replace_string_field(analysis, &["projectRoot"], "<PROJECT_ROOT>");
4046
replace_string_field(analysis, &["metadata", "generatedAt"], "<GENERATED_AT>");
@@ -46,6 +52,7 @@ fn normalize_analysis_value(analysis: &mut Value) {
4652
normalize_object_array_string_field(analysis, &["treeShakingWarnings"], "files");
4753
}
4854

55+
#[allow(dead_code)]
4956
fn replace_string_field(root: &mut Value, path: &[&str], replacement: &str) {
5057
let Some(value) = get_path_mut(root, path) else {
5158
return;
@@ -56,6 +63,7 @@ fn replace_string_field(root: &mut Value, path: &[&str], replacement: &str) {
5663
}
5764
}
5865

66+
#[allow(dead_code)]
5967
fn normalize_string_array(root: &mut Value, path: &[&str]) {
6068
let Some(Value::Array(items)) = get_path_mut(root, path) else {
6169
return;
@@ -68,6 +76,7 @@ fn normalize_string_array(root: &mut Value, path: &[&str]) {
6876
}
6977
}
7078

79+
#[allow(dead_code)]
7180
fn normalize_object_array_string_field(root: &mut Value, path: &[&str], field: &str) {
7281
let Some(Value::Array(items)) = get_path_mut(root, path) else {
7382
return;
@@ -86,6 +95,7 @@ fn normalize_object_array_string_field(root: &mut Value, path: &[&str], field: &
8695
}
8796
}
8897

98+
#[allow(dead_code)]
8999
fn get_path_mut<'a>(value: &'a mut Value, path: &[&str]) -> Option<&'a mut Value> {
90100
let mut current = value;
91101

@@ -96,6 +106,7 @@ fn get_path_mut<'a>(value: &'a mut Value, path: &[&str]) -> Option<&'a mut Value
96106
Some(current)
97107
}
98108

109+
#[allow(dead_code)]
99110
fn to_posix(value: String) -> String {
100111
value.replace('\\', "/")
101112
}

crates/legolas-cli/tests/text_report_parity.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ fn matches_scan_visualize_and_optimize_oracles() {
2525
format_visualization_report(&analysis, 10),
2626
"basic-app/visualize.txt",
2727
);
28-
assert_report_matches_oracle(format_optimize_report(&analysis, 5), "basic-app/optimize.txt");
28+
assert_report_matches_oracle(
29+
format_optimize_report(&analysis, 5),
30+
"basic-app/optimize.txt",
31+
);
2932
}
3033

3134
#[test]
@@ -97,8 +100,7 @@ fn optimize_and_visualize_reports_clamp_zero_limits_and_cover_lazy_load_fallback
97100
potential_kb_saved: 42,
98101
estimated_lcp_improvement_ms: 88,
99102
confidence: "directional".to_string(),
100-
summary: "Targeted impact: a handful of focused optimizations should pay off."
101-
.to_string(),
103+
summary: "Targeted impact: a handful of focused optimizations should pay off.".to_string(),
102104
};
103105
optimize_analysis.lazy_load_candidates = vec![LazyLoadCandidate {
104106
name: "chart.js".to_string(),

crates/legolas-core/src/analyze.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
cmp::Reverse,
23
fs,
34
path::Path,
45
time::{Duration, SystemTime, UNIX_EPOCH},
@@ -38,9 +39,8 @@ const KNOWN_BUNDLE_ARTIFACTS: [&str; 5] = [
3839

3940
pub fn analyze_project<P: AsRef<Path>>(input_path: P) -> Result<Analysis> {
4041
let project_root = find_project_root(input_path)?;
41-
let manifest: Value = read_json_if_exists(project_root.join("package.json"))?.ok_or_else(|| {
42-
LegolasError::PackageJsonMissing(project_root.display().to_string())
43-
})?;
42+
let manifest: Value = read_json_if_exists(project_root.join("package.json"))?
43+
.ok_or_else(|| LegolasError::PackageJsonMissing(project_root.display().to_string()))?;
4444

4545
let package_manager = detect_package_manager(&project_root, &manifest)?;
4646
let frameworks = detect_frameworks(&project_root, &manifest)?;
@@ -73,7 +73,10 @@ pub fn analyze_project<P: AsRef<Path>>(input_path: P) -> Result<Analysis> {
7373
duplicate_packages: duplicate_analysis.duplicates,
7474
lazy_load_candidates,
7575
tree_shaking_warnings,
76-
unused_dependency_candidates: build_unused_dependency_candidates(&manifest, &source_analysis),
76+
unused_dependency_candidates: build_unused_dependency_candidates(
77+
&manifest,
78+
&source_analysis,
79+
),
7780
warnings: duplicate_analysis.warnings,
7881
impact,
7982
metadata: Metadata {
@@ -126,15 +129,17 @@ fn build_heavy_dependency_report(
126129
category: intel.category.to_string(),
127130
rationale: intel.rationale.to_string(),
128131
recommendation: intel.recommendation.to_string(),
129-
imported_by: import_info.map(|item| item.files.clone()).unwrap_or_default(),
132+
imported_by: import_info
133+
.map(|item| item.files.clone())
134+
.unwrap_or_default(),
130135
dynamic_imported_by: import_info
131136
.map(|item| item.dynamic_files.clone())
132137
.unwrap_or_default(),
133138
import_count: import_info.map(|item| item.files.len()).unwrap_or(0),
134139
});
135140
}
136141

137-
heavy_dependencies.sort_by(|left, right| right.estimated_kb.cmp(&left.estimated_kb));
142+
heavy_dependencies.sort_by_key(|item| Reverse(item.estimated_kb));
138143
heavy_dependencies
139144
}
140145

@@ -151,8 +156,9 @@ fn merged_dependency_entries(manifest: &Value) -> Vec<(String, String)> {
151156
continue;
152157
};
153158

154-
if let Some((_, existing_range)) =
155-
entries.iter_mut().find(|(existing_name, _)| existing_name == name)
159+
if let Some((_, existing_range)) = entries
160+
.iter_mut()
161+
.find(|(existing_name, _)| existing_name == name)
156162
{
157163
*existing_range = version_range.to_string();
158164
continue;
@@ -203,13 +209,13 @@ fn build_lazy_load_candidates(
203209
});
204210
}
205211

206-
candidates.sort_by(|left, right| right.estimated_savings_kb.cmp(&left.estimated_savings_kb));
212+
candidates.sort_by_key(|item| Reverse(item.estimated_savings_kb));
207213
candidates
208214
}
209215

210216
fn build_tree_shaking_warnings(source_analysis: &SourceAnalysis) -> Vec<TreeShakingWarning> {
211217
let mut warnings = source_analysis.tree_shaking_warnings.clone();
212-
warnings.sort_by(|left, right| right.estimated_kb.cmp(&left.estimated_kb));
218+
warnings.sort_by_key(|item| Reverse(item.estimated_kb));
213219
warnings
214220
}
215221

@@ -278,9 +284,7 @@ fn format_iso8601_utc(duration: Duration) -> String {
278284
let minute = (seconds_of_day % 3_600) / 60;
279285
let second = seconds_of_day % 60;
280286

281-
format!(
282-
"{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{milliseconds:03}Z"
283-
)
287+
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{milliseconds:03}Z")
284288
}
285289

286290
fn civil_from_days(days_since_unix_epoch: i64) -> (i32, u32, u32) {

crates/legolas-core/src/import_scanner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ fn ensure_directory_exists(path: &Path) -> Result<()> {
202202

203203
fn walk(current_path: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
204204
let mut entries = fs::read_dir(current_path)?.collect::<std::io::Result<Vec<_>>>()?;
205-
entries.sort_by(|left, right| left.file_name().cmp(&right.file_name()));
205+
entries.sort_by_key(|entry| entry.file_name());
206206

207207
for entry in entries {
208208
let absolute_path = entry.path();

crates/legolas-core/src/lockfiles.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,10 @@ fn collect_from_package_lock(package_lock: Option<Value>) -> VersionsByName {
156156
return versions_by_name;
157157
};
158158

159-
if let Some(packages) = package_lock_object.get("packages").and_then(Value::as_object) {
159+
if let Some(packages) = package_lock_object
160+
.get("packages")
161+
.and_then(Value::as_object)
162+
{
160163
if !packages.is_empty() {
161164
for (package_path, metadata) in packages {
162165
let Some(version) = metadata
@@ -526,9 +529,7 @@ fn starts_with_ascii_alpha(value: &str) -> bool {
526529

527530
fn strip_wrapping_quotes(value: &str) -> &str {
528531
let trimmed_prefix = value.strip_prefix('"').unwrap_or(value);
529-
trimmed_prefix
530-
.strip_suffix('"')
531-
.unwrap_or(trimmed_prefix)
532+
trimmed_prefix.strip_suffix('"').unwrap_or(trimmed_prefix)
532533
}
533534

534535
fn unsupported_bun_lockfile_warning(lockfile: &Lockfile) -> String {

crates/legolas-core/tests/analyze_parity.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ fn analyze_project_switches_to_artifact_assisted_mode_only_for_real_files() {
3737
let analysis = analyze_project(root).expect("analyze project");
3838

3939
assert_eq!(analysis.metadata.mode, "artifact-assisted");
40-
assert_eq!(analysis.bundle_artifacts, vec!["dist/stats.json".to_string()]);
40+
assert_eq!(
41+
analysis.bundle_artifacts,
42+
vec!["dist/stats.json".to_string()]
43+
);
4144
}
4245

4346
#[test]

0 commit comments

Comments
 (0)