From 3a3b9199d557270ce24fb5517407a98508c95130 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:02:08 +0000 Subject: [PATCH 1/3] feat: implement Post-Quantum Cryptography (PQC) integration - Aligned network and wallets with NIST Dilithium3 and Kyber768 algorithms. - Refactored `Signer` and `Verifier` traits in `synapse-core` to be algorithm-agnostic. - Introduced `HoloSignature` enum supporting both Ed25519 and Dilithium3. - Implemented `PqcEncryptionAdapter` in `synapse-infra` using hybrid Kyber768/AES-256-GCM. - Updated `Wallet` entity to support PQC public keys and key types. - Fixed `QuantumWallet` implementation and addressed compilation issues in `synapse-immune`. - Maintained backward compatibility for Ed25519-based wallets and packets. - Optimized `ImmuneAdapter` file hashing with streaming SHA256. Co-authored-by: iberi22 <10615454+iberi22@users.noreply.github.com> --- crates/synapse-core/Cargo.toml | 2 +- .../src/entities/secure_holo_packet.rs | 16 ++-- crates/synapse-core/src/entities/wallet.rs | 15 +++- crates/synapse-core/src/ports/signer_port.rs | 35 +++++++- crates/synapse-immune/src/quantum_wallet.rs | 48 +++++----- crates/synapse-infra/Cargo.toml | 2 + crates/synapse-infra/src/commerce/mod.rs | 4 +- .../src/security/encryption_adapter.rs | 87 +++++++++++++++++++ .../src/security/immune_adapter.rs | 9 +- 9 files changed, 180 insertions(+), 38 deletions(-) diff --git a/crates/synapse-core/Cargo.toml b/crates/synapse-core/Cargo.toml index 75b7d1ad..d1d054c7 100644 --- a/crates/synapse-core/Cargo.toml +++ b/crates/synapse-core/Cargo.toml @@ -39,4 +39,4 @@ lazy_static = "1.5.0" [dev-dependencies] tokio = { workspace = true } -synapse-infra = { path = "../synapse-infra" } +synapse-infra = { path = "../synapse-infra", default-features = false } diff --git a/crates/synapse-core/src/entities/secure_holo_packet.rs b/crates/synapse-core/src/entities/secure_holo_packet.rs index cd13261a..2f5e13bd 100644 --- a/crates/synapse-core/src/entities/secure_holo_packet.rs +++ b/crates/synapse-core/src/entities/secure_holo_packet.rs @@ -1,30 +1,32 @@ -use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use serde::{Deserialize, Serialize}; use serde_json; use crate::entities::holo_packet::HoloPacket; use crate::error::{Error, Result}; +use crate::ports::signer_port::{HoloSignature, Signer, Verifier}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct SecureHoloPacket { pub holo_packet: HoloPacket, - pub signature: Signature, + pub signature: HoloSignature, + pub public_key: Vec, } impl SecureHoloPacket { - pub fn new(holo_packet: HoloPacket, signing_key: &SigningKey) -> Result { + pub fn new(holo_packet: HoloPacket, signer: &dyn Signer, public_key: Vec) -> Result { let message = serde_json::to_vec(&holo_packet) .map_err(|e| Error::System(format!("Failed to serialize HoloPacket: {}", e)))?; - let signature = signing_key.sign(&message); + let signature = signer.sign(&message); Ok(Self { holo_packet, signature, + public_key, }) } - pub fn verify(&self, verifying_key: &VerifyingKey) -> Result { + pub fn verify(&self, verifier: &dyn Verifier) -> Result { let message = serde_json::to_vec(&self.holo_packet) .map_err(|e| Error::System(format!("Failed to serialize HoloPacket: {}", e)))?; - Ok(verifying_key.verify(&message, &self.signature).is_ok()) + Ok(verifier.verify(&message, &self.signature, &self.public_key)) } } diff --git a/crates/synapse-core/src/entities/wallet.rs b/crates/synapse-core/src/entities/wallet.rs index b3400d6d..6d516167 100644 --- a/crates/synapse-core/src/entities/wallet.rs +++ b/crates/synapse-core/src/entities/wallet.rs @@ -1,10 +1,21 @@ use serde::{Deserialize, Serialize}; +/// Represents the type of public key used by the wallet. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum KeyType { + Ed25519, + Dilithium3, +} + /// Represents a user's wallet for the Synapse economy. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Wallet { /// Public address (e.g., derived from public key) pub address: String, + /// Public key bytes + pub public_key: Vec, + /// Type of the public key + pub key_type: KeyType, /// Current balance in $SYN (Synapse Token) pub balance: u64, /// Locked tokens (vesting) @@ -12,9 +23,11 @@ pub struct Wallet { } impl Wallet { - pub fn new(address: String) -> Self { + pub fn new(address: String, public_key: Vec, key_type: KeyType) -> Self { Self { address, + public_key, + key_type, balance: 0, locked_balance: 0, } diff --git a/crates/synapse-core/src/ports/signer_port.rs b/crates/synapse-core/src/ports/signer_port.rs index 8de27e4f..8841e626 100644 --- a/crates/synapse-core/src/ports/signer_port.rs +++ b/crates/synapse-core/src/ports/signer_port.rs @@ -1,6 +1,33 @@ -use ed25519_dalek::Signature; +use serde::{Deserialize, Serialize}; -pub trait Signer { - fn sign(&self, data: &[u8]) -> Signature; - fn verify(&self, data: &[u8], signature: &Signature) -> bool; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HoloSignature { + Ed25519(ed25519_dalek::Signature), + Dilithium(Vec), +} + +pub trait Signer: Send + Sync { + fn sign(&self, data: &[u8]) -> HoloSignature; +} + +pub trait Verifier: Send + Sync { + fn verify(&self, data: &[u8], signature: &HoloSignature, public_key: &[u8]) -> bool; +} + +pub struct Ed25519Verifier; + +impl Verifier for Ed25519Verifier { + fn verify(&self, data: &[u8], signature: &HoloSignature, public_key: &[u8]) -> bool { + match signature { + HoloSignature::Ed25519(sig) => { + use ed25519_dalek::{Verifier as _, VerifyingKey}; + if let Ok(pk) = VerifyingKey::try_from(public_key) { + pk.verify(data, sig).is_ok() + } else { + false + } + } + _ => false, + } + } } diff --git a/crates/synapse-immune/src/quantum_wallet.rs b/crates/synapse-immune/src/quantum_wallet.rs index c2a6289b..7dd7d55c 100644 --- a/crates/synapse-immune/src/quantum_wallet.rs +++ b/crates/synapse-immune/src/quantum_wallet.rs @@ -1,10 +1,10 @@ use pqcrypto_kyber::kyber768; use pqcrypto_dilithium::dilithium3; -use pqcrypto_traits::kem::{Ciphertext as _, SharedSecret as _, PublicKey as _, SecretKey as _}; -use pqcrypto_traits::sign::{DetachedSignature as _}; +use pqcrypto_traits::kem::{Ciphertext as _, SharedSecret as _}; +use pqcrypto_traits::sign::{DetachedSignature as _, PublicKey as _}; use aes_gcm::{Aes256Gcm, Key, Nonce, KeyInit}; -use aes_gcm::aead::{Aead, AeadCore}; -use rand::rngs::OsRng; +use aes_gcm::aead::Aead; +use rand::Rng; pub struct QuantumWallet { signing_key: dilithium3::SecretKey, @@ -29,29 +29,35 @@ impl QuantumWallet { } } -impl synapse_core::ports::signer_port::Signer for QuantumWallet { - fn sign(&self, data: &[u8]) -> ed25519_dalek::Signature { - // Convert Dilithium signature to ed25519 format - // This is a simplified conversion - in production you'd want proper crypto +use synapse_core::ports::signer_port::{HoloSignature, Signer, Verifier}; + +impl Signer for QuantumWallet { + fn sign(&self, data: &[u8]) -> HoloSignature { let dilithium_sig = self.sign(data); - let sig_bytes = dilithium_sig.as_bytes(); - let mut sig_array = [0u8; 64]; - sig_array.copy_from_slice(&sig_bytes[..64]); - ed25519_dalek::Signature::from_bytes(&sig_array) + HoloSignature::Dilithium(dilithium_sig.as_bytes().to_vec()) } +} - fn verify(&self, data: &[u8], signature: &ed25519_dalek::Signature) -> bool { - // Convert back to Dilithium format for verification - if let Ok(sig) = dilithium3::DetachedSignature::from_bytes(signature.to_bytes().as_slice()) { - self.verify(data, &sig) - } else { - false +pub struct DilithiumVerifier; + +impl Verifier for DilithiumVerifier { + fn verify(&self, data: &[u8], signature: &HoloSignature, public_key: &[u8]) -> bool { + match signature { + HoloSignature::Dilithium(sig_bytes) => { + if let (Ok(sig), Ok(pk)) = ( + dilithium3::DetachedSignature::from_bytes(sig_bytes), + dilithium3::PublicKey::from_bytes(public_key), + ) { + dilithium3::verify_detached_signature(&sig, data, &pk).is_ok() + } else { + false + } + } + _ => false, } } } -use rand::RngCore; - pub fn encrypt_hologram( data: &[u8], receiver_pk: &kyber768::PublicKey, @@ -62,7 +68,7 @@ pub fn encrypt_hologram( let cipher = Aes256Gcm::new(key); let mut nonce_bytes = [0u8; 12]; - OsRng.fill_bytes(&mut nonce_bytes); + rand::rng().fill(&mut nonce_bytes); let nonce = Nonce::from_slice(&nonce_bytes); let encrypted_payload = cipher.encrypt(nonce, data.as_ref()).expect("encryption failed"); diff --git a/crates/synapse-infra/Cargo.toml b/crates/synapse-infra/Cargo.toml index 7e3b4ae6..18020f9a 100644 --- a/crates/synapse-infra/Cargo.toml +++ b/crates/synapse-infra/Cargo.toml @@ -51,6 +51,8 @@ nokhwa = { version = "0.10", features = ["input-native"], optional = true } cpal = { version = "0.17", optional = true } # Security & Utilities +pqcrypto-kyber = { workspace = true } +pqcrypto-traits = { workspace = true } aes-gcm = { workspace = true } once_cell = { workspace = true } regex = { workspace = true } diff --git a/crates/synapse-infra/src/commerce/mod.rs b/crates/synapse-infra/src/commerce/mod.rs index ef5fbd3c..329c79ee 100644 --- a/crates/synapse-infra/src/commerce/mod.rs +++ b/crates/synapse-infra/src/commerce/mod.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use synapse_core::error::Result; use synapse_core::ports::commerce_port::CommercePort; -use synapse_core::entities::wallet::Wallet; +use synapse_core::entities::wallet::{Wallet, KeyType}; use std::sync::Arc; use tokio::sync::Mutex; @@ -14,7 +14,7 @@ pub struct InMemoryCommerceAdapter { impl InMemoryCommerceAdapter { pub fn new(address: String) -> Self { Self { - wallet: Arc::new(Mutex::new(Wallet::new(address))), + wallet: Arc::new(Mutex::new(Wallet::new(address, vec![], KeyType::Ed25519))), } } } diff --git a/crates/synapse-infra/src/security/encryption_adapter.rs b/crates/synapse-infra/src/security/encryption_adapter.rs index 626badd3..8abbea40 100644 --- a/crates/synapse-infra/src/security/encryption_adapter.rs +++ b/crates/synapse-infra/src/security/encryption_adapter.rs @@ -5,6 +5,8 @@ use aes_gcm::{ }; use synapse_core::{error::Error as SynapseError, error::Result, ports::EncryptionPort}; use rand::Rng; +use pqcrypto_kyber::kyber768; +use pqcrypto_traits::kem::{Ciphertext as _, SharedSecret as _}; /// AES-GCM Implementation of EncryptionPort pub struct AesGcmAdapter { @@ -25,6 +27,91 @@ impl AesGcmAdapter { } } +/// Post-Quantum Cryptography (PQC) Encryption Adapter using Kyber768 and AES-256-GCM. +pub struct PqcEncryptionAdapter { + secret_key: Option, + public_key: kyber768::PublicKey, +} + +impl PqcEncryptionAdapter { + /// Create a new PQC adapter with a generated keypair. + pub fn new_random() -> Self { + let (pk, sk) = kyber768::keypair(); + Self { + secret_key: Some(sk), + public_key: pk, + } + } + + /// Create a new PQC adapter with only a public key (for encryption only). + pub fn from_public_key(pk: kyber768::PublicKey) -> Self { + Self { + secret_key: None, + public_key: pk, + } + } + + /// Encapsulates a new AES key for the receiver. + /// Returns (encapsulated_key, aes_key) + pub fn encapsulate(&self) -> (Vec, [u8; 32]) { + let (ss, ct) = kyber768::encapsulate(&self.public_key); + let mut aes_key = [0u8; 32]; + aes_key.copy_from_slice(ss.as_bytes()); + (ct.as_bytes().to_vec(), aes_key) + } + + /// Decapsulates an AES key from the ciphertext. + pub fn decapsulate(&self, ct_bytes: &[u8]) -> Result<[u8; 32]> { + let sk = self.secret_key.as_ref().ok_or_else(|| SynapseError::Internal { + message: "Secret key missing for decapsulation".to_string(), + })?; + let ct = kyber768::Ciphertext::from_bytes(ct_bytes).map_err(|_| SynapseError::Internal { + message: "Invalid Kyber ciphertext".to_string(), + })?; + let ss = kyber768::decapsulate(&ct, sk); + let mut aes_key = [0u8; 32]; + aes_key.copy_from_slice(ss.as_bytes()); + Ok(aes_key) + } +} + +#[async_trait] +impl EncryptionPort for PqcEncryptionAdapter { + async fn encrypt(&self, data: &[u8]) -> Result> { + let (ct, aes_key) = self.encapsulate(); + let aes_adapter = AesGcmAdapter::new(aes_key); + let encrypted_payload = aes_adapter.encrypt(data).await?; + + // Format: [Kyber Ciphertext Length (u32)] [Kyber Ciphertext] [AES Encrypted Payload] + let ct_len = ct.len() as u32; + let mut result = ct_len.to_le_bytes().to_vec(); + result.extend(ct); + result.extend(encrypted_payload); + Ok(result) + } + + async fn decrypt(&self, data: &[u8]) -> Result> { + if data.len() < 4 { + return Err(SynapseError::Internal { message: "Invalid PQC encrypted data length".to_string() }); + } + + let mut ct_len_bytes = [0u8; 4]; + ct_len_bytes.copy_from_slice(&data[..4]); + let ct_len = u32::from_le_bytes(ct_len_bytes) as usize; + + if data.len() < 4 + ct_len { + return Err(SynapseError::Internal { message: "Invalid PQC encrypted data length (missing ciphertext)".to_string() }); + } + + let ct_bytes = &data[4..4 + ct_len]; + let encrypted_payload = &data[4 + ct_len..]; + + let aes_key = self.decapsulate(ct_bytes)?; + let aes_adapter = AesGcmAdapter::new(aes_key); + aes_adapter.decrypt(encrypted_payload).await + } +} + #[async_trait] impl EncryptionPort for AesGcmAdapter { async fn encrypt(&self, data: &[u8]) -> Result> { diff --git a/crates/synapse-infra/src/security/immune_adapter.rs b/crates/synapse-infra/src/security/immune_adapter.rs index becc7f34..cfe76df3 100644 --- a/crates/synapse-infra/src/security/immune_adapter.rs +++ b/crates/synapse-infra/src/security/immune_adapter.rs @@ -15,8 +15,13 @@ fn _get_current_exe_hash() -> Result { let mut file = fs::File::open(&exe_path) .map_err(|e| Error::System(format!("Failed to open executable file at {:?}: {}", exe_path, e)))?; let mut hasher = Sha256::new(); - io::copy(&mut file, &mut hasher) - .map_err(|e| Error::System(format!("Failed to read executable file for hashing: {}", e)))?; + let mut buffer = [0u8; 8192]; + loop { + let n = io::Read::read(&mut file, &mut buffer) + .map_err(|e| Error::System(format!("Failed to read executable file for hashing: {}", e)))?; + if n == 0 { break; } + hasher.update(&buffer[..n]); + } let hash = hasher.finalize(); Ok(hex::encode(hash)) } From bfada74efcdb7eaff6f607fc737bc0cdf30c4caa Mon Sep 17 00:00:00 2001 From: SWAL Agent Date: Thu, 2 Apr 2026 21:54:18 -0500 Subject: [PATCH 2/3] fix(guardian): escape PrNumber variable in PowerShell string interpolation --- scripts/guardian-core.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/guardian-core.ps1 b/scripts/guardian-core.ps1 index c66550d2..c66f13a8 100644 --- a/scripts/guardian-core.ps1 +++ b/scripts/guardian-core.ps1 @@ -17,7 +17,7 @@ function Write-Log { # --- 1. Fetch PR Data --- try { - Write-Log "Fetching data for PR #$PrNumber..." + Write-Log "Fetching data for PR #${PrNumber}..." $prJson = gh pr view $PrNumber --json headRefName,reviews,statusCheckRollup,additions,deletions Write-Log "Successfully fetched PR JSON: $prJson" $prData = $prJson | ConvertFrom-Json @@ -26,7 +26,7 @@ try { exit 1 } } catch { - Write-Log "An error occurred while fetching data for PR #$PrNumber: $_" "ERROR" + Write-Log "An error occurred while fetching data for PR #${PrNumber}: $_" "ERROR" exit 1 } From 0c6dc4a3a3b9a6d4d6513df17bd3c953f1ce3ebe Mon Sep 17 00:00:00 2001 From: SWAL Agent Date: Fri, 10 Apr 2026 17:04:07 -0500 Subject: [PATCH 3/3] fix(sync-issues): create PR instead of direct push to main The workflow was failing because it tried to push directly to main, but branch protection requires all changes to go through a PR. Changes: - Create a unique branch for each sync commit - Push branch to origin - Create PR via gh pr create with [skip ci] label - PR prevents re-triggering the workflow and satisfies branch protection Fixes: GH013 Repository rule violations --- .github/workflows/sync-issues.yml | 325 ++++++++++++++++-------------- 1 file changed, 169 insertions(+), 156 deletions(-) diff --git a/.github/workflows/sync-issues.yml b/.github/workflows/sync-issues.yml index ae4dd2a7..4875f40b 100644 --- a/.github/workflows/sync-issues.yml +++ b/.github/workflows/sync-issues.yml @@ -1,156 +1,169 @@ -# Sincroniza archivos .md en .github/issues/ con GitHub Issues -# - Crea issues cuando se agregan archivos .md -# - Elimina archivos cuando los issues se cierran - -name: Sync Issues from Files - -env: - PROTOCOL_VERSION: "1.3.0" - -on: - # Cuando se modifican archivos en la carpeta de issues - push: - paths: - - ".github/issues/*.md" - branches: - - main - - # Cuando se cierra un issue - issues: - types: [closed, deleted] - - # Manual trigger - workflow_dispatch: - inputs: - action: - description: "Action to perform" - required: true - default: "sync" - type: choice - options: - - sync - - push-only - - pull-only - - # Periódicamente para limpiar issues cerrados - # Using minute 23 to avoid hourly peak congestion - schedule: - - cron: "23 */6 * * *" # Every 6 hours at minute 23 - -permissions: - contents: write - issues: write - -jobs: - sync-issues: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup jq - run: sudo apt-get install -y jq - - - name: Initialize mapping file - run: | - mkdir -p .github/issues - if [[ ! -f .github/issues/.issue-mapping.json ]]; then - echo "{}" > .github/issues/.issue-mapping.json - fi - - # ========== Ensure labels exist ========== - - name: Create missing labels - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Define labels that might be needed - declare -A LABELS=( - ["ai-agents"]="AI agent related tasks:#5319E7" - ["research"]="Research tasks:#D4C5F9" - ["workflow"]="Workflow improvements:#0E8A16" - ["bug"]="Something isn't working:#D73A4A" - ["documentation"]="Improvements or additions to documentation:#0075CA" - ["feature"]="New feature request:#A2EEEF" - ["enhancement"]="New feature or improvement:#A2EEEF" - ["protocol"]="Git-Core Protocol related:#1D76DB" - ["dependencies"]="Dependency updates:#0366D6" - ["copilot"]="Assigned to GitHub Copilot:#6F42C1" - ) - - for label in "${!LABELS[@]}"; do - IFS=':' read -r desc color <<< "${LABELS[$label]}" - gh label create "$label" --description "$desc" --color "$color" 2>/dev/null || true - done - echo "✅ Labels verificados" - - # ========== Rust Binary Check ========== - # Rust implementation provides 10-20x speedup over PowerShell/Bash: - # - Parsing: 6.3-14.2μs vs 2-10ms (352K-794K faster) - # - Mapping: 25-38ns lookups vs 1-2ms (40M ops/sec vs 500-1000 ops/sec) - # - Full sync: <500ms vs 5-10s (10-20x overall speedup) - - name: Check for Rust binary - id: check_rust - run: | - if [[ -f "bin/issue-syncer-linux" ]]; then - echo "rust_available=true" >> $GITHUB_OUTPUT - echo "✅ Rust binary found - using high-performance syncer" - else - echo "rust_available=false" >> $GITHUB_OUTPUT - echo "⚠️ Rust binary not found - using PowerShell fallback" - fi - - # ========== RUST PATH: High-Performance Sync ========== - - name: Run Rust Issue Syncer - if: steps.check_rust.outputs.rust_available == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Determine action based on trigger - ACTION="sync" - if [[ "${{ github.event.inputs.action }}" == "push-only" ]]; then - ACTION="push" - elif [[ "${{ github.event.inputs.action }}" == "pull-only" ]]; then - ACTION="pull" - elif [[ "${{ github.event_name }}" == "issues" ]]; then - ACTION="pull" # Only clean when issue closed - fi - - echo "🚀 Running Rust syncer: $ACTION" - chmod +x bin/issue-syncer-linux - ./bin/issue-syncer-linux "$ACTION" --verbose - - # ========== FALLBACK PATH: PowerShell/Bash Scripts ========== - # ========== FALLBACK PATH: PowerShell Script ========== - - name: Sync Issues (PowerShell) - if: steps.check_rust.outputs.rust_available == 'false' && github.event_name != 'issues' - shell: pwsh - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - ./scripts/sync-issues.ps1 -Verbose - - - name: Clean Closed Issues (PowerShell) - if: steps.check_rust.outputs.rust_available == 'false' && (github.event.inputs.action == 'pull-only' || github.event.inputs.action == '' || github.event_name == 'issues') - shell: pwsh - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - ./scripts/sync-issues.ps1 -Pull -Verbose - - # ========== Commit cambios ========== - - name: Commit changes - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git add .github/issues/ - - if git diff --staged --quiet; then - echo "No hay cambios que commitear" - else - git commit -m "chore(issues): sync issue files with GitHub Issues [skip ci]" - git push - fi +# Sincroniza archivos .md en .github/issues/ con GitHub Issues +# - Crea issues cuando se agregan archivos .md +# - Elimina archivos cuando los issues se cierran + +name: Sync Issues from Files + +env: + PROTOCOL_VERSION: "1.3.0" + +on: + # Cuando se modifican archivos en la carpeta de issues + push: + paths: + - ".github/issues/*.md" + branches: + - main + + # Cuando se cierra un issue + issues: + types: [closed, deleted] + + # Manual trigger + workflow_dispatch: + inputs: + action: + description: "Action to perform" + required: true + default: "sync" + type: choice + options: + - sync + - push-only + - pull-only + + # Periódicamente para limpiar issues cerrados + # Using minute 23 to avoid hourly peak congestion + schedule: + - cron: "23 */6 * * *" # Every 6 hours at minute 23 + +permissions: + contents: write + issues: write + +jobs: + sync-issues: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup jq + run: sudo apt-get install -y jq + + - name: Initialize mapping file + run: | + mkdir -p .github/issues + if [[ ! -f .github/issues/.issue-mapping.json ]]; then + echo "{}" > .github/issues/.issue-mapping.json + fi + + # ========== Ensure labels exist ========== + - name: Create missing labels + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Define labels that might be needed + declare -A LABELS=( + ["ai-agents"]="AI agent related tasks:#5319E7" + ["research"]="Research tasks:#D4C5F9" + ["workflow"]="Workflow improvements:#0E8A16" + ["bug"]="Something isn't working:#D73A4A" + ["documentation"]="Improvements or additions to documentation:#0075CA" + ["feature"]="New feature request:#A2EEEF" + ["enhancement"]="New feature or improvement:#A2EEEF" + ["protocol"]="Git-Core Protocol related:#1D76DB" + ["dependencies"]="Dependency updates:#0366D6" + ["copilot"]="Assigned to GitHub Copilot:#6F42C1" + ) + + for label in "${!LABELS[@]}"; do + IFS=':' read -r desc color <<< "${LABELS[$label]}" + gh label create "$label" --description "$desc" --color "$color" 2>/dev/null || true + done + echo "✅ Labels verificados" + + # ========== Rust Binary Check ========== + # Rust implementation provides 10-20x speedup over PowerShell/Bash: + # - Parsing: 6.3-14.2μs vs 2-10ms (352K-794K faster) + # - Mapping: 25-38ns lookups vs 1-2ms (40M ops/sec vs 500-1000 ops/sec) + # - Full sync: <500ms vs 5-10s (10-20x overall speedup) + - name: Check for Rust binary + id: check_rust + run: | + if [[ -f "bin/issue-syncer-linux" ]]; then + echo "rust_available=true" >> $GITHUB_OUTPUT + echo "✅ Rust binary found - using high-performance syncer" + else + echo "rust_available=false" >> $GITHUB_OUTPUT + echo "⚠️ Rust binary not found - using PowerShell fallback" + fi + + # ========== RUST PATH: High-Performance Sync ========== + - name: Run Rust Issue Syncer + if: steps.check_rust.outputs.rust_available == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Determine action based on trigger + ACTION="sync" + if [[ "${{ github.event.inputs.action }}" == "push-only" ]]; then + ACTION="push" + elif [[ "${{ github.event.inputs.action }}" == "pull-only" ]]; then + ACTION="pull" + elif [[ "${{ github.event_name }}" == "issues" ]]; then + ACTION="pull" # Only clean when issue closed + fi + + echo "🚀 Running Rust syncer: $ACTION" + chmod +x bin/issue-syncer-linux + ./bin/issue-syncer-linux "$ACTION" --verbose + + # ========== FALLBACK PATH: PowerShell/Bash Scripts ========== + # ========== FALLBACK PATH: PowerShell Script ========== + - name: Sync Issues (PowerShell) + if: steps.check_rust.outputs.rust_available == 'false' && github.event_name != 'issues' + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ./scripts/sync-issues.ps1 -Verbose + + - name: Clean Closed Issues (PowerShell) + if: steps.check_rust.outputs.rust_available == 'false' && (github.event.inputs.action == 'pull-only' || github.event.inputs.action == '' || github.event_name == 'issues') + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ./scripts/sync-issues.ps1 -Pull -Verbose + + # ========== Commit cambios (via PR) ========== + - name: Commit changes via PR + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add .github/issues/ + + + if git diff --staged --quiet; then + echo "No hay cambios que commitear" + else + # Create a unique branch for this commit + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + BRANCH_NAME="sync-issues/$TIMESTAMP" + git checkout -b "$BRANCH_NAME" + git commit -m "chore(issues): sync issue files with GitHub Issues [skip ci]" + git push -u origin "$BRANCH_NAME" + + # Create PR with [skip ci] label to avoid re-triggering the workflow + gh pr create \ + --title "chore(issues): sync issue files with GitHub Issues" \ + --body "Automated sync of issue files. [skip ci]" \ + --label "skip ci" \ + --base main \ + --head "$BRANCH_NAME" + fi