Skip to content

Commit e625d20

Browse files
committed
feat: label protocol images as latest if artifacts match offchain
1 parent ff75304 commit e625d20

File tree

3 files changed

+266
-2
lines changed

3 files changed

+266
-2
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env rust-script
2+
//! ```cargo
3+
//! [package]
4+
//! name = "directory-comparison"
5+
//! version = "0.1.0"
6+
//! edition = "2021"
7+
//! description = "A tool to compare directory contents using Merkle trees"
8+
//!
9+
//! [dependencies]
10+
//! merkle_hash = "3.7"
11+
//! clap = { version = "4.0", features = ["derive", "env"] }
12+
//! ```
13+
14+
use std::collections::HashMap;
15+
use std::error::Error;
16+
use std::path::Path;
17+
use std::process;
18+
use merkle_hash::{MerkleTree, Encodable};
19+
use clap::Parser;
20+
21+
#[derive(Parser)]
22+
#[command(name = "directory-comparison")]
23+
#[command(about = "Compare directory contents using Merkle trees")]
24+
struct Cli {
25+
/// First directory to compare
26+
dir1: String,
27+
28+
/// Second directory to compare
29+
dir2: String,
30+
31+
/// Pattern to ignore during comparison
32+
#[arg(long, env = "IGNORE_PATTERN")]
33+
ignore: Option<String>,
34+
}
35+
36+
fn main() {
37+
let cli = Cli::parse();
38+
39+
match directories_match(&cli.dir1, &cli.dir2, cli.ignore.as_deref()) {
40+
Ok(true) => process::exit(0),
41+
Ok(false) => process::exit(1),
42+
Err(e) => {
43+
eprintln!("Error: {}", e);
44+
process::exit(1);
45+
}
46+
}
47+
}
48+
49+
fn directories_match(dir1: &str, dir2: &str, ignore: Option<&str>) -> Result<bool, Box<dyn Error>> {
50+
let tree1 = MerkleTree::builder(dir1).build()?;
51+
let tree2 = MerkleTree::builder(dir2).build()?;
52+
53+
let files1: HashMap<_, _> = tree1.iter()
54+
.filter(|i| !should_ignore_file(dir1, i, ignore))
55+
.map(|i| (i.path.relative.to_string(), i.hash.to_hex_string()))
56+
.collect();
57+
58+
let files2: HashMap<_, _> = tree2.iter()
59+
.filter(|i| !should_ignore_file(dir2, i, ignore))
60+
.map(|i| (i.path.relative.to_string(), i.hash.to_hex_string()))
61+
.collect();
62+
63+
Ok(files1 == files2)
64+
}
65+
66+
fn should_ignore_file(base_dir: &str, item: &merkle_hash::tree::Item, ignore: Option<&str>) -> bool {
67+
let path_str = item.path.relative.to_string();
68+
path_str.is_empty() ||
69+
ignore.map_or(false, |p| path_str.contains(p)) ||
70+
!Path::new(base_dir).join(&path_str).is_file()
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::*;
76+
use std::fs::{self, File};
77+
use std::io::Write;
78+
use std::path::Path;
79+
80+
fn write_file(dir: &Path, path: &str, content: &str) {
81+
let file_path = dir.join(path);
82+
if let Some(parent) = file_path.parent() {
83+
fs::create_dir_all(parent).unwrap();
84+
}
85+
File::create(file_path).unwrap().write_all(content.as_bytes()).unwrap();
86+
}
87+
88+
#[test]
89+
fn test_identical_files() {
90+
let temp = std::env::temp_dir();
91+
let dir1 = temp.join("test1");
92+
let dir2 = temp.join("test2");
93+
94+
fs::create_dir_all(&dir1).unwrap();
95+
fs::create_dir_all(&dir2).unwrap();
96+
97+
write_file(&dir1, "file.txt", "content");
98+
write_file(&dir2, "file.txt", "content");
99+
100+
assert!(directories_match(dir1.to_str().unwrap(), dir2.to_str().unwrap(), None).unwrap());
101+
102+
fs::remove_dir_all(&dir1).ok();
103+
fs::remove_dir_all(&dir2).ok();
104+
}
105+
106+
#[test]
107+
fn test_different_files() {
108+
let temp = std::env::temp_dir();
109+
let dir1 = temp.join("test3");
110+
let dir2 = temp.join("test4");
111+
112+
fs::create_dir_all(&dir1).unwrap();
113+
fs::create_dir_all(&dir2).unwrap();
114+
115+
write_file(&dir1, "file.txt", "content A");
116+
write_file(&dir2, "file.txt", "content B");
117+
118+
assert!(!directories_match(dir1.to_str().unwrap(), dir2.to_str().unwrap(), None).unwrap());
119+
120+
fs::remove_dir_all(&dir1).ok();
121+
fs::remove_dir_all(&dir2).ok();
122+
}
123+
124+
#[test]
125+
fn test_ignore_pattern() {
126+
let temp = std::env::temp_dir();
127+
let dir1 = temp.join("test5");
128+
let dir2 = temp.join("test6");
129+
130+
fs::create_dir_all(&dir1).unwrap();
131+
fs::create_dir_all(&dir2).unwrap();
132+
133+
write_file(&dir1, "keep.txt", "same");
134+
write_file(&dir2, "keep.txt", "same");
135+
write_file(&dir1, "build-info/ignore.txt", "diff1");
136+
write_file(&dir2, "build-info/ignore.txt", "diff2");
137+
138+
assert!(directories_match(dir1.to_str().unwrap(), dir2.to_str().unwrap(), Some("build-info")).unwrap());
139+
140+
fs::remove_dir_all(&dir1).ok();
141+
fs::remove_dir_all(&dir2).ok();
142+
}
143+
}

