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
335 changes: 335 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
name: NotifyChain CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: short
NODE_VERSION: "22"

jobs:
# ==========================================================================
# Dependency Installation Reproducibility + Lockfile Drift Detection
# ==========================================================================
dependency-integrity:
name: Dependency Integrity (Rust + Node)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
component:
- name: "Rust (contract/)"
working-directory: "contract"
kind: "rust"
- name: "Node (dashboard/)"
working-directory: "dashboard"
kind: "node"
- name: "Node (listener/)"
working-directory: "listener"
kind: "node"
- name: "Node (frontend/)"
working-directory: "frontend"
kind: "node"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust toolchain
if: matrix.component.kind == 'rust'
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippy

- name: Cache Rust dependencies
if: matrix.component.kind == 'rust'
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
contract/target
key: ${{ runner.os }}-cargo-${{ hashFiles('contract/Cargo.lock', 'contract/**/Cargo.toml') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Install Node.js
if: matrix.component.kind == 'node'
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Cache Node dependencies
if: matrix.component.kind == 'node'
uses: actions/cache@v4
with:
path: ${{ matrix.component.working-directory }}/node_modules
key: ${{ runner.os }}-node-${{ matrix.component.working-directory }}-${{ hashFiles(format('{0}/package-lock.json', matrix.component.working-directory)) }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.component.working-directory }}-

# ------------------------------------------------------------------
# Locked-mode installation
# ------------------------------------------------------------------
- name: "Rust: build with --locked (enforce Cargo.lock)"
if: matrix.component.kind == 'rust'
working-directory: ${{ matrix.component.working-directory }}
run: cargo build --locked --release

- name: "Node: install with npm ci (enforce package-lock.json)"
if: matrix.component.kind == 'node'
working-directory: ${{ matrix.component.working-directory }}
run: npm ci

# ------------------------------------------------------------------
# Post-install lockfile drift detection
# ------------------------------------------------------------------
- name: Detect lockfile drift after install
id: drift
shell: bash
run: |
set -eu
CHANGES="$(git status --porcelain)"
if [ -n "$CHANGES" ]; then
echo "========================================"
echo "LOCKFILE DRIFT DETECTED"
echo "========================================"
echo "git status --porcelain output:"
echo "$CHANGES"
echo ""
echo "The following working tree files changed during"
echo "locked-mode installation. This means the committed lockfile"
echo "is out of date with the declared manifest dependencies."
echo ""
echo "----------------------------------------"
echo "HOW TO FIX (run locally and commit):"
echo "----------------------------------------"
if [ "${{ matrix.component.kind }}" = "rust" ]; then
echo " cd ${{ matrix.component.working-directory }}"
echo " cargo update # regenerates Cargo.lock from Cargo.toml"
echo " # or run the build that caused the drift then diff"
else
echo " cd ${{ matrix.component.working-directory }}"
echo " rm -rf node_modules package-lock.json"
echo " npm install # regenerates package-lock.json from package.json"
fi
echo ""
echo "Then: git add <lockfile> && git commit -m \"chore(deps): refresh lockfile\""
echo "========================================"
exit 1
else
echo "No lockfile drift detected — install is reproducible."
fi

# ==========================================================================
# Dependency Vulnerability Scanning
# ==========================================================================
vulnerability-scan:
name: Vulnerability Scan (JS + Rust)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
component:
- name: "Rust (contract/)"
working-directory: "contract"
kind: "rust"
- name: "Node (dashboard/)"
working-directory: "dashboard"
kind: "node"
- name: "Node (listener/)"
working-directory: "listener"
kind: "node"
- name: "Node (frontend/)"
working-directory: "frontend"
kind: "node"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust toolchain + cargo-audit
if: matrix.component.kind == 'rust'
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo-audit
if: matrix.component.kind == 'rust'
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/bin/cargo-audit
key: ${{ runner.os }}-cargo-audit-v2

- name: Install cargo-audit
if: matrix.component.kind == 'rust'
run: |
if ! command -v cargo-audit >/dev/null 2>&1; then
cargo install cargo-audit --locked
fi

- name: Install Node.js
if: matrix.component.kind == 'node'
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: "Node: install with npm ci (enforce package-lock.json)"
if: matrix.component.kind == 'node'
working-directory: ${{ matrix.component.working-directory }}
run: npm ci

# ------------------------------------------------------------------
# Rust audit (block on High/Critical, warn on Low/Medium)
# ------------------------------------------------------------------
- name: "Rust: cargo audit (informational, all severities)"
if: matrix.component.kind == 'rust'
working-directory: ${{ matrix.component.working-directory }}
continue-on-error: true
run: |
echo "=== cargo audit (informational: all severities) ==="
cargo audit || true

- name: "Rust: cargo audit (blocking: deny warnings = High/Critical)"
if: matrix.component.kind == 'rust'
working-directory: ${{ matrix.component.working-directory }}
run: |
echo "=== cargo audit (blocking: High + Critical only) ==="
# --deny-warnings exits non-zero for any 'warning' (High/Critical)
# Informational/Low (unmaintained crates without known vulns) pass
cargo audit --deny warnings

# ------------------------------------------------------------------
# npm audit (block on High/Critical, warn on Low/Medium)
# ------------------------------------------------------------------
- name: "Node: npm audit (informational, all severities)"
if: matrix.component.kind == 'node'
working-directory: ${{ matrix.component.working-directory }}
continue-on-error: true
run: |
echo "=== npm audit (informational: all severities) ==="
npm audit --json > audit-report.json || true
node -e "
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('audit-report.json', 'utf8'));
const severity = report.metadata && report.metadata.vulnerabilities || {};
console.log('Summary:', JSON.stringify(severity, null, 2));
" 2>/dev/null || true
npm audit || true

