Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/reproducible-dependencies.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Reproducible Dependency Installation Check

on:
pull_request:
paths:
- "**/package.json"
- "**/package-lock.json"
- "**/Cargo.toml"
- "**/Cargo.lock"
- "scripts/check-dependency-locks.sh"
push:
branches: [main]
paths:
- "**/package.json"
- "**/package-lock.json"
- "**/Cargo.toml"
- "**/Cargo.lock"
workflow_dispatch:

concurrency:
group: deps-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
verify-lockfiles:
name: verify-frozen-lockfiles
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Run Reproducible Dependency Verification
run: |
chmod +x scripts/check-dependency-locks.sh
./scripts/check-dependency-locks.sh
33 changes: 33 additions & 0 deletions docs/REPRODUCIBLE_DEPENDENCY_INSTALLATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 🔒 Reproducible Dependency Installation & Lockfile Policy

This document details NotifyChain's policy for reproducible, deterministic builds and CI dependency verification (Issue #719).

---

## 1. Core Principles

To prevent supply-chain drift, build non-determinism, and upstream package discrepancies, all dependencies must be installed from committed lockfiles using **frozen/locked mode**:

| Component | Language / Toolchain | Committed Lockfile | CI Install Command |
|---|---|---|---|
| **Listener** | Node.js (TypeScript) | `listener/package-lock.json` | `npm ci` |
| **Dashboard** | Node.js (React/Vite) | `dashboard/package-lock.json` | `npm ci` |
| **Smart Contracts** | Rust / Soroban | `contract/Cargo.lock` | `cargo check --locked` |

---

## 2. Preventing Lockfile Drift

* When editing `package.json`, developers must commit the resulting `package-lock.json`.
* CI strictly executes `scripts/check-dependency-locks.sh` on every pull request touching package manifests.
* If a lockfile drifts out-of-sync with its manifest, CI fails with clear remediation instructions.

---

## 3. Local Verification

Verify all repository lockfiles locally:

```bash
./scripts/check-dependency-locks.sh
```
100 changes: 100 additions & 0 deletions scripts/check-dependency-locks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# ==============================================================================
# NotifyChain Reproducible Dependency Installation Checker (Issue #719)
# ==============================================================================
# Verifies that dependencies in Node.js (listener, dashboard) and Rust (contract)
# can be reproduced deterministically from committed lockfiles without drift.
# ==============================================================================
set -u

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

ERRORS=0

echo -e "${BLUE}==================================================================${NC}"
echo -e "${BLUE} 🔒 NotifyChain Reproducible Dependency Installation Verification ${NC}"
echo -e "${BLUE}==================================================================${NC}\n"

check_npm_lockfile() {
local dir_name="$1"
local full_path="${ROOT_DIR}/${dir_name}"

printf "%-35s " "Checking ${dir_name} package-lock.json..."
if [ ! -f "${full_path}/package.json" ]; then
echo -e "${YELLOW}SKIPPED (no package.json)${NC}"
return
fi

if [ ! -f "${full_path}/package-lock.json" ]; then
echo -e "${RED}✗ FAILED (missing package-lock.json)${NC}"
echo -e " ${YELLOW}➔ Action: Run 'npm install --package-lock-only' in ${dir_name}/${NC}"
ERRORS=$((ERRORS + 1))
return
fi

if command -v npm >/dev/null 2>&1; then
# Run npm ci dry-run to ensure lockfile is in sync with package.json
if (cd "${full_path}" && npm ci --dry-run >/dev/null 2>&1); then
echo -e "${GREEN}✓ VALID & REPRODUCIBLE${NC}"
else
echo -e "${RED}✗ LOCKFILE DRIFT DETECTED${NC}"
echo -e " ${YELLOW}➔ Action: Lockfile is out of sync with package.json. Run 'npm install' in ${dir_name}/${NC}"
ERRORS=$((ERRORS + 1))
fi
else
echo -e "${GREEN}✓ PRESENT${NC} (npm CLI not found locally, skipping live dry-run)"
fi
}

check_cargo_lockfile() {
local dir_name="$1"
local full_path="${ROOT_DIR}/${dir_name}"

printf "%-35s " "Checking ${dir_name} Cargo.lock..."
if [ ! -f "${full_path}/Cargo.toml" ]; then
echo -e "${YELLOW}SKIPPED (no Cargo.toml)${NC}"
return
fi

if [ ! -f "${full_path}/Cargo.lock" ]; then
echo -e "${RED}✗ FAILED (missing Cargo.lock)${NC}"
echo -e " ${YELLOW}➔ Action: Run 'cargo generate-lockfile' in ${dir_name}/${NC}"
ERRORS=$((ERRORS + 1))
return
fi

if command -v cargo >/dev/null 2>&1; then
if (cd "${full_path}" && cargo check --locked --quiet >/dev/null 2>&1); then
echo -e "${GREEN}✓ VALID & FROZEN${NC}"
else
echo -e "${RED}✗ CARGO LOCK DRIFT DETECTED${NC}"
echo -e " ${YELLOW}➔ Action: Cargo.lock is out of sync. Run 'cargo check' in ${dir_name}/${NC}"
ERRORS=$((ERRORS + 1))
fi
else
echo -e "${GREEN}✓ PRESENT${NC} (cargo CLI not found locally, skipping live check)"
fi
}

echo -e "${BLUE}--- [1/2] Verifying Node.js Package Lockfiles ---${NC}"
check_npm_lockfile "listener"
check_npm_lockfile "dashboard"

echo -e "\n${BLUE}--- [2/2] Verifying Rust Smart Contract Lockfile ---${NC}"
check_cargo_lockfile "contract"

echo -e "\n${BLUE}==================================================================${NC}"
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}🎉 All dependency lockfiles are reproducible with zero drift!${NC}"
exit 0
else
echo -e "${RED}❌ Found ${ERRORS} dependency lockfile issue(s). Action required above.${NC}"
exit 1
fi