.github/workflows/artifacts.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Sync Contract Artifacts
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
protocol_ref:
7+
description: Branch tag to checkout for otim-protocol
8+
type: string
9+
default: main
10+
offchain_ref:
11+
description: Branch tag to checkout for otim-offchain
12+
type: string
13+
default: main
14+
secrets:
15+
GH_APP_ID:
16+
description: GitHub App ID for token generation
17+
required: true
18+
GH_APP_PRIVATE_KEY:
19+
description: GitHub App private key for token generation
20+
required: true
21+
outputs:
22+
artifacts_match:
23+
description: Whether artifacts match between repositories
24+
value: ${{ jobs.sync-artifacts.outputs.artifacts_match }}
25+
26+
permissions:
27+
contents: write
28+
id-token: write
29+
30+
jobs:
31+
sync-artifacts:
32+
name: Sync Contract Artifacts
33+
runs-on: ubuntu-latest
34+
outputs:
35+
artifacts_match: ${{ steps.compare-artifacts.outputs.artifacts_match }}
36+
steps:
37+
- name: Generate GitHub App Token
38+
id: bot_token
39+
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
40+
with:
41+
app_id: ${{ secrets.GH_APP_ID }}
42+
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
43+
44+
- name: Checkout protocol
45+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
46+
with:
47+
ref: ${{ inputs.protocol_ref || 'main' }}
48+
49+
- name: Checkout offchain
50+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
51+
with:
52+
repository: otimlabs/otim-offchain
53+
token: ${{ steps.bot_token.outputs.token }}
54+
path: offchain
55+
ref: ${{ inputs.offchain_ref || 'main' }}
56+
57+
- name: Install foundry
58+
uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c # v1.3.1
59+
with:
60+
version: nightly
61+
cache-key: ${{ github.job }}-${{ github.sha }}
62+
63+
- name: Build protocol
64+
id: build-protocol
65+
run: |
66+
echo "protocol_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
67+
forge soldeer update --config-location foundry
68+
forge build ./src --sizes
69+
70+
- name: Setup Rust
71+
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
72+
with:
73+
working-directory: ./offchain
74+
75+
- name: Install rust-script
76+
run: |
77+
curl -LsSf https://github.com/fornwall/rust-script/releases/latest/download/rust-script-x86_64-unknown-linux-gnu.tar.gz | tar xzf -
78+
sudo install rust-script /usr/local/bin/
79+
80+
- name: Compare and sync artifacts
81+
id: compare-artifacts
82+
run: |
83+
if rust-script .github/scripts/directory-comparison.rs "./out" "./offchain/crates/contracts/artifacts" "build-info" >/dev/null 2>&1; then
84+
echo "artifacts_match=true" >> $GITHUB_OUTPUT
85+
echo "✅ Artifacts match"
86+
else
87+
echo "artifacts_match=false" >> $GITHUB_OUTPUT
88+
echo "❌ Artifacts differ - syncing"
89+
90+
# Prepare offchain for artifact sync
91+
cd ./offchain
92+
rm -rf ./crates/contracts/artifacts
93+
cp -r ../out ./crates/contracts/artifacts
94+
fi
95+
96+
- name: Create artifact sync PR
97+
if: steps.compare-artifacts.outputs.artifacts_match == 'false'
98+
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
99+
with:
100+
token: ${{ steps.bot_token.outputs.token }}
101+
path: ./offchain
102+
commit-message: "sync: otim-protocol artifacts (${{ steps.build-protocol.outputs.protocol_sha }})"
103+
title: "Sync: otim-protocol build artifacts"
104+
body: |
105+
Syncing build artifacts from otim-protocol.
106+
107+
- Protocol: `${{ steps.build-protocol.outputs.protocol_sha }}`
108+
- Offchain: `${{ inputs.offchain_ref || 'main' }}`
109+
branch: chore/sync-artifacts
110+
branch-suffix: short-commit-hash
111+
base: ${{ inputs.offchain_ref || 'main' }}
112+
delete-branch: false

.github/workflows/docker.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,17 @@ permissions:
99
id-token: write
1010

1111
jobs:
12+
check-artifacts:
13+
if: github.ref_name == 'main'
14+
uses: ./.github/workflows/artifacts.yml
15+
with:
16+
protocol_ref: ${{ github.sha }}
17+
offchain_ref: main
18+
secrets: inherit
19+
1220
build-and-publish:
21+
needs: [check-artifacts]
22+
if: always()
1323
runs-on: ubuntu-latest
1424

1525
steps:
@@ -39,9 +49,8 @@ jobs:
3949
images: |
4050
ghcr.io/${{ github.repository }}
4151
tags: |
42-
type=raw,value=latest,enable=true
52+
type=raw,value=latest,enable=${{ github.ref_name == 'main' && needs.check-artifacts.outputs.artifacts_match == 'true' }}
4353
type=ref,event=branch
44-
type=ref,event=pr
4554
type=sha
4655
4756
- name: Build and push Docker image

0 commit comments

Comments
 (0)