- name: "Node: npm audit (blocking: audit-level = high)"
if: matrix.component.kind == 'node'
working-directory: ${{ matrix.component.working-directory }}
run: |
echo "=== npm audit (blocking: high + critical only) ==="
echo "Low / Medium vulnerabilities are informational only and do not block CI."
echo "If this step fails, fix the high/critical advisories listed above."
npm audit --audit-level=high

# ==========================================================================
# Contract: Rust unit + integration tests (Soroban)
# ==========================================================================
contract-tests:
name: Smart Contract Tests
runs-on: ubuntu-latest
needs: dependency-integrity
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippy

- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
contract/target
key: ${{ runner.os }}-cargo-tests-${{ hashFiles('contract/Cargo.lock', 'contract/**/Cargo.toml') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Run cargo build --locked
working-directory: contract
run: cargo build --locked

- name: Run cargo test (pause + all contract tests)
working-directory: contract/contracts/hello-world
run: cargo test --locked -- --nocapture

- name: cargo clippy (contract workspace)
working-directory: contract
run: cargo clippy --locked --all-targets -- -D warnings

- name: cargo fmt --check
working-directory: contract
run: cargo fmt --all -- --check

# ==========================================================================
# Event Documentation Drift Detection
# ==========================================================================
event-docs-drift:
name: Event Documentation Drift Check
runs-on: ubuntu-latest
needs: dependency-integrity
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Install listener deps (provides ts-node + typescript)
working-directory: listener
run: npm ci

- name: Run event documentation drift detector
working-directory: listener
run: npm run check:event-docs

# ==========================================================================
# Node component tests + typechecks
# ==========================================================================
node-tests:
name: Node Component Tests (dashboard, listener, frontend)
runs-on: ubuntu-latest
needs: dependency-integrity
strategy:
fail-fast: false
matrix:
include:
- component: "dashboard"
steps: "npm run lint && npm run test"
- component: "listener"
steps: "npm run lint && npm run test"
- component: "frontend"
steps: "npm run lint && npm run test"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Install with npm ci
working-directory: ${{ matrix.component }}
run: npm ci

- name: Run lint + test
working-directory: ${{ matrix.component }}
run: |
set -e
${{ matrix.steps }}
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,27 @@ The PR template will prompt you for:

---

## Security and Vulnerability Management

Before modifying dependencies, manifests, or lockfiles:

1. Run the vulnerability scanners locally (they also run automatically in CI):
- **Rust**: `cd contract && cargo audit --deny warnings`
- **Node**: `cd <component> && npm audit --audit-level=high`
2. Ensure installs are reproducible: `npm ci` (Node) and `cargo build --locked`
(Rust). Never commit a modified `package.json` / `Cargo.toml` without the
matching refreshed lockfile.
3. Verify the on-chain event reference is not stale:
```bash
cd listener
npm run check:event-docs
```

Full policy, step-by-step remediation playbook, and contacts for responsible
disclosure are documented in [`docs/security.md`](docs/security.md).

---

## Releasing NotifyChain

Maintainers preparing a tagged release should follow the steps in
Expand Down
Loading
Loading