mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
docs: remove obsolete archive and migration materials
The ngit-relay migration is complete and archived operational reports and migration scripts are no longer needed. Remove the entire docs/archive directory and its documentation links, including older dangling references. Keep investigation, deployment-check, and soak notes in ignored work/ and update agent and work-directory instructions to prevent recreating the archive. Lasting product documentation belongs in the existing Diataxis directories. This changes no runtime behavior and does not rewrite published history. Validation: verified the directory is absent, checked tracked documentation and configuration for archive references, and ran git diff --check. Runtime tests are unnecessary for documentation and retired-script removal. Assisted-by: GPT-6
This commit is contained in:
@@ -305,7 +305,8 @@ This was a key learning from GRASP-01: docs described plans, not implementation,
|
||||
|
||||
**Session files go in `work/` (gitignored except README.md)**
|
||||
|
||||
- Archive valuable content to `docs/archive/YYYY-MM-DD-*.md` at session end
|
||||
- Keep investigation reports, deployment checks, and soak notes in ignored `work/`; do not commit them.
|
||||
- Put lasting product documentation directly in the appropriate Diátaxis directory.
|
||||
- Delete temporary files
|
||||
- Keep root clean (only README.md, AGENTS.md)
|
||||
|
||||
|
||||
@@ -98,9 +98,6 @@ WORKING │ How-To │ Reference │
|
||||
|
||||
## 📂 Additional Resources
|
||||
|
||||
### [Archive](archive/)
|
||||
Historical session notes and completed work. Useful for understanding project evolution but not required reading.
|
||||
|
||||
### [Learnings](learnings/)
|
||||
**DEPRECATED** - Being migrated to Diátaxis structure:
|
||||
- Gotchas → How-To Guides
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# relay.ngit.dev Migration Archive (January 2026)
|
||||
|
||||
**Status:** Reference only - not maintained
|
||||
|
||||
This directory contains the migration guide and scripts used during the
|
||||
relay.ngit.dev migration from ngit-relay to ngit-grasp in January 2026.
|
||||
|
||||
## ⚠️ Important
|
||||
|
||||
These materials are **archived for reference only**:
|
||||
|
||||
- **Scripts are specific to the relay.ngit.dev migration context**
|
||||
- **Not designed for general use or other migrations**
|
||||
- **May not work without modification**
|
||||
- **Not maintained or supported**
|
||||
|
||||
Do not expect these scripts to work out of the box for your migration.
|
||||
|
||||
## What's Here
|
||||
|
||||
- `migration-guide.md` - Lessons learned, approach, and context from the actual migration
|
||||
- `scripts/` - Analysis and validation scripts used during the migration process
|
||||
|
||||
## Why Archive This?
|
||||
|
||||
The relay.ngit.dev migration uncovered numerous bugs and edge cases that resulted
|
||||
in critical production fixes. See commits in the `4bc5-relay-ngit-dev-migration-v2`
|
||||
branch for details.
|
||||
|
||||
These materials document:
|
||||
|
||||
- Real-world migration challenges encountered
|
||||
- Debugging approaches that worked in practice
|
||||
- Context for production fixes merged from this branch
|
||||
- Iterative script development during active migration
|
||||
|
||||
## Using This as Reference
|
||||
|
||||
If you're planning a migration to ngit-grasp:
|
||||
|
||||
1. **Read the migration guide** for conceptual approach and lessons learned
|
||||
2. **Review the scripts** to understand what kinds of analysis were needed
|
||||
3. **Expect to write your own scripts** tailored to your specific context
|
||||
4. **Test extensively** in a non-production environment first
|
||||
|
||||
These materials show what was needed for one specific migration, not a
|
||||
general-purpose migration toolkit.
|
||||
|
||||
## Context
|
||||
|
||||
This migration was completed in January 2026 and resulted in relay.ngit.dev
|
||||
running ngit-grasp in production. The branch containing these materials also
|
||||
includes critical fixes for:
|
||||
|
||||
- Git protocol error handling
|
||||
- Naughty list false positives
|
||||
- Purgatory event tracking
|
||||
- Sync startup issues
|
||||
- Configuration management
|
||||
|
||||
Those fixes are now part of the main codebase.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,533 +0,0 @@
|
||||
# How-To: Test Sync Against Production Data
|
||||
|
||||
> **Quick Start Prompt:** Check work/active-issues/ for existing issues. If issues exist, pick the most important, fix it, test with cargo test, run clippy and fmt, commit, and report back with a brief 1-2 sentence summary of each issue you identified. If no issues exist, run a 60-second production sync test, analyze logs, create individual issue files in work/active-issues/ (one per issue with minimal description), then report summary listing each issue in 1-2 sentences.
|
||||
|
||||
**Problem:** Debug and improve sync behavior using real-world data from production relays
|
||||
**Difficulty:** Intermediate
|
||||
**Time:** 30 minutes per iteration
|
||||
|
||||
## Two-Mode Workflow
|
||||
|
||||
This guide operates in two modes:
|
||||
|
||||
### Mode 1: Fix Existing Issues
|
||||
**When:** There are files in `work/active-issues/` (excluding README.md)
|
||||
|
||||
1. Check for active issues: `ls work/active-issues/`
|
||||
2. Pick the most important issue to fix
|
||||
3. **Review proposed fix and ask for permission before implementing**
|
||||
4. Implement the fix (after approval)
|
||||
5. Run `cargo test` to verify tests pass
|
||||
6. Run `cargo clippy` to check for warnings
|
||||
7. Run `cargo fmt` to format code
|
||||
8. Commit changes with descriptive message
|
||||
9. Report back - **DO NOT** do another issue or run more tests
|
||||
|
||||
### Mode 2: Discover New Issues
|
||||
**When:** No active issues in `work/active-issues/`
|
||||
|
||||
1. Run 60-second production sync test (logs saved to `tmp/run-{timestamp}/`)
|
||||
2. Analyze logs for errors, warnings, unexpected patterns
|
||||
3. Document each issue as a separate markdown file in `work/active-issues/`
|
||||
4. Keep issue files minimal - just enough to identify the issue
|
||||
5. Report brief summary listing each issue in 1-2 sentences
|
||||
6. **DO NOT** create separate detailed analysis files
|
||||
7. **DO NOT** do thorough investigation or root cause analysis
|
||||
|
||||
## Overview
|
||||
|
||||
This guide helps you run ngit-grasp's sync system against production relays to discover unexpected errors, inefficiencies, and edge cases that don't appear in controlled tests.
|
||||
|
||||
**Why production testing matters:**
|
||||
- Real data has inconsistencies, malformed events, and edge cases
|
||||
- Production relays may behave differently (rate limiting, timeouts, partial NIP-77 support)
|
||||
- Volume and patterns reveal performance bottlenecks
|
||||
- Sync discovery leads to cascading subscriptions we can't predict in tests
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- ngit-grasp compiles successfully (`cargo build`)
|
||||
- Familiarity with [GRASP-02 Proactive Sync](../explanation/grasp-02-proactive-sync.md)
|
||||
- Understanding of log levels and tracing
|
||||
|
||||
## Test Setup
|
||||
|
||||
### 1. Choose a Test Identity
|
||||
|
||||
Pick a domain with manageable sync volume. Smaller domains mean fewer repos to sync, making logs tractable.
|
||||
|
||||
**Recommended starting point:**
|
||||
```bash
|
||||
--domain ngit.danconwaydev.com
|
||||
```
|
||||
|
||||
This domain has few repo announcements listing it, so sync stays manageable.
|
||||
|
||||
### 2. Choose a Bootstrap Relay
|
||||
|
||||
The bootstrap relay provides the initial set of announcements to discover repos:
|
||||
|
||||
```bash
|
||||
--sync-bootstrap-relay-url wss://git.shakespeare.diy
|
||||
```
|
||||
|
||||
### 3. Run with Time Limit
|
||||
|
||||
Run for **60 seconds** to allow the full sync cascade to complete. This duration allows:
|
||||
- Layer 1: Discovery of repo announcements (0-5s)
|
||||
- Layer 2: Sending `#a`, `#A`, `#q` filters for repos (5-30s)
|
||||
- Layer 3: Receiving issues, patches, PRs (30-60s)
|
||||
- Layer 4: Receiving comments on root events (40-60s)
|
||||
|
||||
Each run creates its own subdirectory in `tmp/` to keep data and logs isolated:
|
||||
|
||||
```bash
|
||||
# Create run directory with timestamp
|
||||
RUN_DIR="tmp/run-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
# Run for 60 seconds, saving both raw and sanitized logs
|
||||
timeout 60s cargo run -- \
|
||||
--sync-bootstrap-relay-url wss://git.shakespeare.diy \
|
||||
--domain ngit.danconwaydev.com \
|
||||
--git-data-path "$RUN_DIR/git-data" \
|
||||
--relay-data-path "$RUN_DIR/relay-data" \
|
||||
2>&1 | tee "$RUN_DIR/sync-raw.log" | ./scripts/sanitize-logs.sh | tee "$RUN_DIR/sync.log"
|
||||
```
|
||||
|
||||
**Why 60 seconds?** The sync system uses a 5-second batch window for aggregating discovered repos. Repos discovered late in the sync need time for:
|
||||
1. Batch window to expire (5s)
|
||||
2. Layer 2 filters to be sent and processed
|
||||
3. Layer 3 events (issues/patches/PRs) to be returned
|
||||
4. Layer 4 filters for root events to be sent
|
||||
5. Comments and threaded replies to be returned
|
||||
|
||||
Testing shows 30 seconds is too short for late-discovered repos to complete the Layer 2→3→4 cascade.
|
||||
|
||||
**Note:** The `timeout` command returns exit code 124, which is expected.
|
||||
|
||||
**Directory structure after run:**
|
||||
```
|
||||
tmp/
|
||||
└── run-20260109-143022/
|
||||
├── git-data/ # Git repository data
|
||||
├── relay-data/ # Relay database
|
||||
├── sync.log # Sanitized log (for quick analysis)
|
||||
└── sync-raw.log # Raw log (for full details when needed)
|
||||
```
|
||||
|
||||
**When to use which log:**
|
||||
- **sync.log** - Use for quick scanning and pattern recognition (long lines truncated)
|
||||
- **sync-raw.log** - Use when you need full details (e.g., complete rejection reasons, full event data)
|
||||
|
||||
## Log Sanitization
|
||||
|
||||
Raw logs include full events and hundreds of event IDs per line, making them unwieldy for analysis. The sanitizer truncates long lines:
|
||||
|
||||
```bash
|
||||
./scripts/sanitize-logs.sh < raw.log > sanitized.log
|
||||
|
||||
# Or pipe directly
|
||||
cargo run -- [args] 2>&1 | ./scripts/sanitize-logs.sh
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--head-chars N` - First N characters to show (default: 200)
|
||||
- `--tail-chars N` - Last N characters to show (default: 100)
|
||||
|
||||
Example output:
|
||||
```
|
||||
2024-01-09T10:00:00Z DEBUG sync: Processing events ids=[abc123, def456, ghi789, jkl012...<1847 chars>...xyz999, end123]
|
||||
```
|
||||
|
||||
### Retrieving Full Details from Raw Logs
|
||||
|
||||
When sanitized logs show truncated messages (e.g., rejection reasons), use the raw log to see the complete content:
|
||||
|
||||
```bash
|
||||
# Find specific error in raw log
|
||||
grep "Rejected repository announcement" "$RUN_DIR/sync-raw.log"
|
||||
|
||||
# Extract full line for specific event ID
|
||||
grep "note1z5ys7wf3ms5yxhnp3kfw7hpu5asfkx4jngzt5zgs4tm4tnvggnsqjfqeyt" "$RUN_DIR/sync-raw.log"
|
||||
|
||||
# View context around a truncated warning
|
||||
grep -A 2 -B 2 "pattern from sanitized log" "$RUN_DIR/sync-raw.log"
|
||||
```
|
||||
|
||||
The raw log contains complete, untruncated messages including full rejection reasons, event data, and debug details.
|
||||
|
||||
## What to Look For
|
||||
|
||||
### Phase 1: Connection & Bootstrap (0-5 seconds)
|
||||
|
||||
**Expected behavior:**
|
||||
- Connection to bootstrap relay succeeds
|
||||
- Layer 1 (announcement) subscription starts
|
||||
- First batch of 30617/30618 events received
|
||||
|
||||
**Red flags:**
|
||||
- Connection timeout or failure
|
||||
- NIP-77 negentropy errors (should fall back gracefully)
|
||||
- Immediate rate limiting
|
||||
|
||||
### Phase 2: Discovery Cascade (5-15 seconds)
|
||||
|
||||
**Expected behavior:**
|
||||
- Self-subscriber batches fire as announcements are processed
|
||||
- New relays discovered from announcement `relays` tags
|
||||
- Layer 2 (repo tags) subscriptions created
|
||||
|
||||
**Red flags:**
|
||||
- Excessive relay discovery (>10 relays rapidly)
|
||||
- Filter consolidation warnings (>70 filters)
|
||||
- Missing self-subscriber batch logs
|
||||
|
||||
### Phase 3: Steady State (15+ seconds)
|
||||
|
||||
**Expected behavior:**
|
||||
- Historic sync batches completing (EOSE received)
|
||||
- Periodic health checks running
|
||||
- Events being saved to database
|
||||
|
||||
**Red flags:**
|
||||
- Pending batches never confirming
|
||||
- Repeated connection/disconnect cycles
|
||||
- Memory growth (check with `top` in another terminal)
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
When analyzing logs, look for these patterns:
|
||||
|
||||
### Errors to Investigate
|
||||
|
||||
| Pattern | Possible Cause | Action |
|
||||
|---------|----------------|--------|
|
||||
| `error` (any) | Unexpected failure | Investigate immediately |
|
||||
| `connection failed` | Network/relay issue | Check relay URL, try different relay |
|
||||
| `rate limit` | Too many requests | Check consolidation, increase backoff |
|
||||
| `negentropy` + `error` | NIP-77 incompatibility | Should fall back - verify it does |
|
||||
| `timeout` | Slow relay or large sync | Increase timeouts or reduce scope |
|
||||
|
||||
### Warnings to Monitor
|
||||
|
||||
| Pattern | Meaning | Action |
|
||||
|---------|---------|--------|
|
||||
| `consolidating filters` | Filter count high | Expected, but frequent = problem |
|
||||
| `backing off` | Health tracker retry | Normal, but watch for excessive |
|
||||
| `batch failed` | Historic sync incomplete | Check which batches, why |
|
||||
|
||||
### Debug Patterns to Verify
|
||||
|
||||
| Pattern | What it shows |
|
||||
|---------|---------------|
|
||||
| `fresh_start` | Full sync initiated |
|
||||
| `quick_reconnect` | Incremental sync (<15min gap) |
|
||||
| `historic sync complete` | Sync finished successfully |
|
||||
| `sync_live` | Live subscriptions active |
|
||||
| `PendingBatch` | Items awaiting EOSE confirmation |
|
||||
|
||||
## Mode 1: Fix Existing Issues (Detailed)
|
||||
|
||||
When `work/active-issues/` contains issue files:
|
||||
|
||||
### Step 1: Check for Active Issues
|
||||
|
||||
```bash
|
||||
ls work/active-issues/
|
||||
```
|
||||
|
||||
If any `.md` files exist (excluding README.md), you're in Mode 1.
|
||||
|
||||
### Step 2: Pick Most Important Issue
|
||||
|
||||
Review issue files and select based on:
|
||||
- Severity (errors > warnings > log quality)
|
||||
- Impact (functionality > performance > UX)
|
||||
- Complexity (quick fixes first to clear backlog)
|
||||
|
||||
### Step 3: Review Proposed Fix and Get Permission
|
||||
|
||||
**IMPORTANT:** Before implementing any changes:
|
||||
|
||||
1. Read relevant code files to understand the issue
|
||||
2. Analyze the root cause
|
||||
3. Propose a fix with explanation of what will change and why
|
||||
4. Summarize the proposed fix in 2-3 sentences
|
||||
5. **Ask for user permission to proceed**
|
||||
|
||||
**Do NOT implement changes without explicit approval.**
|
||||
|
||||
### Step 4: Implement the Fix
|
||||
|
||||
After receiving permission, make the necessary code changes based on the issue description and approved plan.
|
||||
|
||||
### Step 5: Test, Lint, Format
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Check for warnings
|
||||
cargo clippy
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
```
|
||||
|
||||
### Step 6: Commit
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "fix: [brief description of what was fixed]"
|
||||
```
|
||||
|
||||
### Step 7: Report Back
|
||||
|
||||
**STOP HERE.** Report what was fixed. Do NOT:
|
||||
- Fix another issue
|
||||
- Run production sync test
|
||||
- Do additional investigation
|
||||
|
||||
The workflow will cycle back through Mode 1 if more issues remain.
|
||||
|
||||
## Mode 2: Discover New Issues (Detailed)
|
||||
|
||||
When `work/active-issues/` is empty (or only contains README.md):
|
||||
|
||||
### Step 1: Run Production Sync Test
|
||||
|
||||
```bash
|
||||
# Create run directory with timestamp
|
||||
RUN_DIR="tmp/run-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
# Run 60-second test, saving both raw and sanitized logs
|
||||
timeout 60s cargo run -- \
|
||||
--sync-bootstrap-relay-url wss://git.shakespeare.diy \
|
||||
--domain ngit.danconwaydev.com \
|
||||
--git-data-path "$RUN_DIR/git-data" \
|
||||
--relay-data-path "$RUN_DIR/relay-data" \
|
||||
2>&1 | tee "$RUN_DIR/sync-raw.log" | ./scripts/sanitize-logs.sh | tee "$RUN_DIR/sync.log"
|
||||
```
|
||||
|
||||
Each run is isolated in its own timestamped directory under `tmp/`, keeping data and logs organized. Both raw and sanitized logs are saved for flexible analysis.
|
||||
|
||||
**Note:** 60 seconds allows the full sync cascade (Layer 1→2→3→4) to complete for late-discovered repos.
|
||||
|
||||
### Step 2: Analyze Logs
|
||||
|
||||
Scan for errors and unexpected patterns:
|
||||
```bash
|
||||
# Find the most recent run
|
||||
LATEST_RUN=$(ls -1t tmp/run-*/sync.log | head -n1)
|
||||
LATEST_RAW=$(ls -1t tmp/run-*/sync-raw.log | head -n1)
|
||||
|
||||
# Analyze sanitized log for quick scanning
|
||||
grep -i error "$LATEST_RUN"
|
||||
grep -i warn "$LATEST_RUN"
|
||||
grep -i panic "$LATEST_RUN"
|
||||
|
||||
# If you find truncated messages, check the raw log for full details
|
||||
grep "pattern from truncated message" "$LATEST_RAW"
|
||||
```
|
||||
|
||||
### Step 3: Document Issues
|
||||
|
||||
Create **one markdown file per issue** in `work/active-issues/`:
|
||||
|
||||
```bash
|
||||
# Example: Minimal issue documentation
|
||||
cat > work/active-issues/bootstrap-disconnect.md <<'EOF'
|
||||
# Bootstrap relay disconnects when empty
|
||||
|
||||
Bootstrap relay wss://git.shakespeare.diy disconnects after sync finds 0 events. Should persist since user-specified.
|
||||
|
||||
Log: "Disconnecting empty relay relay=wss://git.shakespeare.diy"
|
||||
File: src/sync/mod.rs (check_disconnects function)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Keep each file brief:**
|
||||
- Descriptive title (one line)
|
||||
- What happens (1-2 sentences max)
|
||||
- Relevant log excerpt (one line)
|
||||
- File/function location if obvious (one line)
|
||||
- **NO** separate detailed analysis files
|
||||
- **NO** root cause analysis
|
||||
- **NO** proposed solutions (unless immediately obvious)
|
||||
|
||||
### Step 4: Report Summary
|
||||
|
||||
Provide a brief closing message with 1-2 sentence summary of **each issue** identified:
|
||||
- State what the issue is
|
||||
- Where it occurs (file/component)
|
||||
- Keep it concise
|
||||
|
||||
**STOP HERE.** Do NOT:
|
||||
- Fix the issues immediately
|
||||
- Create separate detailed analysis markdown files
|
||||
- Do thorough investigations
|
||||
- Write lengthy explanations
|
||||
|
||||
The workflow will cycle back through Mode 1 to fix issues one at a time.
|
||||
|
||||
## Logging Improvements
|
||||
|
||||
If the logs aren't helpful enough, improve them. Common needs:
|
||||
|
||||
### Add Context to Existing Logs
|
||||
|
||||
```rust
|
||||
// Before
|
||||
tracing::debug!("Processing events");
|
||||
|
||||
// After
|
||||
tracing::debug!(
|
||||
relay = %relay_url,
|
||||
event_count = events.len(),
|
||||
"Processing events"
|
||||
);
|
||||
```
|
||||
|
||||
### Add New Log Points
|
||||
|
||||
Key places that may need more logging:
|
||||
- `src/sync/mod.rs` - SyncManager state transitions
|
||||
- `src/sync/relay_connection.rs` - Connection lifecycle
|
||||
- `src/sync/self_subscriber.rs` - Batch processing
|
||||
|
||||
### Reduce Noise
|
||||
|
||||
If a log line appears too frequently:
|
||||
```rust
|
||||
// Change from debug! to trace!
|
||||
tracing::trace!("Per-event detail that's too noisy");
|
||||
```
|
||||
|
||||
## Managing Active Issues
|
||||
|
||||
Issues are tracked in `work/active-issues/` as individual markdown files.
|
||||
|
||||
**Check for active issues:**
|
||||
```bash
|
||||
ls work/active-issues/
|
||||
```
|
||||
|
||||
**After fixing an issue:**
|
||||
```bash
|
||||
# Delete the resolved issue file
|
||||
rm work/active-issues/issue-name.md
|
||||
|
||||
# Or archive if important for future reference
|
||||
mv work/active-issues/issue-name.md docs/archive/2026-01-09-issue-name.md
|
||||
```
|
||||
|
||||
**Issue file format (minimal):**
|
||||
```markdown
|
||||
# Brief title
|
||||
|
||||
What happens (1-2 sentences).
|
||||
|
||||
Log evidence: "relevant log line"
|
||||
File: src/path/to/file.rs (function_name if known)
|
||||
```
|
||||
|
||||
Keep documentation minimal - just enough to identify and locate the issue.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Summary
|
||||
|
||||
```
|
||||
Check work/active-issues/
|
||||
│
|
||||
├─ Has issues? ──► Mode 1: Pick one issue
|
||||
│ │
|
||||
│ ├─ Review & propose fix
|
||||
│ ├─ Ask permission
|
||||
│ ├─ Fix code (after approval)
|
||||
│ ├─ cargo test
|
||||
│ ├─ cargo clippy
|
||||
│ ├─ cargo fmt
|
||||
│ ├─ git commit
|
||||
│ └─ Report & STOP
|
||||
│
|
||||
└─ No issues? ──► Mode 2: Run production sync
|
||||
│
|
||||
├─ timeout 60s cargo run ...
|
||||
├─ Analyze logs
|
||||
├─ Document issues (minimal)
|
||||
└─ Report summary & STOP
|
||||
```
|
||||
|
||||
**Key Rules:**
|
||||
- Only do ONE thing per cycle (fix one issue OR discover issues)
|
||||
- Always stop after reporting
|
||||
- Keep issue documentation minimal
|
||||
- No root cause analysis during discovery
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Minimal Test Command
|
||||
|
||||
```bash
|
||||
# Create run directory
|
||||
RUN_DIR="tmp/run-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
# Run test with both raw and sanitized logs (60s for full cascade)
|
||||
timeout 60s cargo run -- \
|
||||
--sync-bootstrap-relay-url wss://git.shakespeare.diy \
|
||||
--domain ngit.danconwaydev.com \
|
||||
--git-data-path "$RUN_DIR/git-data" \
|
||||
--relay-data-path "$RUN_DIR/relay-data" \
|
||||
2>&1 | tee "$RUN_DIR/sync-raw.log" | ./scripts/sanitize-logs.sh | tee "$RUN_DIR/sync.log"
|
||||
```
|
||||
|
||||
### With Metrics Endpoint
|
||||
|
||||
```bash
|
||||
# Create run directory
|
||||
RUN_DIR="tmp/run-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
# Run with metrics and both log formats (60s for full cascade)
|
||||
timeout 60s cargo run -- \
|
||||
--sync-bootstrap-relay-url wss://git.shakespeare.diy \
|
||||
--domain ngit.danconwaydev.com \
|
||||
--git-data-path "$RUN_DIR/git-data" \
|
||||
--relay-data-path "$RUN_DIR/relay-data" \
|
||||
--metrics-address 127.0.0.1:9090 \
|
||||
2>&1 | tee "$RUN_DIR/sync-raw.log" | ./scripts/sanitize-logs.sh | tee "$RUN_DIR/sync.log"
|
||||
```
|
||||
|
||||
Then in another terminal: `curl http://127.0.0.1:9090/metrics`
|
||||
|
||||
### Cleanup Old Runs
|
||||
|
||||
```bash
|
||||
# Remove runs older than 7 days
|
||||
find tmp/run-* -type d -mtime +7 -exec rm -rf {} +
|
||||
|
||||
# Remove all test runs
|
||||
rm -rf tmp/run-*
|
||||
```
|
||||
|
||||
### Different Log Level
|
||||
|
||||
The default is DEBUG. For more detail:
|
||||
```bash
|
||||
RUST_LOG=trace cargo run -- [args]
|
||||
```
|
||||
|
||||
For less noise:
|
||||
```bash
|
||||
RUST_LOG=info cargo run -- [args]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Part of the [ngit-grasp documentation](../README.md) using the [Diátaxis](https://diataxis.fr/) framework.*
|
||||
@@ -1,213 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 01-fetch-events.sh - Fetch nostr events from a relay for migration analysis
|
||||
#
|
||||
# PHASE 1 of the GRASP relay to ngit-grasp migration analysis pipeline.
|
||||
# Fetches kind 30618 (state), 30617 (announcement), and 5 (deletion) events.
|
||||
#
|
||||
# USAGE:
|
||||
# ./01-fetch-events.sh <relay-url> <output-dir>
|
||||
#
|
||||
# EXAMPLES:
|
||||
# # Fetch from production relay
|
||||
# ./01-fetch-events.sh wss://relay.ngit.dev output/prod
|
||||
#
|
||||
# # Fetch from archive relay
|
||||
# ./01-fetch-events.sh wss://archive.relay.ngit.dev output/archive
|
||||
#
|
||||
# # Full migration analysis setup
|
||||
# mkdir -p work/migration-analysis-$(date +%Y%m%d-%H%M)
|
||||
# ./01-fetch-events.sh wss://relay.ngit.dev work/migration-analysis-*/prod
|
||||
# ./01-fetch-events.sh wss://archive.relay.ngit.dev work/migration-analysis-*/archive
|
||||
#
|
||||
# OUTPUT:
|
||||
# <output-dir>/raw/state-events.json - kind 30618 events (one per line, JSONL)
|
||||
# <output-dir>/raw/announcements.json - kind 30617 events (one per line, JSONL)
|
||||
# <output-dir>/raw/deletions.json - kind 5 events (one per line, JSONL)
|
||||
#
|
||||
# OUTPUT FORMAT:
|
||||
# Each file contains one JSON event per line (JSONL format).
|
||||
# Events are the raw nostr event objects as returned by the relay.
|
||||
#
|
||||
# PREREQUISITES:
|
||||
# - nak (Nostr Army Knife) - https://github.com/fiatjaf/nak
|
||||
# - jq (for counting/validation)
|
||||
#
|
||||
# RUNTIME: ~30 seconds per relay (depends on network and event count)
|
||||
#
|
||||
# NOTES:
|
||||
# - Uses --paginate to ensure all events are fetched (not just first page)
|
||||
# - If event counts are exact multiples of 250, pagination may have failed
|
||||
# - Run Phase 1 and Phase 2 back-to-back for accurate snapshot
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <relay-url> <output-dir>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " relay-url WebSocket URL of the relay (e.g., wss://relay.ngit.dev)"
|
||||
echo " output-dir Directory to store fetched events (e.g., output/prod)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 wss://relay.ngit.dev output/prod"
|
||||
echo " $0 wss://archive.relay.ngit.dev output/archive"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
local missing=0
|
||||
|
||||
if ! command -v nak &> /dev/null; then
|
||||
log_error "nak not found. Install from: https://github.com/fiatjaf/nak"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "jq not found. Install with your package manager."
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if [[ $missing -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetch events of a specific kind
|
||||
# Args: $1=relay, $2=kind, $3=output_file, $4=description
|
||||
fetch_kind() {
|
||||
local relay="$1"
|
||||
local kind="$2"
|
||||
local output_file="$3"
|
||||
local description="$4"
|
||||
|
||||
log_info "Fetching $description (kind $kind) from $relay..."
|
||||
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
# Use --paginate to ensure we get all events, not just first page
|
||||
# nak outputs one event per line (JSONL format)
|
||||
# NOTE: nak buffers output when stdout is not a TTY (e.g. non-interactive SSH).
|
||||
# Use 'script' to provide a pseudo-TTY so nak flushes each line immediately.
|
||||
# Then strip the nak connection banner ("connecting to ...") and carriage returns
|
||||
# that 'script' injects into the output stream.
|
||||
if ! script -q -c "nak req -k $kind --paginate $relay" /dev/null 2>/dev/null \
|
||||
| tr -d '\r' \
|
||||
| grep -v '^connecting to ' \
|
||||
> "$output_file"; then
|
||||
log_error "Failed to fetch $description from $relay"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
# Count events
|
||||
local count
|
||||
count=$(wc -l < "$output_file" | tr -d ' ')
|
||||
|
||||
# Warn if count is suspicious (exact multiple of 250 suggests pagination issue)
|
||||
if [[ $count -gt 0 ]] && [[ $((count % 250)) -eq 0 ]]; then
|
||||
log_warn "$description count ($count) is exact multiple of 250 - pagination may have failed!"
|
||||
fi
|
||||
|
||||
log_success "Fetched $count $description in ${duration}s -> $output_file"
|
||||
|
||||
echo "$count"
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
if [[ $# -ne 2 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local relay="$1"
|
||||
local output_dir="$2"
|
||||
|
||||
# Validate relay URL
|
||||
if [[ ! "$relay" =~ ^wss?:// ]]; then
|
||||
log_error "Invalid relay URL: $relay (must start with ws:// or wss://)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_prerequisites
|
||||
|
||||
log_info "Starting event fetch from $relay"
|
||||
log_info "Output directory: $output_dir"
|
||||
|
||||
# Create output directory structure
|
||||
local raw_dir="$output_dir/raw"
|
||||
mkdir -p "$raw_dir"
|
||||
|
||||
local total_start
|
||||
total_start=$(date +%s)
|
||||
|
||||
# Fetch each event type
|
||||
local state_count announcement_count deletion_count
|
||||
|
||||
state_count=$(fetch_kind "$relay" 30618 "$raw_dir/state-events.json" "state events")
|
||||
announcement_count=$(fetch_kind "$relay" 30617 "$raw_dir/announcements.json" "announcements")
|
||||
deletion_count=$(fetch_kind "$relay" 5 "$raw_dir/deletions.json" "deletion requests")
|
||||
|
||||
local total_end
|
||||
total_end=$(date +%s)
|
||||
local total_duration=$((total_end - total_start))
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
log_info "=== Fetch Summary ==="
|
||||
log_info "Relay: $relay"
|
||||
log_info "Output: $output_dir"
|
||||
log_info "State events (30618): $state_count"
|
||||
log_info "Announcements (30617): $announcement_count"
|
||||
log_info "Deletions (5): $deletion_count"
|
||||
log_info "Total time: ${total_duration}s"
|
||||
echo ""
|
||||
|
||||
# Output file listing for easy copy/paste
|
||||
log_info "Output files:"
|
||||
echo " $raw_dir/state-events.json"
|
||||
echo " $raw_dir/announcements.json"
|
||||
echo " $raw_dir/deletions.json"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,564 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 10-check-git-sync.sh - Compare state events to actual git data on disk
|
||||
#
|
||||
# PHASE 2 of the GRASP relay to ngit-grasp migration analysis pipeline.
|
||||
# Compares kind 30618 state events against actual git refs on disk.
|
||||
#
|
||||
# USAGE:
|
||||
# ./10-check-git-sync.sh <state-events.json> <git-base-dir> <output-dir> [--categorize]
|
||||
#
|
||||
# EXAMPLES:
|
||||
# # Check source relay against source git data
|
||||
# ./10-check-git-sync.sh output/prod/raw/state-events.json /var/lib/grasp-relay/git output/prod
|
||||
#
|
||||
# # Check target relay against target git data
|
||||
# ./10-check-git-sync.sh output/archive/raw/state-events.json /var/lib/ngit-grasp/git output/archive
|
||||
#
|
||||
# # Check and categorize in one step (convenience mode)
|
||||
# ./10-check-git-sync.sh output/prod/raw/state-events.json /var/lib/grasp-relay/git output/prod --categorize
|
||||
#
|
||||
# INPUT:
|
||||
# state-events.json - JSONL file from Phase 1 (01-fetch-events.sh)
|
||||
# One kind 30618 event per line
|
||||
# git-base-dir - Base directory containing git repos
|
||||
# Structure: <git-base>/<npub>/<repo>.git/
|
||||
#
|
||||
# OUTPUT:
|
||||
# <output-dir>/git-sync-status.tsv - Tab-separated values:
|
||||
# repo<TAB>npub<TAB>state_refs<TAB>git_refs<TAB>matches<TAB>reason
|
||||
#
|
||||
# With --categorize flag, also outputs:
|
||||
# <output-dir>/category1-complete-match.txt
|
||||
# <output-dir>/category2-empty-blank.txt
|
||||
# <output-dir>/category3-partial-match.txt
|
||||
# <output-dir>/category4-no-match.txt
|
||||
#
|
||||
# CATEGORIES:
|
||||
# 1. Complete Match - All refs in state event match git data perfectly
|
||||
# 2. Empty/Blank - No git data available (directory missing or empty)
|
||||
# 3. Partial Match - Some refs match, some don't
|
||||
# 4. No Match - Git data exists but commit hashes don't match
|
||||
#
|
||||
# PREREQUISITES:
|
||||
# - nak (for npub encoding) - https://github.com/fiatjaf/nak
|
||||
# - jq (for JSON parsing)
|
||||
# - Read access to git directories (may need sudo)
|
||||
#
|
||||
# RUNTIME: ~20 minutes on VPS (git operations are slow)
|
||||
#
|
||||
# NOTES:
|
||||
# - Must run on VPS with access to git directories
|
||||
# - Progress indicator updates every 10 events
|
||||
# - Handles packed refs (git show-ref) and loose refs
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
# 01-fetch-events.sh - Phase 1 script that produces input for this script
|
||||
# 20-categorize.sh - Phase 3a script that consumes output from this script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_progress() {
|
||||
# Overwrite current line for progress updates
|
||||
echo -ne "\r${BLUE}[PROGRESS]${NC} $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <state-events.json> <git-base-dir> <output-dir> [--categorize]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " state-events.json JSONL file from Phase 1 (kind 30618 events)"
|
||||
echo " git-base-dir Base directory for git repos (e.g., /var/lib/grasp-relay/git)"
|
||||
echo " output-dir Directory to store output files"
|
||||
echo " --categorize Optional: also output category files (like Phase 3)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 output/prod/raw/state-events.json /var/lib/grasp-relay/git output/prod"
|
||||
echo " $0 output/archive/raw/state-events.json /var/lib/ngit-grasp/git output/archive"
|
||||
echo ""
|
||||
echo "Output:"
|
||||
echo " git-sync-status.tsv - TSV with: repo, npub, state_refs, git_refs, matches, reason"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
local missing=0
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
log_error "git not found. Install with your package manager."
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v nak &> /dev/null; then
|
||||
log_error "nak not found. Install from: https://github.com/fiatjaf/nak"
|
||||
log_error "Or run: nix-shell -p nak jq --run \"$0 $*\""
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "jq not found. Install with your package manager."
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if [[ $missing -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Convert hex pubkey to npub
|
||||
# Args: $1=hex_pubkey
|
||||
# Returns: npub string or empty on error
|
||||
hex_to_npub() {
|
||||
local hex="$1"
|
||||
nak encode npub "$hex" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
# Count refs in state event (only refs/heads/)
|
||||
# Args: $1=event_json
|
||||
# Returns: count
|
||||
count_state_refs() {
|
||||
local event="$1"
|
||||
echo "$event" | jq '[.tags[] | select(.[0] | startswith("refs/heads/"))] | length' 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Get git refs from disk
|
||||
# Args: $1=git_dir
|
||||
# Returns: count of refs/heads/ refs
|
||||
count_git_refs() {
|
||||
local git_dir="$1"
|
||||
|
||||
if [[ ! -d "$git_dir" ]]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Try git show-ref first (handles packed refs correctly)
|
||||
# Note: We capture output separately to avoid pipefail issues
|
||||
local count
|
||||
if count=$(git --git-dir="$git_dir" show-ref --heads 2>/dev/null | wc -l); then
|
||||
echo "$count" | tr -d ' '
|
||||
return
|
||||
fi
|
||||
|
||||
# Fallback: count loose refs (when git is not available or fails)
|
||||
if [[ -d "$git_dir/refs/heads" ]]; then
|
||||
find "$git_dir/refs/heads" -type f 2>/dev/null | wc -l | tr -d ' '
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get ref hash from git directory
|
||||
# Args: $1=git_dir, $2=ref_path (e.g., refs/heads/main)
|
||||
# Returns: commit hash or empty
|
||||
get_git_ref_hash() {
|
||||
local git_dir="$1"
|
||||
local ref_path="$2"
|
||||
|
||||
# Try git show-ref first (handles packed refs)
|
||||
local hash
|
||||
hash=$(git --git-dir="$git_dir" show-ref --hash "$ref_path" 2>/dev/null | head -1 || echo "")
|
||||
|
||||
if [[ -n "$hash" ]]; then
|
||||
echo "$hash"
|
||||
return
|
||||
fi
|
||||
|
||||
# Fallback: read loose ref file
|
||||
local ref_file="$git_dir/$ref_path"
|
||||
if [[ -f "$ref_file" ]]; then
|
||||
cat "$ref_file" 2>/dev/null | tr -d '\n' || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Compare state event refs to git refs
|
||||
# Args: $1=event_json, $2=git_dir
|
||||
# Returns: count of matching refs
|
||||
count_matching_refs() {
|
||||
local event="$1"
|
||||
local git_dir="$2"
|
||||
local matching=0
|
||||
|
||||
# Extract refs/heads/ tags and compare
|
||||
while IFS= read -r ref_tag; do
|
||||
[[ -z "$ref_tag" ]] && continue
|
||||
|
||||
local ref_path expected_hash
|
||||
ref_path=$(echo "$ref_tag" | jq -r '.[0]' 2>/dev/null || echo "")
|
||||
expected_hash=$(echo "$ref_tag" | jq -r '.[1]' 2>/dev/null || echo "")
|
||||
|
||||
# Skip if not a heads ref or hash is missing
|
||||
[[ ! "$ref_path" =~ ^refs/heads/ ]] && continue
|
||||
[[ -z "$expected_hash" || "$expected_hash" == "null" ]] && continue
|
||||
|
||||
# Get actual hash from git
|
||||
local actual_hash
|
||||
actual_hash=$(get_git_ref_hash "$git_dir" "$ref_path")
|
||||
|
||||
if [[ "$expected_hash" == "$actual_hash" ]]; then
|
||||
matching=$((matching + 1))
|
||||
fi
|
||||
done < <(echo "$event" | jq -c '.tags[] | select(.[0] | startswith("refs/heads/"))' 2>/dev/null)
|
||||
|
||||
echo "$matching"
|
||||
}
|
||||
|
||||
# Categorize a single entry
|
||||
# Args: $1=state_refs, $2=git_refs, $3=matches, $4=reason
|
||||
# Returns: category number (1-4)
|
||||
categorize_entry() {
|
||||
local state_refs="$1"
|
||||
local git_refs="$2"
|
||||
local matches="$3"
|
||||
local reason="$4"
|
||||
|
||||
# Category 2: Empty/Blank
|
||||
if [[ -n "$reason" ]] || [[ "$git_refs" -eq 0 ]]; then
|
||||
echo "2"
|
||||
return
|
||||
fi
|
||||
|
||||
# Category 1: Complete Match
|
||||
if [[ "$state_refs" -gt 0 ]] && [[ "$state_refs" -eq "$git_refs" ]] && [[ "$matches" -eq "$state_refs" ]]; then
|
||||
echo "1"
|
||||
return
|
||||
fi
|
||||
|
||||
# Category 4: No Match
|
||||
if [[ "$git_refs" -gt 0 ]] && [[ "$matches" -eq 0 ]]; then
|
||||
echo "4"
|
||||
return
|
||||
fi
|
||||
|
||||
# Category 3: Partial Match (default for anything else with matches > 0)
|
||||
if [[ "$matches" -gt 0 ]]; then
|
||||
echo "3"
|
||||
return
|
||||
fi
|
||||
|
||||
# Fallback to category 2
|
||||
echo "2"
|
||||
}
|
||||
|
||||
# Format entry for category file
|
||||
# Args: $1=repo, $2=npub, $3=state_refs, $4=git_refs, $5=matches, $6=reason
|
||||
format_category_line() {
|
||||
local repo="$1"
|
||||
local npub="$2"
|
||||
local state_refs="$3"
|
||||
local git_refs="$4"
|
||||
local matches="$5"
|
||||
local reason="$6"
|
||||
|
||||
if [[ -n "$reason" ]]; then
|
||||
echo "$repo | $npub | state_refs=$state_refs | git_refs=$git_refs | matches=$matches | reason=$reason"
|
||||
else
|
||||
echo "$repo | $npub | state_refs=$state_refs | git_refs=$git_refs | matches=$matches"
|
||||
fi
|
||||
}
|
||||
|
||||
# Process a single state event
|
||||
# Args: $1=event_json, $2=git_base
|
||||
# Outputs: TSV line to stdout
|
||||
process_event() {
|
||||
local event="$1"
|
||||
local git_base="$2"
|
||||
|
||||
# Extract repository identifier (d tag)
|
||||
local identifier
|
||||
identifier=$(echo "$event" | jq -r '.tags[] | select(.[0] == "d") | .[1]' 2>/dev/null | head -1 || echo "")
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract maintainer pubkey (hex)
|
||||
local hex_pubkey
|
||||
hex_pubkey=$(echo "$event" | jq -r '.pubkey' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$hex_pubkey" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Convert to npub
|
||||
local npub
|
||||
npub=$(hex_to_npub "$hex_pubkey")
|
||||
|
||||
if [[ -z "$npub" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Count state refs
|
||||
local state_refs
|
||||
state_refs=$(count_state_refs "$event")
|
||||
|
||||
# Find git directory
|
||||
local git_dir="$git_base/${npub}/${identifier}.git"
|
||||
|
||||
# Check git directory status
|
||||
local git_refs=0
|
||||
local matches=0
|
||||
local reason=""
|
||||
|
||||
if [[ ! -d "$git_dir" ]]; then
|
||||
reason="no_git_dir"
|
||||
elif [[ ! -d "$git_dir/refs/heads" ]] && [[ ! -f "$git_dir/packed-refs" ]]; then
|
||||
reason="empty_refs"
|
||||
else
|
||||
git_refs=$(count_git_refs "$git_dir")
|
||||
|
||||
if [[ "$git_refs" -eq 0 ]]; then
|
||||
reason="empty_refs"
|
||||
elif [[ "$state_refs" -eq 0 ]]; then
|
||||
reason="no_state_refs"
|
||||
else
|
||||
matches=$(count_matching_refs "$event" "$git_dir")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Output TSV line: repo, npub, state_refs, git_refs, matches, reason
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$identifier" "$npub" "$state_refs" "$git_refs" "$matches" "$reason"
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
local do_categorize=0
|
||||
local args=()
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--categorize" ]]; then
|
||||
do_categorize=1
|
||||
else
|
||||
args+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#args[@]} -ne 3 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local state_events_file="${args[0]}"
|
||||
local git_base="${args[1]}"
|
||||
local output_dir="${args[2]}"
|
||||
|
||||
# Validate inputs
|
||||
if [[ ! -f "$state_events_file" ]]; then
|
||||
log_error "State events file not found: $state_events_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$git_base" ]]; then
|
||||
log_error "Git base directory not found: $git_base"
|
||||
log_error "This script must run on the VPS with access to git directories."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check read permissions
|
||||
if ! ls "$git_base" >/dev/null 2>&1; then
|
||||
log_error "Cannot read git base directory (permission denied): $git_base"
|
||||
log_error "Try running with sudo or grant read permissions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_prerequisites
|
||||
|
||||
log_info "=== Git State Synchronization Check ==="
|
||||
log_info "State events: $state_events_file"
|
||||
log_info "Git base: $git_base"
|
||||
log_info "Output: $output_dir"
|
||||
if [[ $do_categorize -eq 1 ]]; then
|
||||
log_info "Mode: TSV + categorization"
|
||||
else
|
||||
log_info "Mode: TSV only (use 20-categorize.sh for categories)"
|
||||
fi
|
||||
log_info "Started: $(date)"
|
||||
echo ""
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Output files
|
||||
local tsv_file="$output_dir/git-sync-status.tsv"
|
||||
|
||||
# Initialize TSV with header
|
||||
echo -e "repo\tnpub\tstate_refs\tgit_refs\tmatches\treason" > "$tsv_file"
|
||||
|
||||
# Initialize category files if categorizing
|
||||
local cat1="" cat2="" cat3="" cat4=""
|
||||
if [[ $do_categorize -eq 1 ]]; then
|
||||
cat1="$output_dir/category1-complete-match.txt"
|
||||
cat2="$output_dir/category2-empty-blank.txt"
|
||||
cat3="$output_dir/category3-partial-match.txt"
|
||||
cat4="$output_dir/category4-no-match.txt"
|
||||
> "$cat1"
|
||||
> "$cat2"
|
||||
> "$cat3"
|
||||
> "$cat4"
|
||||
fi
|
||||
|
||||
# Count total events
|
||||
local total_events
|
||||
total_events=$(wc -l < "$state_events_file" | tr -d ' ')
|
||||
log_info "Processing $total_events state events..."
|
||||
echo ""
|
||||
|
||||
# Process each event
|
||||
local count=0
|
||||
local processed=0
|
||||
local skipped=0
|
||||
local count_cat1=0 count_cat2=0 count_cat3=0 count_cat4=0
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
while IFS= read -r event; do
|
||||
count=$((count + 1))
|
||||
|
||||
# Skip empty lines
|
||||
[[ -z "$event" ]] && continue
|
||||
|
||||
# Process event
|
||||
local result
|
||||
if result=$(process_event "$event" "$git_base"); then
|
||||
processed=$((processed + 1))
|
||||
|
||||
# Write to TSV (skip header line)
|
||||
echo "$result" >> "$tsv_file"
|
||||
|
||||
# Categorize if requested
|
||||
if [[ $do_categorize -eq 1 ]]; then
|
||||
# Parse result
|
||||
IFS=$'\t' read -r repo npub state_refs git_refs matches reason <<< "$result"
|
||||
|
||||
local category
|
||||
category=$(categorize_entry "$state_refs" "$git_refs" "$matches" "$reason")
|
||||
|
||||
local cat_line
|
||||
cat_line=$(format_category_line "$repo" "$npub" "$state_refs" "$git_refs" "$matches" "$reason")
|
||||
|
||||
case "$category" in
|
||||
1) echo "$cat_line" >> "$cat1"; count_cat1=$((count_cat1 + 1)) ;;
|
||||
2) echo "$cat_line" >> "$cat2"; count_cat2=$((count_cat2 + 1)) ;;
|
||||
3) echo "$cat_line" >> "$cat3"; count_cat3=$((count_cat3 + 1)) ;;
|
||||
4) echo "$cat_line" >> "$cat4"; count_cat4=$((count_cat4 + 1)) ;;
|
||||
esac
|
||||
fi
|
||||
else
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
|
||||
# Progress indicator every 10 events
|
||||
if [[ $((count % 10)) -eq 0 ]]; then
|
||||
local elapsed=$(($(date +%s) - start_time))
|
||||
local rate=0
|
||||
if [[ $elapsed -gt 0 ]]; then
|
||||
rate=$((count / elapsed))
|
||||
fi
|
||||
local eta="?"
|
||||
if [[ $rate -gt 0 ]]; then
|
||||
eta=$(( (total_events - count) / rate ))
|
||||
fi
|
||||
log_progress "Processed $count/$total_events events (~${rate}/s, ETA: ${eta}s)..."
|
||||
fi
|
||||
done < "$state_events_file"
|
||||
|
||||
# Clear progress line
|
||||
echo "" >&2
|
||||
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
log_info "=== Analysis Complete ==="
|
||||
log_info "Finished: $(date)"
|
||||
log_info "Duration: ${duration}s"
|
||||
log_info "Processed: $processed events"
|
||||
if [[ $skipped -gt 0 ]]; then
|
||||
log_warn "Skipped: $skipped events (missing identifier or pubkey)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [[ $do_categorize -eq 1 ]]; then
|
||||
# Calculate percentages
|
||||
local total=$((count_cat1 + count_cat2 + count_cat3 + count_cat4))
|
||||
local pct1=0 pct2=0 pct3=0 pct4=0
|
||||
if [[ $total -gt 0 ]]; then
|
||||
pct1=$(awk "BEGIN {printf \"%.1f\", ($count_cat1/$total)*100}")
|
||||
pct2=$(awk "BEGIN {printf \"%.1f\", ($count_cat2/$total)*100}")
|
||||
pct3=$(awk "BEGIN {printf \"%.1f\", ($count_cat3/$total)*100}")
|
||||
pct4=$(awk "BEGIN {printf \"%.1f\", ($count_cat4/$total)*100}")
|
||||
fi
|
||||
|
||||
log_info "=== Category Summary ==="
|
||||
log_success "Category 1 (Complete Match): $count_cat1 ($pct1%)"
|
||||
log_warn "Category 2 (Empty/Blank): $count_cat2 ($pct2%)"
|
||||
log_warn "Category 3 (Partial Match): $count_cat3 ($pct3%)"
|
||||
log_error "Category 4 (No Match): $count_cat4 ($pct4%)"
|
||||
echo ""
|
||||
|
||||
# Validation warning
|
||||
if [[ $count_cat2 -eq $total ]] && [[ $total -gt 0 ]]; then
|
||||
log_error "WARNING: 100% of repos categorized as Empty/Blank"
|
||||
log_error "This usually indicates a permission or path issue."
|
||||
echo ""
|
||||
log_info "Troubleshooting:"
|
||||
echo " 1. Verify git data exists: sudo ls -la $git_base | head -10"
|
||||
echo " 2. Check sample repo: sudo find $git_base -name '*.git' -type d | head -1"
|
||||
echo " 3. Re-run with sudo if not already using it"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Output files:"
|
||||
echo " $tsv_file"
|
||||
if [[ $do_categorize -eq 1 ]]; then
|
||||
echo " $cat1"
|
||||
echo " $cat2"
|
||||
echo " $cat3"
|
||||
echo " $cat4"
|
||||
else
|
||||
echo ""
|
||||
log_info "Next step: Run 20-categorize.sh to categorize results"
|
||||
echo " ./20-categorize.sh $tsv_file $output_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,212 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 20-categorize.sh - Categorize git sync status into 4 categories
|
||||
#
|
||||
# PHASE 3a of the GRASP relay to ngit-grasp migration analysis pipeline.
|
||||
# Takes git-sync-status.tsv from Phase 2 and categorizes into 4 files.
|
||||
#
|
||||
# USAGE:
|
||||
# ./20-categorize.sh <git-sync-status.tsv> <output-dir>
|
||||
#
|
||||
# EXAMPLES:
|
||||
# ./20-categorize.sh output/prod/git-sync-status.tsv output/prod
|
||||
# ./20-categorize.sh output/archive/git-sync-status.tsv output/archive
|
||||
#
|
||||
# INPUT FORMAT (git-sync-status.tsv):
|
||||
# Tab-separated values with columns:
|
||||
# repo<TAB>npub<TAB>state_refs<TAB>git_refs<TAB>matches<TAB>reason
|
||||
#
|
||||
# Where reason is optional and can be: no_git_dir, empty_refs, no_state_refs
|
||||
#
|
||||
# OUTPUT:
|
||||
# <output-dir>/category1-complete-match.txt - All refs match perfectly
|
||||
# <output-dir>/category2-empty-blank.txt - No git data available
|
||||
# <output-dir>/category3-partial-match.txt - Some refs match
|
||||
# <output-dir>/category4-no-match.txt - Git exists but refs don't match
|
||||
#
|
||||
# OUTPUT FORMAT:
|
||||
# repo | npub | state_refs=N | git_refs=N | matches=N [| reason=X]
|
||||
#
|
||||
# CATEGORIES:
|
||||
# 1. Complete Match: state_refs == git_refs == matches (all > 0)
|
||||
# 2. Empty/Blank: git_refs == 0 OR reason in (no_git_dir, empty_refs, no_state_refs)
|
||||
# 3. Partial Match: matches > 0 AND matches < state_refs
|
||||
# 4. No Match: git_refs > 0 AND matches == 0
|
||||
#
|
||||
# PREREQUISITES:
|
||||
# - awk (standard Unix tool)
|
||||
#
|
||||
# RUNTIME: < 1 second (local processing only)
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
# 10-check-git-sync.sh - Phase 2 script that produces input for this script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <git-sync-status.tsv> <output-dir>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " git-sync-status.tsv TSV file from Phase 2 (10-check-git-sync.sh)"
|
||||
echo " output-dir Directory to store categorized output"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 output/prod/git-sync-status.tsv output/prod"
|
||||
echo " $0 output/archive/git-sync-status.tsv output/archive"
|
||||
echo ""
|
||||
echo "Input format (TSV):"
|
||||
echo " repo<TAB>npub<TAB>state_refs<TAB>git_refs<TAB>matches<TAB>reason"
|
||||
echo ""
|
||||
echo "Output files:"
|
||||
echo " category1-complete-match.txt - All refs match"
|
||||
echo " category2-empty-blank.txt - No git data"
|
||||
echo " category3-partial-match.txt - Some refs match"
|
||||
echo " category4-no-match.txt - Git exists, refs don't match"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
if [[ $# -ne 2 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local input_file="$1"
|
||||
local output_dir="$2"
|
||||
|
||||
# Validate input file
|
||||
if [[ ! -f "$input_file" ]]; then
|
||||
log_error "Input file not found: $input_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Categorizing git sync status"
|
||||
log_info "Input: $input_file"
|
||||
log_info "Output: $output_dir"
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Output files
|
||||
local cat1="$output_dir/category1-complete-match.txt"
|
||||
local cat2="$output_dir/category2-empty-blank.txt"
|
||||
local cat3="$output_dir/category3-partial-match.txt"
|
||||
local cat4="$output_dir/category4-no-match.txt"
|
||||
|
||||
# Clear previous results
|
||||
> "$cat1"
|
||||
> "$cat2"
|
||||
> "$cat3"
|
||||
> "$cat4"
|
||||
|
||||
# Process input file with awk
|
||||
# Input: repo<TAB>npub<TAB>state_refs<TAB>git_refs<TAB>matches<TAB>reason
|
||||
awk -F'\t' -v cat1="$cat1" -v cat2="$cat2" -v cat3="$cat3" -v cat4="$cat4" '
|
||||
BEGIN {
|
||||
count1 = 0; count2 = 0; count3 = 0; count4 = 0
|
||||
}
|
||||
NR == 1 && /^repo/ { next } # Skip header if present
|
||||
NF >= 5 {
|
||||
repo = $1
|
||||
npub = $2
|
||||
state_refs = int($3)
|
||||
git_refs = int($4)
|
||||
matches = int($5)
|
||||
reason = (NF >= 6) ? $6 : ""
|
||||
|
||||
# Format output line
|
||||
if (reason != "") {
|
||||
line = repo " | " npub " | state_refs=" state_refs " | git_refs=" git_refs " | matches=" matches " | reason=" reason
|
||||
} else {
|
||||
line = repo " | " npub " | state_refs=" state_refs " | git_refs=" git_refs " | matches=" matches
|
||||
}
|
||||
|
||||
# Categorize
|
||||
if (reason == "no_git_dir" || reason == "empty_refs" || reason == "no_state_refs" || git_refs == 0) {
|
||||
# Category 2: Empty/Blank
|
||||
print line >> cat2
|
||||
count2++
|
||||
} else if (state_refs > 0 && state_refs == git_refs && matches == state_refs) {
|
||||
# Category 1: Complete Match
|
||||
print line >> cat1
|
||||
count1++
|
||||
} else if (matches > 0 && matches < state_refs) {
|
||||
# Category 3: Partial Match
|
||||
print line >> cat3
|
||||
count3++
|
||||
} else if (git_refs > 0 && matches == 0) {
|
||||
# Category 4: No Match
|
||||
print line >> cat4
|
||||
count4++
|
||||
} else if (matches > 0) {
|
||||
# Edge case: matches > 0 but does not fit other categories
|
||||
# This can happen when git_refs > state_refs but all state refs match
|
||||
# Treat as partial match
|
||||
print line >> cat3
|
||||
count3++
|
||||
} else {
|
||||
# Fallback: treat as category 2 (empty/blank)
|
||||
print line >> cat2
|
||||
count2++
|
||||
}
|
||||
}
|
||||
END {
|
||||
total = count1 + count2 + count3 + count4
|
||||
print "COUNTS:" count1 ":" count2 ":" count3 ":" count4 ":" total
|
||||
}
|
||||
' "$input_file" 2>&1 | while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^COUNTS: ]]; then
|
||||
# Parse counts from awk output
|
||||
IFS=':' read -r _ c1 c2 c3 c4 total <<< "$line"
|
||||
|
||||
echo ""
|
||||
log_info "=== Categorization Summary ==="
|
||||
log_info "Total entries: $total"
|
||||
log_success "Category 1 (Complete Match): $c1"
|
||||
log_warn "Category 2 (Empty/Blank): $c2"
|
||||
log_warn "Category 3 (Partial Match): $c3"
|
||||
log_error "Category 4 (No Match): $c4"
|
||||
echo ""
|
||||
log_info "Output files:"
|
||||
echo " $cat1"
|
||||
echo " $cat2"
|
||||
echo " $cat3"
|
||||
echo " $cat4"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,294 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 21-compare-relays.sh - Compare prod vs archive category files to find gaps
|
||||
#
|
||||
# PHASE 3b of the GRASP relay to ngit-grasp migration analysis pipeline.
|
||||
# Compares categorized output from prod and archive to identify:
|
||||
# - Repos complete in prod but missing/incomplete in archive
|
||||
# - Repos in archive but not in prod
|
||||
# - Status differences between relays
|
||||
#
|
||||
# USAGE:
|
||||
# ./21-compare-relays.sh <prod-dir> <archive-dir> <output-dir>
|
||||
#
|
||||
# EXAMPLES:
|
||||
# ./21-compare-relays.sh output/prod output/archive output/comparison
|
||||
#
|
||||
# INPUT:
|
||||
# Both prod-dir and archive-dir must contain:
|
||||
# - category1-complete-match.txt
|
||||
# - category2-empty-blank.txt
|
||||
# - category3-partial-match.txt
|
||||
# - category4-no-match.txt
|
||||
#
|
||||
# OUTPUT:
|
||||
# <output-dir>/complete-in-both.txt - Repos complete in both relays (no action)
|
||||
# <output-dir>/complete-prod-missing-archive.txt - Complete in prod, not in archive cat1
|
||||
# <output-dir>/complete-prod-incomplete-archive.txt - Complete in prod, incomplete in archive
|
||||
# <output-dir>/incomplete-in-both.txt - Incomplete in both relays
|
||||
# <output-dir>/in-archive-not-prod.txt - In archive but not in prod
|
||||
# <output-dir>/summary.txt - Human-readable summary
|
||||
#
|
||||
# OUTPUT FORMAT:
|
||||
# Each file contains lines in the format:
|
||||
# repo | npub | prod_status | archive_status
|
||||
#
|
||||
# PREREQUISITES:
|
||||
# - awk, sort, comm (standard Unix tools)
|
||||
#
|
||||
# RUNTIME: < 1 second (local processing only)
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
# 20-categorize.sh - Phase 3a script that produces input for this script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <prod-dir> <archive-dir> <output-dir>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " prod-dir Directory containing prod category files"
|
||||
echo " archive-dir Directory containing archive category files"
|
||||
echo " output-dir Directory to store comparison results"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 output/prod output/archive output/comparison"
|
||||
echo ""
|
||||
echo "Required input files in each directory:"
|
||||
echo " category1-complete-match.txt"
|
||||
echo " category2-empty-blank.txt"
|
||||
echo " category3-partial-match.txt"
|
||||
echo " category4-no-match.txt"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract repo|npub key from category line
|
||||
# Input: "repo | npub | state_refs=N | ..."
|
||||
# Output: "repo|npub"
|
||||
extract_key() {
|
||||
awk -F' \\| ' '{print $1 "|" $2}'
|
||||
}
|
||||
|
||||
# Build lookup table from category files
|
||||
# Args: $1=directory, $2=output_file
|
||||
build_lookup() {
|
||||
local dir="$1"
|
||||
local output="$2"
|
||||
|
||||
# Process all 4 category files
|
||||
for cat in 1 2 3 4; do
|
||||
local file="$dir/category${cat}-*.txt"
|
||||
# shellcheck disable=SC2086
|
||||
if ls $file 1>/dev/null 2>&1; then
|
||||
# shellcheck disable=SC2086
|
||||
cat $file | while IFS= read -r line; do
|
||||
key=$(echo "$line" | extract_key)
|
||||
echo "${key}|cat${cat}|${line}"
|
||||
done
|
||||
fi
|
||||
done | sort -t'|' -k1,2 > "$output"
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
if [[ $# -ne 3 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local prod_dir="$1"
|
||||
local archive_dir="$2"
|
||||
local output_dir="$3"
|
||||
|
||||
# Validate input directories
|
||||
for dir in "$prod_dir" "$archive_dir"; do
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
log_error "Directory not found: $dir"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$dir/category1-complete-match.txt" ]]; then
|
||||
log_error "Missing category1-complete-match.txt in $dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Comparing relay categories"
|
||||
log_info "Prod: $prod_dir"
|
||||
log_info "Archive: $archive_dir"
|
||||
log_info "Output: $output_dir"
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Create temp files for processing
|
||||
local tmp_dir
|
||||
tmp_dir=$(mktemp -d)
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '$tmp_dir'" EXIT
|
||||
|
||||
log_info "Building lookup tables..."
|
||||
|
||||
# Build lookup tables: key|category|full_line
|
||||
build_lookup "$prod_dir" "$tmp_dir/prod_lookup.txt"
|
||||
build_lookup "$archive_dir" "$tmp_dir/archive_lookup.txt"
|
||||
|
||||
# Extract just keys for comparison
|
||||
cut -d'|' -f1,2 "$tmp_dir/prod_lookup.txt" | sort -u > "$tmp_dir/prod_keys.txt"
|
||||
cut -d'|' -f1,2 "$tmp_dir/archive_lookup.txt" | sort -u > "$tmp_dir/archive_keys.txt"
|
||||
|
||||
log_info "Comparing categories..."
|
||||
|
||||
# Initialize output files
|
||||
> "$output_dir/complete-in-both.txt"
|
||||
> "$output_dir/complete-prod-missing-archive.txt"
|
||||
> "$output_dir/complete-prod-incomplete-archive.txt"
|
||||
> "$output_dir/incomplete-in-both.txt"
|
||||
> "$output_dir/in-archive-not-prod.txt"
|
||||
|
||||
# Process prod category 1 (complete) entries
|
||||
while IFS='|' read -r repo npub cat full_line; do
|
||||
key="${repo}|${npub}"
|
||||
|
||||
# Look up in archive
|
||||
archive_entry=$(grep "^${key}|" "$tmp_dir/archive_lookup.txt" 2>/dev/null | head -1 || echo "")
|
||||
|
||||
if [[ -z "$archive_entry" ]]; then
|
||||
# Not in archive at all
|
||||
echo "$repo | $npub | prod=complete | archive=missing" >> "$output_dir/complete-prod-missing-archive.txt"
|
||||
else
|
||||
archive_cat=$(echo "$archive_entry" | cut -d'|' -f3)
|
||||
if [[ "$archive_cat" == "cat1" ]]; then
|
||||
# Complete in both
|
||||
echo "$repo | $npub | prod=complete | archive=complete" >> "$output_dir/complete-in-both.txt"
|
||||
else
|
||||
# Complete in prod, incomplete in archive
|
||||
echo "$repo | $npub | prod=complete | archive=$archive_cat" >> "$output_dir/complete-prod-incomplete-archive.txt"
|
||||
fi
|
||||
fi
|
||||
done < <(grep '|cat1|' "$tmp_dir/prod_lookup.txt" | sed 's/|cat1|/|cat1|/')
|
||||
|
||||
# Process prod categories 2-4 (incomplete) entries
|
||||
for cat in cat2 cat3 cat4; do
|
||||
while IFS='|' read -r repo npub _ full_line; do
|
||||
key="${repo}|${npub}"
|
||||
|
||||
# Look up in archive
|
||||
archive_entry=$(grep "^${key}|" "$tmp_dir/archive_lookup.txt" 2>/dev/null | head -1 || echo "")
|
||||
|
||||
if [[ -z "$archive_entry" ]]; then
|
||||
# Incomplete in prod, missing in archive
|
||||
echo "$repo | $npub | prod=$cat | archive=missing" >> "$output_dir/incomplete-in-both.txt"
|
||||
else
|
||||
archive_cat=$(echo "$archive_entry" | cut -d'|' -f3)
|
||||
if [[ "$archive_cat" != "cat1" ]]; then
|
||||
# Incomplete in both
|
||||
echo "$repo | $npub | prod=$cat | archive=$archive_cat" >> "$output_dir/incomplete-in-both.txt"
|
||||
fi
|
||||
# If archive is complete but prod is not, that's unusual but not an error
|
||||
fi
|
||||
done < <(grep "|${cat}|" "$tmp_dir/prod_lookup.txt")
|
||||
done
|
||||
|
||||
# Find entries in archive but not in prod
|
||||
comm -23 "$tmp_dir/archive_keys.txt" "$tmp_dir/prod_keys.txt" | while IFS='|' read -r repo npub; do
|
||||
key="${repo}|${npub}"
|
||||
archive_entry=$(grep "^${key}|" "$tmp_dir/archive_lookup.txt" 2>/dev/null | head -1 || echo "")
|
||||
archive_cat=$(echo "$archive_entry" | cut -d'|' -f3)
|
||||
echo "$repo | $npub | prod=missing | archive=$archive_cat" >> "$output_dir/in-archive-not-prod.txt"
|
||||
done
|
||||
|
||||
# Count results
|
||||
local count_both count_missing count_incomplete count_both_incomplete count_archive_only
|
||||
count_both=$(wc -l < "$output_dir/complete-in-both.txt" | tr -d ' ')
|
||||
count_missing=$(wc -l < "$output_dir/complete-prod-missing-archive.txt" | tr -d ' ')
|
||||
count_incomplete=$(wc -l < "$output_dir/complete-prod-incomplete-archive.txt" | tr -d ' ')
|
||||
count_both_incomplete=$(wc -l < "$output_dir/incomplete-in-both.txt" | tr -d ' ')
|
||||
count_archive_only=$(wc -l < "$output_dir/in-archive-not-prod.txt" | tr -d ' ')
|
||||
|
||||
# Generate summary
|
||||
cat > "$output_dir/summary.txt" << EOF
|
||||
# Relay Comparison Summary
|
||||
Generated: $(date -Iseconds)
|
||||
|
||||
## Input
|
||||
- Prod: $prod_dir
|
||||
- Archive: $archive_dir
|
||||
|
||||
## Results
|
||||
|
||||
### No Action Required
|
||||
- Complete in both relays: $count_both
|
||||
|
||||
### Action/Decision Required
|
||||
- Complete in prod, MISSING from archive: $count_missing
|
||||
- Complete in prod, INCOMPLETE in archive: $count_incomplete
|
||||
- Incomplete in BOTH relays: $count_both_incomplete
|
||||
|
||||
### For Reference
|
||||
- In archive but not in prod: $count_archive_only
|
||||
|
||||
## Files
|
||||
- complete-in-both.txt: Repos successfully migrated (no action)
|
||||
- complete-prod-missing-archive.txt: Need investigation - why not in archive?
|
||||
- complete-prod-incomplete-archive.txt: Archive sync may still be in progress
|
||||
- incomplete-in-both.txt: Git data incomplete on both relays
|
||||
- in-archive-not-prod.txt: May be deleted from prod or new to archive
|
||||
|
||||
## Next Steps
|
||||
1. Review complete-prod-missing-archive.txt - these repos need attention
|
||||
2. Check if archive sync is still running for incomplete entries
|
||||
3. Cross-reference with deletion events (kind 5) from Phase 1
|
||||
4. Use Phase 4 logs to understand parse failures and purgatory expiry
|
||||
EOF
|
||||
|
||||
# Display summary
|
||||
echo ""
|
||||
log_info "=== Comparison Summary ==="
|
||||
log_success "Complete in both: $count_both (no action needed)"
|
||||
log_error "Complete in prod, MISSING from archive: $count_missing"
|
||||
log_warn "Complete in prod, incomplete in archive: $count_incomplete"
|
||||
log_warn "Incomplete in both: $count_both_incomplete"
|
||||
log_info "In archive only: $count_archive_only"
|
||||
echo ""
|
||||
log_info "Output files:"
|
||||
echo " $output_dir/complete-in-both.txt"
|
||||
echo " $output_dir/complete-prod-missing-archive.txt"
|
||||
echo " $output_dir/complete-prod-incomplete-archive.txt"
|
||||
echo " $output_dir/incomplete-in-both.txt"
|
||||
echo " $output_dir/in-archive-not-prod.txt"
|
||||
echo " $output_dir/summary.txt"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,390 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 22-compare-git-data.sh - Compare actual git data between prod and archive relays
|
||||
#
|
||||
# PHASE 3c of the GRASP relay to ngit-grasp migration analysis pipeline.
|
||||
# Compares actual git commits between prod and archive to determine which is ahead.
|
||||
#
|
||||
# KEY INSIGHT:
|
||||
# Archive (ngit-grasp) enforces GRASP - git data ALWAYS matches a state event.
|
||||
# If archive has different/newer data than prod, it means:
|
||||
# - A state event authorized those commits at some point
|
||||
# - Archive is actually MORE up-to-date than prod
|
||||
# - Migration should use archive data (it's already correct)
|
||||
#
|
||||
# USAGE:
|
||||
# ./22-compare-git-data.sh <prod-git-base> <archive-git-base> <repo-list> <output-dir>
|
||||
#
|
||||
# EXAMPLES:
|
||||
# ./22-compare-git-data.sh /var/lib/grasp-relay/git /var/lib/ngit-grasp/git \
|
||||
# output/comparison/complete-prod-incomplete-archive.txt output/comparison
|
||||
#
|
||||
# INPUT:
|
||||
# prod-git-base Base directory for prod git repos (e.g., /var/lib/grasp-relay/git)
|
||||
# archive-git-base Base directory for archive git repos (e.g., /var/lib/ngit-grasp/git)
|
||||
# repo-list File with repos to compare (format: "repo | npub | ...")
|
||||
#
|
||||
# OUTPUT:
|
||||
# <output-dir>/git-ancestry.tsv - Tab-separated values:
|
||||
# repo<TAB>npub<TAB>relationship<TAB>details
|
||||
#
|
||||
# Relationship values:
|
||||
# archive-ahead - Archive has all prod commits plus more (GOOD - use archive)
|
||||
# in-sync - Both have identical commits
|
||||
# prod-ahead - Prod has commits archive is missing (needs re-sync)
|
||||
# diverged - Both have unique commits (manual review)
|
||||
# archive-only - Only archive has git data
|
||||
# prod-only - Only prod has git data
|
||||
# both-empty - Neither has git data
|
||||
#
|
||||
# PREREQUISITES:
|
||||
# - git (for ref comparison)
|
||||
# - Read access to both git directories (may need sudo)
|
||||
#
|
||||
# RUNTIME: Depends on number of repos to compare
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
# 21-compare-relays.sh - Phase 3b script that identifies repos to compare
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_progress() {
|
||||
echo -ne "\r${BLUE}[PROGRESS]${NC} $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <prod-git-base> <archive-git-base> <repo-list> <output-dir>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " prod-git-base Base directory for prod git repos"
|
||||
echo " archive-git-base Base directory for archive git repos"
|
||||
echo " repo-list File with repos to compare (format: 'repo | npub | ...')"
|
||||
echo " output-dir Directory to store output files"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 /var/lib/grasp-relay/git /var/lib/ngit-grasp/git \\"
|
||||
echo " output/comparison/complete-prod-incomplete-archive.txt output/comparison"
|
||||
echo ""
|
||||
echo "Output:"
|
||||
echo " git-ancestry.tsv - TSV with: repo, npub, relationship, details"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get all branch refs from a git directory
|
||||
# Args: $1=git_dir
|
||||
# Returns: sorted list of "ref_name commit_hash" lines
|
||||
get_git_refs() {
|
||||
local git_dir="$1"
|
||||
|
||||
if [[ ! -d "$git_dir" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
git --git-dir="$git_dir" show-ref --heads 2>/dev/null | sort || true
|
||||
}
|
||||
|
||||
# Check if commit A is ancestor of commit B
|
||||
# Args: $1=git_dir, $2=commit_a, $3=commit_b
|
||||
# Returns: 0 if A is ancestor of B, 1 otherwise
|
||||
is_ancestor() {
|
||||
local git_dir="$1"
|
||||
local commit_a="$2"
|
||||
local commit_b="$3"
|
||||
|
||||
git --git-dir="$git_dir" merge-base --is-ancestor "$commit_a" "$commit_b" 2>/dev/null
|
||||
}
|
||||
|
||||
# Compare git data between prod and archive for a single repo
|
||||
# Args: $1=prod_git_dir, $2=archive_git_dir
|
||||
# Returns: relationship string
|
||||
compare_repo_git() {
|
||||
local prod_git="$1"
|
||||
local archive_git="$2"
|
||||
|
||||
local prod_exists=false
|
||||
local archive_exists=false
|
||||
|
||||
[[ -d "$prod_git" ]] && prod_exists=true
|
||||
[[ -d "$archive_git" ]] && archive_exists=true
|
||||
|
||||
# Handle cases where one or both don't exist
|
||||
if [[ "$prod_exists" == "false" && "$archive_exists" == "false" ]]; then
|
||||
echo "both-empty"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$prod_exists" == "false" ]]; then
|
||||
echo "archive-only"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$archive_exists" == "false" ]]; then
|
||||
echo "prod-only"
|
||||
return
|
||||
fi
|
||||
|
||||
# Both exist - get refs
|
||||
local prod_refs archive_refs
|
||||
prod_refs=$(get_git_refs "$prod_git")
|
||||
archive_refs=$(get_git_refs "$archive_git")
|
||||
|
||||
# Handle empty refs
|
||||
if [[ -z "$prod_refs" && -z "$archive_refs" ]]; then
|
||||
echo "both-empty"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "$prod_refs" ]]; then
|
||||
echo "archive-only"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "$archive_refs" ]]; then
|
||||
echo "prod-only"
|
||||
return
|
||||
fi
|
||||
|
||||
# Compare refs - check if they're identical
|
||||
if [[ "$prod_refs" == "$archive_refs" ]]; then
|
||||
echo "in-sync"
|
||||
return
|
||||
fi
|
||||
|
||||
# Refs differ - need to check ancestry
|
||||
# Strategy: For each branch, check if one is ancestor of the other
|
||||
# If all archive branches are ahead of or equal to prod branches, archive is ahead
|
||||
# If all prod branches are ahead of or equal to archive branches, prod is ahead
|
||||
# Otherwise, they've diverged
|
||||
|
||||
local archive_ahead=true
|
||||
local prod_ahead=true
|
||||
local has_common_branch=false
|
||||
|
||||
# Create temporary file to use archive as reference repo for ancestry checks
|
||||
# We need a repo that has both sets of commits to check ancestry
|
||||
# Use archive since it's the target and should have the superset
|
||||
|
||||
# Check each prod branch against archive
|
||||
while read -r prod_hash prod_ref; do
|
||||
[[ -z "$prod_hash" ]] && continue
|
||||
|
||||
# Get the same branch from archive
|
||||
local archive_hash
|
||||
archive_hash=$(echo "$archive_refs" | grep " $prod_ref$" | awk '{print $1}' || echo "")
|
||||
|
||||
if [[ -z "$archive_hash" ]]; then
|
||||
# Branch exists in prod but not archive - prod has something archive doesn't
|
||||
# But this could be a deleted branch, so don't immediately say prod is ahead
|
||||
continue
|
||||
fi
|
||||
|
||||
has_common_branch=true
|
||||
|
||||
if [[ "$prod_hash" == "$archive_hash" ]]; then
|
||||
# Same commit - neither ahead for this branch
|
||||
continue
|
||||
fi
|
||||
|
||||
# Different commits - check ancestry
|
||||
# First, try to check if prod is ancestor of archive (archive ahead)
|
||||
if is_ancestor "$archive_git" "$prod_hash" "$archive_hash" 2>/dev/null; then
|
||||
# Prod commit is ancestor of archive commit - archive is ahead for this branch
|
||||
prod_ahead=false
|
||||
elif is_ancestor "$archive_git" "$archive_hash" "$prod_hash" 2>/dev/null; then
|
||||
# Archive commit is ancestor of prod commit - prod is ahead for this branch
|
||||
archive_ahead=false
|
||||
else
|
||||
# Neither is ancestor - diverged
|
||||
archive_ahead=false
|
||||
prod_ahead=false
|
||||
fi
|
||||
done <<< "$prod_refs"
|
||||
|
||||
# Also check for branches only in archive (archive has extra branches)
|
||||
while read -r archive_hash archive_ref; do
|
||||
[[ -z "$archive_hash" ]] && continue
|
||||
|
||||
local prod_hash
|
||||
prod_hash=$(echo "$prod_refs" | grep " $archive_ref$" | awk '{print $1}' || echo "")
|
||||
|
||||
if [[ -z "$prod_hash" ]]; then
|
||||
# Branch exists in archive but not prod - archive has something prod doesn't
|
||||
# This means archive is ahead (has extra branches)
|
||||
prod_ahead=false
|
||||
fi
|
||||
done <<< "$archive_refs"
|
||||
|
||||
# Determine final relationship
|
||||
if [[ "$has_common_branch" == "false" ]]; then
|
||||
# No common branches - completely different
|
||||
echo "diverged"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$archive_ahead" == "true" && "$prod_ahead" == "false" ]]; then
|
||||
echo "archive-ahead"
|
||||
elif [[ "$prod_ahead" == "true" && "$archive_ahead" == "false" ]]; then
|
||||
echo "prod-ahead"
|
||||
elif [[ "$archive_ahead" == "true" && "$prod_ahead" == "true" ]]; then
|
||||
# Both true means all common branches are identical
|
||||
# But one might have extra branches
|
||||
echo "in-sync"
|
||||
else
|
||||
echo "diverged"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
if [[ $# -ne 4 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local prod_git_base="$1"
|
||||
local archive_git_base="$2"
|
||||
local repo_list="$3"
|
||||
local output_dir="$4"
|
||||
|
||||
# Validate inputs
|
||||
if [[ ! -d "$prod_git_base" ]]; then
|
||||
log_error "Prod git base directory not found: $prod_git_base"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$archive_git_base" ]]; then
|
||||
log_error "Archive git base directory not found: $archive_git_base"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$repo_list" ]]; then
|
||||
log_error "Repo list file not found: $repo_list"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "=== Git Data Comparison ==="
|
||||
log_info "Prod git base: $prod_git_base"
|
||||
log_info "Archive git base: $archive_git_base"
|
||||
log_info "Repo list: $repo_list"
|
||||
log_info "Output: $output_dir"
|
||||
log_info "Started: $(date)"
|
||||
echo ""
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Output file
|
||||
local tsv_file="$output_dir/git-ancestry.tsv"
|
||||
|
||||
# Initialize TSV with header
|
||||
echo -e "repo\tnpub\trelationship\tdetails" > "$tsv_file"
|
||||
|
||||
# Count repos
|
||||
local total_repos
|
||||
total_repos=$(grep -c -v '^#' "$repo_list" 2>/dev/null || echo "0")
|
||||
log_info "Processing $total_repos repos..."
|
||||
echo ""
|
||||
|
||||
# Counters
|
||||
local count=0
|
||||
local count_archive_ahead=0
|
||||
local count_in_sync=0
|
||||
local count_prod_ahead=0
|
||||
local count_diverged=0
|
||||
local count_archive_only=0
|
||||
local count_prod_only=0
|
||||
local count_both_empty=0
|
||||
|
||||
# Process each repo
|
||||
while IFS='|' read -r repo npub rest || [[ -n "$repo" ]]; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$repo" =~ ^# ]] && continue
|
||||
[[ -z "$repo" ]] && continue
|
||||
|
||||
# Clean up whitespace
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
|
||||
count=$((count + 1))
|
||||
|
||||
# Build git paths
|
||||
local prod_git="$prod_git_base/${npub}/${repo}.git"
|
||||
local archive_git="$archive_git_base/${npub}/${repo}.git"
|
||||
|
||||
# Compare
|
||||
local relationship details=""
|
||||
relationship=$(compare_repo_git "$prod_git" "$archive_git")
|
||||
|
||||
# Count by relationship
|
||||
case "$relationship" in
|
||||
archive-ahead) count_archive_ahead=$((count_archive_ahead + 1)) ;;
|
||||
in-sync) count_in_sync=$((count_in_sync + 1)) ;;
|
||||
prod-ahead) count_prod_ahead=$((count_prod_ahead + 1)) ;;
|
||||
diverged) count_diverged=$((count_diverged + 1)) ;;
|
||||
archive-only) count_archive_only=$((count_archive_only + 1)) ;;
|
||||
prod-only) count_prod_only=$((count_prod_only + 1)) ;;
|
||||
both-empty) count_both_empty=$((count_both_empty + 1)) ;;
|
||||
esac
|
||||
|
||||
# Output TSV line
|
||||
printf '%s\t%s\t%s\t%s\n' "$repo" "$npub" "$relationship" "$details" >> "$tsv_file"
|
||||
|
||||
# Progress indicator every 10 repos
|
||||
if [[ $((count % 10)) -eq 0 ]]; then
|
||||
log_progress "Processed $count/$total_repos repos..."
|
||||
fi
|
||||
done < "$repo_list"
|
||||
|
||||
# Clear progress line
|
||||
echo "" >&2
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
log_info "=== Comparison Summary ==="
|
||||
log_success "Archive ahead (use archive data): $count_archive_ahead"
|
||||
log_success "In sync: $count_in_sync"
|
||||
log_warn "Prod ahead (needs re-sync): $count_prod_ahead"
|
||||
log_error "Diverged (manual review): $count_diverged"
|
||||
log_info "Archive only: $count_archive_only"
|
||||
log_info "Prod only: $count_prod_only"
|
||||
log_info "Both empty: $count_both_empty"
|
||||
echo ""
|
||||
log_info "Total: $count repos"
|
||||
log_info "Output: $tsv_file"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,774 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 30-extract-parse-failures.sh - Extract parse failure events from systemd logs
|
||||
#
|
||||
# PHASE 4a of the GRASP relay to ngit-grasp migration analysis pipeline.
|
||||
# Extracts structured [PARSE_FAIL] log entries AND "Invalid announcement"
|
||||
# rejections from journalctl.
|
||||
#
|
||||
# USAGE:
|
||||
# ./30-extract-parse-failures.sh <service-name> <output-dir> [options]
|
||||
#
|
||||
# EXAMPLES:
|
||||
# # Extract from ngit-grasp service (last 30 days, default)
|
||||
# ./30-extract-parse-failures.sh ngit-grasp.service output/logs
|
||||
#
|
||||
# # Extract with custom time range
|
||||
# ./30-extract-parse-failures.sh ngit-grasp.service output/logs --since "2026-01-01"
|
||||
#
|
||||
# # Extract from specific time window
|
||||
# ./30-extract-parse-failures.sh ngit-grasp.service output/logs --since "2026-01-15" --until "2026-01-22"
|
||||
#
|
||||
# OPTIONS:
|
||||
# --since <date> Start date for log extraction (default: 30 days ago)
|
||||
# --until <date> End date for log extraction (default: now)
|
||||
# --dry-run Show what would be extracted without writing files
|
||||
#
|
||||
# ENRICHMENT:
|
||||
# The script automatically enriches parse failures with repo/npub information
|
||||
# by extracting from "Added rejected announcement" log entries which include
|
||||
# pubkey and identifier fields. Hex pubkeys are converted to npub format using
|
||||
# `nak encode npub <hex-pubkey>` if the nak tool is available.
|
||||
#
|
||||
# OUTPUT:
|
||||
# <output-dir>/parse-failures.txt
|
||||
#
|
||||
# OUTPUT FORMAT (TSV):
|
||||
# event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub
|
||||
#
|
||||
# EXPECTED LOG FORMATS:
|
||||
# The script looks for three types of log entries:
|
||||
#
|
||||
# 1. Structured [PARSE_FAIL] entries:
|
||||
# 2026-01-22T10:30:45Z ngit-grasp[1234]: [PARSE_FAIL] kind=30618 event_id=abc123... reason="invalid refs format" repo=myrepo npub=npub1...
|
||||
#
|
||||
# 2. "Invalid announcement" rejections (write policy):
|
||||
# Event rejected by write policy event_id=abc123... relay=wss://... kind=30617 reason=Invalid announcement: multiple clone tags found...
|
||||
#
|
||||
# 3. "Added rejected announcement" entries (for enrichment):
|
||||
# Added rejected announcement to two-tier index event_id=abc123... kind=30617 identifier=myrepo pubkey=hex...
|
||||
# These entries provide pubkey and identifier for enriching write policy rejections.
|
||||
#
|
||||
# NOTE: Builder logs ("Rejected repository announcement note1xxx:") are NOT extracted
|
||||
# because they use bech32 (note1) IDs while write policy logs use hex IDs. Extracting
|
||||
# both would cause double-counting since deduplication only works within each format.
|
||||
# Write policy logs contain the same events, so we don't lose any data.
|
||||
#
|
||||
# Required fields: kind, event_id, reason
|
||||
# Enrichment fields: repo (identifier), npub (converted from hex pubkey)
|
||||
#
|
||||
# DEPENDENCY:
|
||||
# This script requires logging improvements in ngit-grasp to emit structured
|
||||
# [PARSE_FAIL] log entries. Until those are implemented, this script will
|
||||
# find no matching entries (which is handled gracefully).
|
||||
#
|
||||
# "Invalid announcement" rejections are logged by the write policy and
|
||||
# should be present in any ngit-grasp deployment.
|
||||
#
|
||||
# See: docs/how-to/migrate-to-ngit-grasp.md (Dependencies section)
|
||||
#
|
||||
# Expected Rust logging code for [PARSE_FAIL]:
|
||||
# tracing::warn!(
|
||||
# target: "migration",
|
||||
# "[PARSE_FAIL] kind={} event_id={} reason=\"{}\" repo={} npub={}",
|
||||
# event.kind, event.id, reason, identifier, npub
|
||||
# );
|
||||
#
|
||||
# PREREQUISITES:
|
||||
# - journalctl (systemd)
|
||||
# - grep, awk, sed (standard Unix tools)
|
||||
# - Access to systemd journal (may require sudo or journal group membership)
|
||||
#
|
||||
# RUNTIME: Depends on log volume, typically < 30 seconds
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
# 31-extract-purgatory-expiry.sh - Companion script for purgatory expiry logs
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get script directory for sourcing helpers
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source the service validation helper
|
||||
if [[ -f "$SCRIPT_DIR/validate-service.sh" ]]; then
|
||||
source "$SCRIPT_DIR/validate-service.sh"
|
||||
fi
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <service-name> <output-dir> [options]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " service-name Systemd service name (e.g., ngit-grasp.service)"
|
||||
echo " output-dir Directory to store extracted log data"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --since <date> Start date (default: 30 days ago)"
|
||||
echo " --until <date> End date (default: now)"
|
||||
echo " --dry-run Show what would be extracted without writing"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 ngit-grasp.service output/logs"
|
||||
echo " $0 ngit-grasp.service output/logs --since '2026-01-01'"
|
||||
echo " $0 ngit-grasp.service output/logs --since '2026-01-15' --until '2026-01-22'"
|
||||
echo ""
|
||||
echo "Expected log formats:"
|
||||
echo " [PARSE_FAIL] kind=30618 event_id=abc123 reason=\"...\" repo=myrepo npub=npub1..."
|
||||
echo " Event rejected by write policy event_id=abc123 ... kind=30617 reason=Invalid announcement: ..."
|
||||
echo ""
|
||||
echo "Enrichment:"
|
||||
echo " Parse failures are automatically enriched with repo/npub from"
|
||||
echo " 'Added rejected announcement' log entries. Hex pubkeys are converted"
|
||||
echo " to npub format using 'nak encode npub' if available."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# AWK-BASED BATCH PARSING FUNCTIONS
|
||||
# =============================================================================
|
||||
# These functions use awk for efficient batch processing instead of per-line
|
||||
# grep calls. This provides ~400x speedup for large log files.
|
||||
#
|
||||
# NOTE: parse_builder_rejection_line() was removed to fix double-counting bug.
|
||||
# Builder logs use bech32 (note1) IDs while write policy logs use hex IDs.
|
||||
# Since deduplication only works within each format, extracting both caused
|
||||
# the same event to be counted twice. Write policy logs contain the same
|
||||
# events, so we don't lose any data by only extracting from that source.
|
||||
|
||||
# Parse [PARSE_FAIL] log lines in batch using awk
|
||||
# Input: file containing log lines with [PARSE_FAIL]
|
||||
# Output: TSV lines: event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub
|
||||
parse_parse_fail_batch() {
|
||||
local input_file="$1"
|
||||
awk '
|
||||
{
|
||||
# Extract kind=VALUE
|
||||
kind = ""
|
||||
if (match($0, /kind=([0-9]+)/, m)) kind = m[1]
|
||||
|
||||
# Extract event_id=VALUE (hex string)
|
||||
event_id = ""
|
||||
if (match($0, /event_id=([a-f0-9]+)/, m)) event_id = m[1]
|
||||
|
||||
# Extract reason="VALUE" (quoted string)
|
||||
reason = ""
|
||||
if (match($0, /reason="([^"]*)"/, m)) reason = m[1]
|
||||
|
||||
# Extract repo=VALUE (optional)
|
||||
repo = ""
|
||||
if (match($0, /repo=([^ ]+)/, m)) repo = m[1]
|
||||
|
||||
# Extract npub=VALUE (optional)
|
||||
npub = ""
|
||||
if (match($0, /npub=([^ ]+)/, m)) npub = m[1]
|
||||
|
||||
# Output if we have required fields
|
||||
if (kind != "" && event_id != "" && reason != "") {
|
||||
print event_id "\t" kind "\t" reason "\t" repo "\t" npub
|
||||
}
|
||||
}
|
||||
' "$input_file"
|
||||
}
|
||||
|
||||
# Parse "Invalid announcement" rejection log lines in batch using awk
|
||||
# Input: file containing "Event rejected by write policy" log lines
|
||||
# Output: TSV lines: event_id<TAB>kind<TAB>reason<TAB><empty><TAB><empty>
|
||||
parse_write_policy_rejection_batch() {
|
||||
local input_file="$1"
|
||||
awk '
|
||||
{
|
||||
# Extract event_id=VALUE (hex string)
|
||||
event_id = ""
|
||||
if (match($0, /event_id=([a-f0-9]+)/, m)) event_id = m[1]
|
||||
|
||||
# Extract kind=VALUE
|
||||
kind = ""
|
||||
if (match($0, /kind=([0-9]+)/, m)) kind = m[1]
|
||||
|
||||
# Extract reason=VALUE (everything after "reason=")
|
||||
reason = ""
|
||||
if (match($0, /reason=(.*)$/, m)) reason = m[1]
|
||||
|
||||
# Output if we have required fields (repo and npub are empty)
|
||||
if (kind != "" && event_id != "" && reason != "") {
|
||||
print event_id "\t" kind "\t" reason "\t\t"
|
||||
}
|
||||
}
|
||||
' "$input_file"
|
||||
}
|
||||
|
||||
# Parse "Added rejected announcement" log lines in batch using awk
|
||||
# Input: file containing "Added rejected announcement to two-tier index" log lines
|
||||
# Output: TSV lines: event_id<TAB>identifier<TAB>pubkey_hex
|
||||
parse_rejected_announcement_batch() {
|
||||
local input_file="$1"
|
||||
awk '
|
||||
{
|
||||
# Extract event_id=VALUE (hex string)
|
||||
event_id = ""
|
||||
if (match($0, /event_id=([a-f0-9]+)/, m)) event_id = m[1]
|
||||
|
||||
# Extract identifier=VALUE (repo name)
|
||||
identifier = ""
|
||||
if (match($0, /identifier=([^ ]+)/, m)) identifier = m[1]
|
||||
|
||||
# Extract pubkey=VALUE (hex string)
|
||||
pubkey = ""
|
||||
if (match($0, /pubkey=([a-f0-9]+)/, m)) pubkey = m[1]
|
||||
|
||||
# Output if we have all required fields
|
||||
if (event_id != "" && identifier != "" && pubkey != "") {
|
||||
print event_id "\t" identifier "\t" pubkey
|
||||
}
|
||||
}
|
||||
' "$input_file"
|
||||
}
|
||||
|
||||
# Enrich parse failures with repo/npub by looking up event_id in "Added rejected announcement" log entries
|
||||
# This is critical because "Invalid announcement" rejections only log event_id and kind,
|
||||
# not the repo name or npub. Without enrichment, Phase 5 shows event_id|kind instead
|
||||
# of repo|npub in action-required.txt, making the output unusable.
|
||||
#
|
||||
# Arguments:
|
||||
# $1 - parse failures file to enrich (modified in place)
|
||||
# $2 - lookup file containing event_id -> identifier|pubkey mappings from logs
|
||||
#
|
||||
# The function:
|
||||
# 1. Uses the lookup table built from "Added rejected announcement" log entries
|
||||
# 2. For each parse failure with empty repo/npub, looks up the event_id
|
||||
# 3. Populates repo and npub columns from the lookup
|
||||
# 4. Converts hex pubkeys to npub format using `nak encode npub` if available
|
||||
#
|
||||
# OPTIMIZATION: This function uses batch processing for efficiency:
|
||||
# - Uses awk for O(n) join instead of per-line grep (O(n*m))
|
||||
# - Batches all pubkey->npub conversions in a single nak call
|
||||
# - This reduces runtime from minutes to seconds for large datasets
|
||||
enrich_with_repo_npub() {
|
||||
local parse_failures_file="$1"
|
||||
local lookup_file="$2"
|
||||
|
||||
# Validate lookup file exists and has content
|
||||
if [[ ! -f "$lookup_file" ]] || [[ ! -s "$lookup_file" ]]; then
|
||||
log_warn "No enrichment data available - repo/npub columns will remain empty"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Enriching parse failures with repo/npub from log entries..."
|
||||
|
||||
# Check if we have nak for pubkey->npub conversion
|
||||
local can_convert_npub=false
|
||||
if command -v nak &> /dev/null; then
|
||||
can_convert_npub=true
|
||||
log_info " Using 'nak' for pubkey->npub conversion"
|
||||
else
|
||||
log_warn " 'nak' not found - will use hex pubkeys instead of npub"
|
||||
fi
|
||||
|
||||
local lookup_count
|
||||
lookup_count=$(wc -l < "$lookup_file")
|
||||
lookup_count="${lookup_count//[^0-9]/}"
|
||||
log_info " Lookup table has $lookup_count entries"
|
||||
|
||||
# STEP 1: Extract unique pubkeys that need conversion
|
||||
# Get pubkeys from lookup file (column 3), deduplicate
|
||||
local unique_pubkeys_file npub_map_file
|
||||
unique_pubkeys_file=$(mktemp)
|
||||
npub_map_file=$(mktemp)
|
||||
|
||||
cut -f3 "$lookup_file" | sort -u > "$unique_pubkeys_file"
|
||||
local unique_pubkey_count
|
||||
unique_pubkey_count=$(wc -l < "$unique_pubkeys_file")
|
||||
unique_pubkey_count="${unique_pubkey_count//[^0-9]/}"
|
||||
log_info " Converting $unique_pubkey_count unique pubkeys to npub format..."
|
||||
|
||||
# STEP 2: Batch convert all pubkeys to npub in a single nak call
|
||||
# nak reads hex pubkeys from stdin (one per line) and outputs npubs
|
||||
if [[ "$can_convert_npub" == true && "$unique_pubkey_count" -gt 0 ]]; then
|
||||
# Create mapping file: pubkey_hex<TAB>npub
|
||||
# nak encode npub reads from stdin and outputs one npub per line
|
||||
paste "$unique_pubkeys_file" <(nak encode npub < "$unique_pubkeys_file" 2>/dev/null) > "$npub_map_file" || {
|
||||
# Fallback: if batch conversion fails, use hex pubkeys
|
||||
log_warn " Batch npub conversion failed, using hex pubkeys"
|
||||
awk '{print $1 "\t" $1}' "$unique_pubkeys_file" > "$npub_map_file"
|
||||
}
|
||||
else
|
||||
# No nak available, use hex pubkeys as-is
|
||||
awk '{print $1 "\t" $1}' "$unique_pubkeys_file" > "$npub_map_file"
|
||||
fi
|
||||
|
||||
rm -f "$unique_pubkeys_file"
|
||||
|
||||
# STEP 3: Use awk for efficient join (O(n) instead of O(n*m) grep per line)
|
||||
# This joins parse_failures with lookup_file on event_id, then with npub_map on pubkey
|
||||
local enriched_file
|
||||
enriched_file=$(mktemp)
|
||||
|
||||
# Copy header lines
|
||||
grep '^#' "$parse_failures_file" > "$enriched_file" 2>/dev/null || true
|
||||
|
||||
# Use awk to perform the join efficiently
|
||||
# Input files (order matters for ARGIND):
|
||||
# 1. npub_map_file: pubkey_hex<TAB>npub
|
||||
# 2. lookup_file: event_id<TAB>identifier<TAB>pubkey_hex
|
||||
# 3. parse_failures_file: event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub
|
||||
awk -F'\t' -v OFS='\t' '
|
||||
# Track which file we are processing
|
||||
FNR==1 { file_num++ }
|
||||
|
||||
# First file: npub_map (pubkey_hex -> npub)
|
||||
file_num==1 {
|
||||
npub_map[$1] = $2
|
||||
next
|
||||
}
|
||||
# Second file: lookup (event_id -> identifier, pubkey_hex)
|
||||
file_num==2 {
|
||||
lookup_repo[$1] = $2
|
||||
lookup_pubkey[$1] = $3
|
||||
next
|
||||
}
|
||||
# Third file: parse_failures
|
||||
/^#/ { next } # Skip headers (already copied)
|
||||
{
|
||||
event_id = $1
|
||||
kind = $2
|
||||
reason = $3
|
||||
repo = $4
|
||||
npub = $5
|
||||
|
||||
# If repo/npub empty, try to enrich from lookup
|
||||
if (repo == "" && event_id in lookup_repo) {
|
||||
repo = lookup_repo[event_id]
|
||||
}
|
||||
if (npub == "" && event_id in lookup_pubkey) {
|
||||
pubkey = lookup_pubkey[event_id]
|
||||
if (pubkey in npub_map) {
|
||||
npub = npub_map[pubkey]
|
||||
} else {
|
||||
npub = pubkey # Fallback to hex
|
||||
}
|
||||
}
|
||||
|
||||
print event_id, kind, reason, repo, npub
|
||||
}
|
||||
' "$npub_map_file" "$lookup_file" "$parse_failures_file" >> "$enriched_file"
|
||||
|
||||
rm -f "$npub_map_file"
|
||||
|
||||
# Count enriched entries
|
||||
local enriched_count total_count
|
||||
total_count=$(grep -v '^#' "$parse_failures_file" | wc -l)
|
||||
total_count="${total_count//[^0-9]/}"
|
||||
# Count entries that have non-empty repo AND npub after enrichment
|
||||
enriched_count=$(grep -v '^#' "$enriched_file" | awk -F'\t' '$4 != "" && $5 != ""' | wc -l)
|
||||
enriched_count="${enriched_count//[^0-9]/}"
|
||||
|
||||
# Replace original with enriched version
|
||||
mv "$enriched_file" "$parse_failures_file"
|
||||
|
||||
log_info " Enriched $enriched_count of $total_count parse failures with repo/npub"
|
||||
log_success "Enrichment complete"
|
||||
}
|
||||
|
||||
# Parse "Added rejected announcement" log entries to build enrichment lookup table
|
||||
# Input: log line containing "Added rejected announcement to two-tier index"
|
||||
# Output: TSV line: event_id<TAB>identifier<TAB>pubkey_hex
|
||||
parse_rejected_announcement_line() {
|
||||
local line="$1"
|
||||
|
||||
local event_id identifier pubkey_hex
|
||||
|
||||
# Extract event_id=VALUE (hex string)
|
||||
event_id=$(echo "$line" | grep -oP 'event_id=\K[a-f0-9]+' || echo "")
|
||||
|
||||
# Extract identifier=VALUE (repo name)
|
||||
identifier=$(echo "$line" | grep -oP 'identifier=\K[^ ]+' || echo "")
|
||||
|
||||
# Extract pubkey=VALUE (hex string)
|
||||
pubkey_hex=$(echo "$line" | grep -oP 'pubkey=\K[a-f0-9]+' || echo "")
|
||||
|
||||
# Only output if we have all required fields
|
||||
if [[ -n "$event_id" && -n "$identifier" && -n "$pubkey_hex" ]]; then
|
||||
printf '%s\t%s\t%s\n' "$event_id" "$identifier" "$pubkey_hex"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local service="$1"
|
||||
local output_dir="$2"
|
||||
shift 2
|
||||
|
||||
# Default time range: last 30 days
|
||||
local since_date
|
||||
since_date=$(date -d "30 days ago" "+%Y-%m-%d" 2>/dev/null || date -v-30d "+%Y-%m-%d" 2>/dev/null || echo "")
|
||||
local until_date=""
|
||||
local dry_run=false
|
||||
|
||||
# Parse options
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--since)
|
||||
since_date="$2"
|
||||
shift 2
|
||||
;;
|
||||
--until)
|
||||
until_date="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate service name format
|
||||
if [[ ! "$service" =~ \.service$ ]]; then
|
||||
service="${service}.service"
|
||||
fi
|
||||
|
||||
# Validate service is appropriate for structured logging
|
||||
# This prevents the common mistake of using ngit-relay instead of ngit-grasp
|
||||
if type validate_service_for_structured_logging &>/dev/null; then
|
||||
# Use non-interactive mode if not a terminal, skip log check (we'll do our own)
|
||||
local interactive="true"
|
||||
[[ ! -t 0 ]] && interactive="false"
|
||||
|
||||
if ! validate_service_for_structured_logging "$service" "false" "$interactive"; then
|
||||
log_error "Service validation failed. Use an ngit-grasp service for structured logging."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Fallback validation if helper not available
|
||||
if [[ "$service" == *"ngit-relay"* ]]; then
|
||||
log_error "Service name appears to be ngit-relay: $service"
|
||||
log_error "Structured logging ([PARSE_FAIL]) only exists in ngit-grasp services."
|
||||
log_error "Please use the ngit-grasp archive service instead."
|
||||
log_error ""
|
||||
log_error "To find the correct service:"
|
||||
log_error " systemctl list-units 'ngit-grasp*' --all"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Extracting parse failures from systemd logs"
|
||||
log_info "Service: $service"
|
||||
log_info "Output: $output_dir"
|
||||
log_info "Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
|
||||
# Check if journalctl is available
|
||||
if ! command -v journalctl &> /dev/null; then
|
||||
log_error "journalctl not found. This script requires systemd."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate service exists (check if journalctl can find any logs for it)
|
||||
# Note: We don't require the service to be running, just that it has logs
|
||||
if ! journalctl --no-pager -u "$service" -n 1 &>/dev/null; then
|
||||
log_warn "Could not query logs for service: $service"
|
||||
log_warn "This may indicate the service doesn't exist or you lack permissions."
|
||||
log_warn ""
|
||||
log_warn "To list available ngit-grasp services:"
|
||||
log_warn " systemctl list-units 'ngit-grasp*' --all"
|
||||
log_warn " journalctl --list-boots # Check if you have journal access"
|
||||
log_warn ""
|
||||
# Continue anyway - the service might exist but have no logs yet
|
||||
fi
|
||||
|
||||
# Build journalctl command
|
||||
local journal_cmd="journalctl -u $service --no-pager -o short-iso"
|
||||
|
||||
if [[ -n "$since_date" ]]; then
|
||||
journal_cmd="$journal_cmd --since '$since_date'"
|
||||
fi
|
||||
|
||||
if [[ -n "$until_date" ]]; then
|
||||
journal_cmd="$journal_cmd --until '$until_date'"
|
||||
fi
|
||||
|
||||
log_info "Running: $journal_cmd | grep '[PARSE_FAIL]' or 'Invalid announcement'"
|
||||
|
||||
if [[ "$dry_run" == true ]]; then
|
||||
log_info "[DRY RUN] Would extract to: $output_dir/parse-failures.txt"
|
||||
|
||||
# Show sample of what would be extracted
|
||||
log_info "Checking for matching log entries..."
|
||||
local parse_fail_count invalid_announcement_count
|
||||
parse_fail_count=$(eval "$journal_cmd" 2>/dev/null | grep -c '\[PARSE_FAIL\]' || echo "0")
|
||||
parse_fail_count="${parse_fail_count//[^0-9]/}" # Strip non-numeric characters
|
||||
parse_fail_count="${parse_fail_count:-0}"
|
||||
|
||||
invalid_announcement_count=$(eval "$journal_cmd" 2>/dev/null | grep 'Event rejected by write policy' | grep -c 'Invalid announcement' || echo "0")
|
||||
invalid_announcement_count="${invalid_announcement_count//[^0-9]/}"
|
||||
invalid_announcement_count="${invalid_announcement_count:-0}"
|
||||
|
||||
log_info "Found $parse_fail_count [PARSE_FAIL] entries"
|
||||
log_info "Found $invalid_announcement_count 'Invalid announcement' rejections"
|
||||
|
||||
if [[ "$parse_fail_count" -eq 0 && "$invalid_announcement_count" -eq 0 ]]; then
|
||||
log_warn "No matching entries found in logs."
|
||||
log_warn "This is expected if ngit-grasp logging improvements are not yet deployed."
|
||||
log_warn "See: docs/how-to/migrate-to-ngit-grasp.md (Dependencies section)"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
local output_file="$output_dir/parse-failures.txt"
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
# Extract and parse log entries using streaming (avoids loading all logs into memory)
|
||||
log_info "Extracting log entries..."
|
||||
|
||||
# Create temp files for intermediate results
|
||||
local temp_stderr temp_parse_fail temp_write_policy_rejection temp_rejected_announcement
|
||||
temp_stderr=$(mktemp)
|
||||
temp_parse_fail=$(mktemp)
|
||||
temp_write_policy_rejection=$(mktemp)
|
||||
temp_rejected_announcement=$(mktemp)
|
||||
|
||||
# Extract [PARSE_FAIL] entries directly to temp file (streaming)
|
||||
log_info " Searching for [PARSE_FAIL] entries..."
|
||||
eval "$journal_cmd" 2>"$temp_stderr" | grep '\[PARSE_FAIL\]' > "$temp_parse_fail" || true
|
||||
|
||||
local journal_stderr
|
||||
journal_stderr=$(cat "$temp_stderr" 2>/dev/null || true)
|
||||
if [[ -n "$journal_stderr" ]]; then
|
||||
log_warn "journalctl reported: $journal_stderr"
|
||||
fi
|
||||
|
||||
# Extract "Event rejected by write policy" with "Invalid announcement" (streaming)
|
||||
# NOTE: We only extract from write policy logs (hex IDs), not builder logs (note1 IDs)
|
||||
# to avoid double-counting. Both log sources contain the same events.
|
||||
log_info " Searching for write policy rejections..."
|
||||
eval "$journal_cmd" 2>/dev/null | grep 'Event rejected by write policy' | grep 'Invalid announcement' > "$temp_write_policy_rejection" || true
|
||||
|
||||
# Extract "Added rejected announcement" entries for enrichment (streaming)
|
||||
# These contain pubkey and identifier which we use to enrich write policy rejections
|
||||
log_info " Searching for rejected announcement entries (for enrichment)..."
|
||||
eval "$journal_cmd" 2>/dev/null | grep 'Added rejected announcement to two-tier index' > "$temp_rejected_announcement" || true
|
||||
|
||||
rm -f "$temp_stderr"
|
||||
|
||||
# Check if we found anything
|
||||
local parse_fail_line_count write_policy_line_count rejected_announcement_line_count
|
||||
parse_fail_line_count=$(wc -l < "$temp_parse_fail")
|
||||
parse_fail_line_count="${parse_fail_line_count//[^0-9]/}"
|
||||
write_policy_line_count=$(wc -l < "$temp_write_policy_rejection")
|
||||
write_policy_line_count="${write_policy_line_count//[^0-9]/}"
|
||||
rejected_announcement_line_count=$(wc -l < "$temp_rejected_announcement")
|
||||
rejected_announcement_line_count="${rejected_announcement_line_count//[^0-9]/}"
|
||||
|
||||
log_info " Found $parse_fail_line_count [PARSE_FAIL] log lines"
|
||||
log_info " Found $write_policy_line_count write policy rejection log lines"
|
||||
log_info " Found $rejected_announcement_line_count rejected announcement log lines (for enrichment)"
|
||||
|
||||
local total_invalid_announcement_lines=$write_policy_line_count
|
||||
|
||||
if [[ "$parse_fail_line_count" -eq 0 && "$total_invalid_announcement_lines" -eq 0 ]]; then
|
||||
log_warn "No matching entries found in logs."
|
||||
log_warn ""
|
||||
log_warn "This is expected if ngit-grasp logging improvements are not yet deployed."
|
||||
log_warn "The script looks for:"
|
||||
log_warn ""
|
||||
log_warn " 1. [PARSE_FAIL] kind=30618 event_id=abc123 reason=\"...\" repo=myrepo npub=npub1..."
|
||||
log_warn " 2. Event rejected by write policy event_id=... kind=30617 reason=Invalid announcement: ..."
|
||||
log_warn ""
|
||||
log_warn "See: docs/how-to/migrate-to-ngit-grasp.md (Dependencies section)"
|
||||
log_warn ""
|
||||
|
||||
# Create empty output file with header comment
|
||||
{
|
||||
echo "# Parse failures and invalid announcements extracted from $service"
|
||||
echo "# Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
echo "# Extracted: $(date -Iseconds)"
|
||||
echo "#"
|
||||
echo "# Includes:"
|
||||
echo "# - [PARSE_FAIL] structured log entries"
|
||||
echo "# - \"Invalid announcement\" rejections"
|
||||
echo "#"
|
||||
echo "# Format: event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub"
|
||||
echo "# Note: repo and npub may be empty for some entries"
|
||||
echo "#"
|
||||
echo "# NOTE: No matching entries found."
|
||||
echo "# This is expected if ngit-grasp logging improvements are not yet deployed."
|
||||
} > "$output_file"
|
||||
|
||||
rm -f "$temp_parse_fail" "$temp_write_policy_rejection" "$temp_rejected_announcement"
|
||||
log_info "Created empty output file: $output_file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Write header
|
||||
{
|
||||
echo "# Parse failures and invalid announcements extracted from $service"
|
||||
echo "# Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
echo "# Extracted: $(date -Iseconds)"
|
||||
echo "#"
|
||||
echo "# Includes:"
|
||||
echo "# - [PARSE_FAIL] structured log entries"
|
||||
echo "# - \"Invalid announcement\" rejections"
|
||||
echo "#"
|
||||
echo "# Format: event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub"
|
||||
echo "# Note: repo and npub may be empty for some entries"
|
||||
} > "$output_file"
|
||||
|
||||
# Parse [PARSE_FAIL] entries using batch awk processing
|
||||
log_info " Parsing [PARSE_FAIL] entries..."
|
||||
local parse_fail_count=0
|
||||
if [[ "$parse_fail_line_count" -gt 0 ]]; then
|
||||
parse_parse_fail_batch "$temp_parse_fail" >> "$output_file"
|
||||
parse_fail_count=$(grep -v '^#' "$output_file" | wc -l)
|
||||
parse_fail_count="${parse_fail_count//[^0-9]/}"
|
||||
fi
|
||||
|
||||
# Parse write policy rejection entries using batch awk processing
|
||||
log_info " Parsing write policy rejection entries..."
|
||||
local write_policy_count=0
|
||||
if [[ "$write_policy_line_count" -gt 0 ]]; then
|
||||
local before_count
|
||||
before_count=$(grep -v '^#' "$output_file" 2>/dev/null | wc -l || echo "0")
|
||||
before_count="${before_count//[^0-9]/}"
|
||||
before_count="${before_count:-0}"
|
||||
parse_write_policy_rejection_batch "$temp_write_policy_rejection" >> "$output_file"
|
||||
local after_count
|
||||
after_count=$(grep -v '^#' "$output_file" 2>/dev/null | wc -l || echo "0")
|
||||
after_count="${after_count//[^0-9]/}"
|
||||
after_count="${after_count:-0}"
|
||||
write_policy_count=$((after_count - before_count))
|
||||
fi
|
||||
|
||||
local invalid_announcement_count=$write_policy_count
|
||||
|
||||
# Build enrichment lookup table from "Added rejected announcement" entries
|
||||
local enrichment_lookup_file
|
||||
enrichment_lookup_file=$(mktemp)
|
||||
|
||||
log_info " Building enrichment lookup table..."
|
||||
if [[ "$rejected_announcement_line_count" -gt 0 ]]; then
|
||||
parse_rejected_announcement_batch "$temp_rejected_announcement" > "$enrichment_lookup_file"
|
||||
fi
|
||||
|
||||
rm -f "$temp_parse_fail" "$temp_write_policy_rejection" "$temp_rejected_announcement"
|
||||
|
||||
# Deduplicate by event_id (first column) - keep first occurrence
|
||||
log_info " Deduplicating entries..."
|
||||
local deduped_file
|
||||
deduped_file=$(mktemp)
|
||||
# Preserve header lines (starting with #) and deduplicate data lines
|
||||
grep '^#' "$output_file" > "$deduped_file"
|
||||
grep -v '^#' "$output_file" | sort -t$'\t' -k1,1 -u >> "$deduped_file"
|
||||
mv "$deduped_file" "$output_file"
|
||||
|
||||
# Deduplicate enrichment lookup table by event_id
|
||||
if [[ -s "$enrichment_lookup_file" ]]; then
|
||||
sort -t$'\t' -k1,1 -u "$enrichment_lookup_file" > "$enrichment_lookup_file.deduped"
|
||||
mv "$enrichment_lookup_file.deduped" "$enrichment_lookup_file"
|
||||
fi
|
||||
|
||||
# Enrich with repo/npub from "Added rejected announcement" log entries
|
||||
# This is critical for usability - without it, action-required.txt shows
|
||||
# event_id|kind instead of repo|npub, making parse failures unidentifiable
|
||||
enrich_with_repo_npub "$output_file" "$enrichment_lookup_file"
|
||||
|
||||
rm -f "$enrichment_lookup_file"
|
||||
|
||||
# Count final entries (excluding header lines)
|
||||
local count
|
||||
count=$(grep -v '^#' "$output_file" | wc -l)
|
||||
count="${count//[^0-9]/}" # Strip whitespace
|
||||
count="${count:-0}"
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
log_info "=== Extraction Summary ==="
|
||||
log_info "Service: $service"
|
||||
log_info "Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
log_success "Extracted $count total entries"
|
||||
log_info " - [PARSE_FAIL] entries: $parse_fail_count"
|
||||
log_info " - Invalid announcement rejections: $invalid_announcement_count"
|
||||
echo ""
|
||||
log_info "Output file: $output_file"
|
||||
|
||||
if [[ $count -gt 0 ]]; then
|
||||
echo ""
|
||||
log_info "Sample entries (first 5):"
|
||||
# Use a subshell to avoid SIGPIPE issues with set -e
|
||||
# New format: event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub
|
||||
(grep -v '^#' "$output_file" | head -5 | while IFS=$'\t' read -r event_id kind reason repo npub; do
|
||||
echo " kind=$kind event_id=${event_id:0:16}... reason=\"${reason:0:60}...\""
|
||||
done) || true
|
||||
fi
|
||||
|
||||
# Breakdown by kind
|
||||
if [[ $count -gt 0 ]]; then
|
||||
echo ""
|
||||
log_info "Breakdown by event kind:"
|
||||
# Use a subshell to avoid SIGPIPE issues with set -e
|
||||
# kind is now column 2
|
||||
(grep -v '^#' "$output_file" | awk -F'\t' '{print $2}' | sort | uniq -c | sort -rn | while read -r cnt kind; do
|
||||
echo " kind $kind: $cnt failures"
|
||||
done) || true
|
||||
fi
|
||||
|
||||
# Breakdown by reason pattern (for invalid announcements)
|
||||
if [[ $invalid_announcement_count -gt 0 ]]; then
|
||||
echo ""
|
||||
log_info "Breakdown by reason pattern:"
|
||||
# Extract the main reason type (before the colon details)
|
||||
(grep -v '^#' "$output_file" | awk -F'\t' '{print $3}' | sed 's/:.*//' | sort | uniq -c | sort -rn | head -10 | while read -r cnt reason; do
|
||||
echo " $reason: $cnt"
|
||||
done) || true
|
||||
fi
|
||||
|
||||
# Explicit success exit
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,408 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 31-extract-purgatory-expiry.sh - Extract purgatory expiry events from systemd logs
|
||||
#
|
||||
# PHASE 4b of the GRASP relay to ngit-grasp migration analysis pipeline.
|
||||
# Extracts structured [PURGATORY_EXPIRED] log entries from journalctl.
|
||||
#
|
||||
# USAGE:
|
||||
# ./31-extract-purgatory-expiry.sh <service-name> <output-dir> [options]
|
||||
#
|
||||
# EXAMPLES:
|
||||
# # Extract from ngit-grasp service (last 30 days, default)
|
||||
# ./31-extract-purgatory-expiry.sh ngit-grasp.service output/logs
|
||||
#
|
||||
# # Extract with custom time range
|
||||
# ./31-extract-purgatory-expiry.sh ngit-grasp.service output/logs --since "2026-01-01"
|
||||
#
|
||||
# # Extract from specific time window
|
||||
# ./31-extract-purgatory-expiry.sh ngit-grasp.service output/logs --since "2026-01-15" --until "2026-01-22"
|
||||
#
|
||||
# OPTIONS:
|
||||
# --since <date> Start date for log extraction (default: 30 days ago)
|
||||
# --until <date> End date for log extraction (default: now)
|
||||
# --dry-run Show what would be extracted without writing files
|
||||
#
|
||||
# OUTPUT:
|
||||
# <output-dir>/purgatory-expired.txt
|
||||
#
|
||||
# OUTPUT FORMAT (TSV):
|
||||
# repo<TAB>npub<TAB>timestamp<TAB>reason
|
||||
#
|
||||
# EXPECTED LOG FORMAT:
|
||||
# The script looks for structured log entries in this format:
|
||||
#
|
||||
# 2026-01-22T10:30:45Z ngit-grasp[1234]: [PURGATORY_EXPIRED] repo=myrepo npub=npub1... reason="clone URL unreachable after 7 days"
|
||||
#
|
||||
# Required fields: repo, npub
|
||||
# Optional fields: reason (explains why purgatory expired)
|
||||
#
|
||||
# BACKGROUND:
|
||||
# "Purgatory" is the state where ngit-grasp has received an announcement event
|
||||
# but cannot yet sync the git data (e.g., clone URL unreachable, git server down).
|
||||
# After a configurable timeout (default 7 days), the repository is marked as
|
||||
# expired and removed from purgatory.
|
||||
#
|
||||
# Purgatory expiry during migration analysis indicates repositories that:
|
||||
# - Had valid announcements on the production relay
|
||||
# - Could not be synced to the archive relay
|
||||
# - May need manual intervention or investigation
|
||||
#
|
||||
# DEPENDENCY:
|
||||
# This script requires logging improvements in ngit-grasp to emit structured
|
||||
# [PURGATORY_EXPIRED] log entries. Until those are implemented, this script
|
||||
# will find no matching entries (which is handled gracefully).
|
||||
#
|
||||
# See: docs/how-to/migrate-to-ngit-grasp.md (Dependencies section)
|
||||
#
|
||||
# Expected Rust logging code:
|
||||
# tracing::warn!(
|
||||
# target: "migration",
|
||||
# "[PURGATORY_EXPIRED] repo={} npub={} reason=\"{}\"",
|
||||
# identifier, npub, reason
|
||||
# );
|
||||
#
|
||||
# PREREQUISITES:
|
||||
# - journalctl (systemd)
|
||||
# - grep, awk (standard Unix tools)
|
||||
# - Access to systemd journal (may require sudo or journal group membership)
|
||||
#
|
||||
# RUNTIME: Depends on log volume, typically < 30 seconds
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
# 30-extract-parse-failures.sh - Companion script for parse failure logs
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get script directory for sourcing helpers
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source the service validation helper
|
||||
if [[ -f "$SCRIPT_DIR/validate-service.sh" ]]; then
|
||||
source "$SCRIPT_DIR/validate-service.sh"
|
||||
fi
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <service-name> <output-dir> [options]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " service-name Systemd service name (e.g., ngit-grasp.service)"
|
||||
echo " output-dir Directory to store extracted log data"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --since <date> Start date (default: 30 days ago)"
|
||||
echo " --until <date> End date (default: now)"
|
||||
echo " --dry-run Show what would be extracted without writing"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 ngit-grasp.service output/logs"
|
||||
echo " $0 ngit-grasp.service output/logs --since '2026-01-01'"
|
||||
echo " $0 ngit-grasp.service output/logs --since '2026-01-15' --until '2026-01-22'"
|
||||
echo ""
|
||||
echo "Expected log format:"
|
||||
echo " [PURGATORY_EXPIRED] repo=myrepo npub=npub1... reason=\"...\""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse a single log line and extract fields
|
||||
# Input: log line containing [PURGATORY_EXPIRED]
|
||||
# Output: TSV line: repo<TAB>npub<TAB>timestamp<TAB>reason
|
||||
parse_log_line() {
|
||||
local line="$1"
|
||||
|
||||
# Extract timestamp from the beginning of the log line
|
||||
# Format: 2026-01-22T10:30:45+0000 or similar ISO format
|
||||
local timestamp repo npub reason
|
||||
|
||||
# Extract ISO timestamp from beginning of line
|
||||
timestamp=$(echo "$line" | grep -oP '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}' || echo "")
|
||||
|
||||
# Extract repo=VALUE (unquoted identifier)
|
||||
repo=$(echo "$line" | grep -oP 'repo=\K[^ ]+' || echo "")
|
||||
|
||||
# Extract npub=VALUE (npub1... format)
|
||||
npub=$(echo "$line" | grep -oP 'npub=\K[^ ]+' || echo "")
|
||||
|
||||
# Extract reason="VALUE" (quoted string, optional)
|
||||
reason=$(echo "$line" | grep -oP 'reason="\K[^"]*' || echo "")
|
||||
|
||||
# Only output if we have the required fields
|
||||
if [[ -n "$repo" && -n "$npub" ]]; then
|
||||
printf '%s\t%s\t%s\t%s\n' "$repo" "$npub" "$timestamp" "$reason"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local service="$1"
|
||||
local output_dir="$2"
|
||||
shift 2
|
||||
|
||||
# Default time range: last 30 days
|
||||
local since_date
|
||||
since_date=$(date -d "30 days ago" "+%Y-%m-%d" 2>/dev/null || date -v-30d "+%Y-%m-%d" 2>/dev/null || echo "")
|
||||
local until_date=""
|
||||
local dry_run=false
|
||||
|
||||
# Parse options
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--since)
|
||||
since_date="$2"
|
||||
shift 2
|
||||
;;
|
||||
--until)
|
||||
until_date="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate service name format
|
||||
if [[ ! "$service" =~ \.service$ ]]; then
|
||||
service="${service}.service"
|
||||
fi
|
||||
|
||||
# Validate service is appropriate for structured logging
|
||||
# This prevents the common mistake of using ngit-relay instead of ngit-grasp
|
||||
if type validate_service_for_structured_logging &>/dev/null; then
|
||||
# Use non-interactive mode if not a terminal, skip log check (we'll do our own)
|
||||
local interactive="true"
|
||||
[[ ! -t 0 ]] && interactive="false"
|
||||
|
||||
if ! validate_service_for_structured_logging "$service" "false" "$interactive"; then
|
||||
log_error "Service validation failed. Use an ngit-grasp service for structured logging."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Fallback validation if helper not available
|
||||
if [[ "$service" == *"ngit-relay"* ]]; then
|
||||
log_error "Service name appears to be ngit-relay: $service"
|
||||
log_error "Structured logging ([PURGATORY_EXPIRED]) only exists in ngit-grasp services."
|
||||
log_error "Please use the ngit-grasp archive service instead."
|
||||
log_error ""
|
||||
log_error "To find the correct service:"
|
||||
log_error " systemctl list-units 'ngit-grasp*' --all"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Extracting purgatory expiry events from systemd logs"
|
||||
log_info "Service: $service"
|
||||
log_info "Output: $output_dir"
|
||||
log_info "Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
|
||||
# Check if journalctl is available
|
||||
if ! command -v journalctl &> /dev/null; then
|
||||
log_error "journalctl not found. This script requires systemd."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate service exists (check if journalctl can find any logs for it)
|
||||
# Note: We don't require the service to be running, just that it has logs
|
||||
if ! journalctl --no-pager -u "$service" -n 1 &>/dev/null; then
|
||||
log_warn "Could not query logs for service: $service"
|
||||
log_warn "This may indicate the service doesn't exist or you lack permissions."
|
||||
log_warn ""
|
||||
log_warn "To list available ngit-grasp services:"
|
||||
log_warn " systemctl list-units 'ngit-grasp*' --all"
|
||||
log_warn " journalctl --list-boots # Check if you have journal access"
|
||||
log_warn ""
|
||||
# Continue anyway - the service might exist but have no logs yet
|
||||
fi
|
||||
|
||||
# Build journalctl command
|
||||
local journal_cmd="journalctl -u $service --no-pager -o short-iso"
|
||||
|
||||
if [[ -n "$since_date" ]]; then
|
||||
journal_cmd="$journal_cmd --since '$since_date'"
|
||||
fi
|
||||
|
||||
if [[ -n "$until_date" ]]; then
|
||||
journal_cmd="$journal_cmd --until '$until_date'"
|
||||
fi
|
||||
|
||||
log_info "Running: $journal_cmd | grep '\\[PURGATORY_EXPIRED\\]'"
|
||||
|
||||
if [[ "$dry_run" == true ]]; then
|
||||
log_info "[DRY RUN] Would extract to: $output_dir/purgatory-expired.txt"
|
||||
|
||||
# Show sample of what would be extracted
|
||||
log_info "Checking for matching log entries..."
|
||||
local sample_count
|
||||
sample_count=$(eval "$journal_cmd" 2>/dev/null | grep -c '\[PURGATORY_EXPIRED\]' || echo "0")
|
||||
sample_count="${sample_count//[^0-9]/}" # Strip non-numeric characters
|
||||
sample_count="${sample_count:-0}"
|
||||
log_info "Found $sample_count matching log entries"
|
||||
|
||||
if [[ "$sample_count" -eq 0 ]]; then
|
||||
log_warn "No [PURGATORY_EXPIRED] entries found in logs."
|
||||
log_warn "This is expected if ngit-grasp logging improvements are not yet deployed."
|
||||
log_warn "See: docs/how-to/migrate-to-ngit-grasp.md (Dependencies section)"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
local output_file="$output_dir/purgatory-expired.txt"
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
# Extract and parse log entries
|
||||
log_info "Extracting log entries..."
|
||||
|
||||
# Get raw log lines containing [PURGATORY_EXPIRED]
|
||||
# Capture stderr separately to detect journalctl errors
|
||||
local raw_lines journal_stderr journal_exit
|
||||
local temp_stderr
|
||||
temp_stderr=$(mktemp)
|
||||
|
||||
raw_lines=$(eval "$journal_cmd" 2>"$temp_stderr" | grep '\[PURGATORY_EXPIRED\]' || true)
|
||||
journal_exit=$?
|
||||
journal_stderr=$(cat "$temp_stderr" 2>/dev/null || true)
|
||||
rm -f "$temp_stderr"
|
||||
|
||||
# Report any journalctl errors (but don't fail - empty logs are valid)
|
||||
if [[ -n "$journal_stderr" ]]; then
|
||||
log_warn "journalctl reported: $journal_stderr"
|
||||
fi
|
||||
|
||||
if [[ -z "$raw_lines" ]]; then
|
||||
log_warn "No [PURGATORY_EXPIRED] entries found in logs."
|
||||
log_warn ""
|
||||
log_warn "This is expected if ngit-grasp logging improvements are not yet deployed."
|
||||
log_warn "The structured log format required by this script:"
|
||||
log_warn ""
|
||||
log_warn " [PURGATORY_EXPIRED] repo=myrepo npub=npub1... reason=\"...\""
|
||||
log_warn ""
|
||||
log_warn "See: docs/how-to/migrate-to-ngit-grasp.md (Dependencies section)"
|
||||
log_warn ""
|
||||
|
||||
# Create empty output file with header comment
|
||||
{
|
||||
echo "# Purgatory expiry events extracted from $service"
|
||||
echo "# Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
echo "# Extracted: $(date -Iseconds)"
|
||||
echo "# Format: repo<TAB>npub<TAB>timestamp<TAB>reason"
|
||||
echo "#"
|
||||
echo "# NOTE: No [PURGATORY_EXPIRED] entries found."
|
||||
echo "# This is expected if ngit-grasp logging improvements are not yet deployed."
|
||||
} > "$output_file"
|
||||
|
||||
log_info "Created empty output file: $output_file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Write header
|
||||
{
|
||||
echo "# Purgatory expiry events extracted from $service"
|
||||
echo "# Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
echo "# Extracted: $(date -Iseconds)"
|
||||
echo "# Format: repo<TAB>npub<TAB>timestamp<TAB>reason"
|
||||
} > "$output_file"
|
||||
|
||||
# Parse each line
|
||||
local count=0
|
||||
while IFS= read -r line; do
|
||||
local parsed
|
||||
parsed=$(parse_log_line "$line")
|
||||
if [[ -n "$parsed" ]]; then
|
||||
echo "$parsed" >> "$output_file"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
done <<< "$raw_lines"
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
log_info "=== Extraction Summary ==="
|
||||
log_info "Service: $service"
|
||||
log_info "Time range: ${since_date:-beginning} to ${until_date:-now}"
|
||||
log_success "Extracted $count purgatory expiry entries"
|
||||
echo ""
|
||||
log_info "Output file: $output_file"
|
||||
|
||||
if [[ $count -gt 0 ]]; then
|
||||
echo ""
|
||||
log_info "Sample entries (first 5):"
|
||||
# Use a subshell to avoid SIGPIPE issues with set -e
|
||||
(tail -n +5 "$output_file" | head -5 | while IFS=$'\t' read -r repo npub timestamp reason; do
|
||||
echo " repo=$repo npub=${npub:0:20}... timestamp=$timestamp"
|
||||
done) || true
|
||||
fi
|
||||
|
||||
# Show unique repos affected
|
||||
if [[ $count -gt 0 ]]; then
|
||||
echo ""
|
||||
local unique_repos
|
||||
unique_repos=$(tail -n +5 "$output_file" | awk -F'\t' '{print $1}' | sort -u | wc -l)
|
||||
log_info "Unique repositories affected: $unique_repos"
|
||||
|
||||
echo ""
|
||||
log_info "Repositories with purgatory expiry:"
|
||||
# Use a subshell to avoid SIGPIPE issues with set -e
|
||||
(tail -n +5 "$output_file" | awk -F'\t' '{print $1}' | sort | uniq -c | sort -rn | head -10 | while read -r cnt repo; do
|
||||
echo " $repo: $cnt expiry events"
|
||||
done) || true
|
||||
|
||||
local total_repos
|
||||
total_repos=$(tail -n +5 "$output_file" | awk -F'\t' '{print $1}' | sort -u | wc -l)
|
||||
if [[ $total_repos -gt 10 ]]; then
|
||||
echo " ... and $((total_repos - 10)) more repositories"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Explicit success exit
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,685 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 40-classify-actions.sh - Classify repos by migration action required
|
||||
#
|
||||
# Implements the redesigned classification system (Option B) with user feedback:
|
||||
#
|
||||
# Tier 1: No Action Required (ready-for-migration.txt)
|
||||
# - Complete in both (prod=cat1, archive=cat1)
|
||||
# - Deleted by user (kind 5 event)
|
||||
# - Empty in prod (prod=cat2, any archive status)
|
||||
# - Archive-only (archive=any, prod=missing)
|
||||
# - Not in prod (purgatory-only, prod=missing)
|
||||
# - Archive ahead (archive has newer git data than prod - GRASP enforced)
|
||||
#
|
||||
# Tier 2: Action Required (needs-resync.txt)
|
||||
# - Complete in prod, missing from archive (with purgatory context)
|
||||
# - Complete in prod, incomplete in archive AND prod is ahead (with purgatory context)
|
||||
#
|
||||
# Tier 3: Manual Investigation (manual-review.txt)
|
||||
# - Partial in prod (prod=cat3)
|
||||
# - No-match in prod (prod=cat4)
|
||||
# - Parse failures
|
||||
# - Conflicting states
|
||||
# - Diverged git history (both have unique commits)
|
||||
#
|
||||
# KEY INSIGHT:
|
||||
# Archive (ngit-grasp) enforces GRASP - git data ALWAYS matches a state event.
|
||||
# If archive has different/newer data than prod, it means:
|
||||
# - A state event authorized those commits at some point
|
||||
# - Archive is actually MORE up-to-date than prod
|
||||
# - Migration should use archive data (it's already correct)
|
||||
#
|
||||
# Usage: ./40-classify-actions.sh <analysis-dir>
|
||||
#
|
||||
# Output format: repo | npub | prod_status | archive_status | context | action
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
log_success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
# Check arguments
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <analysis-dir>"
|
||||
echo "Example: $0 work/migration-analysis-20260123-200701"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ANALYSIS_DIR="$1"
|
||||
|
||||
# Validate analysis directory
|
||||
if [[ ! -d "$ANALYSIS_DIR" ]]; then
|
||||
log_error "Analysis directory not found: $ANALYSIS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Define paths
|
||||
PROD_DIR="$ANALYSIS_DIR/prod"
|
||||
ARCHIVE_DIR="$ANALYSIS_DIR/archive"
|
||||
COMPARISON_DIR="$ANALYSIS_DIR/comparison"
|
||||
LOGS_DIR="$ANALYSIS_DIR/logs"
|
||||
RESULTS_DIR="$ANALYSIS_DIR/results"
|
||||
|
||||
# Validate required directories
|
||||
for dir in "$PROD_DIR" "$ARCHIVE_DIR" "$COMPARISON_DIR" "$LOGS_DIR"; do
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
log_error "Required directory not found: $dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Create results directory
|
||||
mkdir -p "$RESULTS_DIR"
|
||||
|
||||
# Output files
|
||||
READY_FILE="$RESULTS_DIR/ready-for-migration.txt"
|
||||
RESYNC_FILE="$RESULTS_DIR/needs-resync.txt"
|
||||
REVIEW_FILE="$RESULTS_DIR/manual-review.txt"
|
||||
SUMMARY_FILE="$RESULTS_DIR/summary.txt"
|
||||
|
||||
# Temporary files for processing
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
log_info "Starting classification with revised system (Option B)"
|
||||
log_info "Analysis directory: $ANALYSIS_DIR"
|
||||
|
||||
# ============================================================================
|
||||
# Phase 1: Build lookup tables from source data
|
||||
# ============================================================================
|
||||
|
||||
log_info "Building lookup tables..."
|
||||
|
||||
# Build prod category lookup: repo|npub -> category
|
||||
declare -A PROD_CAT
|
||||
PROD_CAT_COUNT=0
|
||||
while IFS='|' read -r repo npub rest || [[ -n "$repo" ]]; do
|
||||
repo="${repo// /}" # Remove all spaces
|
||||
npub="${npub// /}" # Remove all spaces
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
PROD_CAT["$repo|$npub"]="cat1"
|
||||
PROD_CAT_COUNT=$((PROD_CAT_COUNT + 1))
|
||||
done < "$PROD_DIR/category1-complete-match.txt"
|
||||
|
||||
while IFS='|' read -r repo npub rest || [[ -n "$repo" ]]; do
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
PROD_CAT["$repo|$npub"]="cat2"
|
||||
PROD_CAT_COUNT=$((PROD_CAT_COUNT + 1))
|
||||
done < "$PROD_DIR/category2-empty-blank.txt"
|
||||
|
||||
while IFS='|' read -r repo npub rest || [[ -n "$repo" ]]; do
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
PROD_CAT["$repo|$npub"]="cat3"
|
||||
PROD_CAT_COUNT=$((PROD_CAT_COUNT + 1))
|
||||
done < "$PROD_DIR/category3-partial-match.txt"
|
||||
|
||||
while IFS='|' read -r repo npub rest || [[ -n "$repo" ]]; do
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
PROD_CAT["$repo|$npub"]="cat4"
|
||||
PROD_CAT_COUNT=$((PROD_CAT_COUNT + 1))
|
||||
done < "$PROD_DIR/category4-no-match.txt"
|
||||
|
||||
log_info "Loaded $PROD_CAT_COUNT prod entries"
|
||||
|
||||
# Build archive category lookup: repo|npub -> category
|
||||
declare -A ARCHIVE_CAT
|
||||
ARCHIVE_CAT_COUNT=0
|
||||
while IFS='|' read -r repo npub rest; do
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
ARCHIVE_CAT["$repo|$npub"]="cat1"
|
||||
ARCHIVE_CAT_COUNT=$((ARCHIVE_CAT_COUNT + 1))
|
||||
done < "$ARCHIVE_DIR/category1-complete-match.txt"
|
||||
|
||||
while IFS='|' read -r repo npub rest; do
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
ARCHIVE_CAT["$repo|$npub"]="cat2"
|
||||
ARCHIVE_CAT_COUNT=$((ARCHIVE_CAT_COUNT + 1))
|
||||
done < "$ARCHIVE_DIR/category2-empty-blank.txt"
|
||||
|
||||
while IFS='|' read -r repo npub rest; do
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
ARCHIVE_CAT["$repo|$npub"]="cat3"
|
||||
ARCHIVE_CAT_COUNT=$((ARCHIVE_CAT_COUNT + 1))
|
||||
done < "$ARCHIVE_DIR/category3-partial-match.txt"
|
||||
|
||||
while IFS='|' read -r repo npub rest; do
|
||||
repo="${repo// /}"
|
||||
npub="${npub// /}"
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
ARCHIVE_CAT["$repo|$npub"]="cat4"
|
||||
ARCHIVE_CAT_COUNT=$((ARCHIVE_CAT_COUNT + 1))
|
||||
done < "$ARCHIVE_DIR/category4-no-match.txt"
|
||||
|
||||
log_info "Loaded $ARCHIVE_CAT_COUNT archive entries"
|
||||
|
||||
# Build purgatory lookup: repo|npub -> 1 (if purgatory expired)
|
||||
declare -A PURGATORY
|
||||
PURGATORY_COUNT=0
|
||||
if [[ -f "$LOGS_DIR/purgatory-expired.txt" ]]; then
|
||||
while IFS=$'\t' read -r repo npub timestamp reason || [[ -n "$repo" ]]; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$repo" =~ ^# ]] && continue
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
PURGATORY["$repo|$npub"]=1
|
||||
PURGATORY_COUNT=$((PURGATORY_COUNT + 1))
|
||||
done < "$LOGS_DIR/purgatory-expired.txt"
|
||||
fi
|
||||
log_info "Loaded $PURGATORY_COUNT purgatory entries"
|
||||
|
||||
# Build parse failure lookup: repo|npub -> 1 (if parse failure logged)
|
||||
# Parse failures file format: event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub
|
||||
declare -A PARSE_FAIL
|
||||
PARSE_FAIL_COUNT=0
|
||||
if [[ -f "$LOGS_DIR/parse-failures.txt" ]]; then
|
||||
while IFS=$'\t' read -r event_id kind reason repo npub || [[ -n "$event_id" ]]; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$event_id" =~ ^# ]] && continue
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
PARSE_FAIL["$repo|$npub"]=1
|
||||
PARSE_FAIL_COUNT=$((PARSE_FAIL_COUNT + 1))
|
||||
done < "$LOGS_DIR/parse-failures.txt"
|
||||
fi
|
||||
log_info "Loaded $PARSE_FAIL_COUNT parse failure entries"
|
||||
|
||||
# Build deletion lookup: repo|npub -> 1 (if kind 5 deletion event)
|
||||
# Deletions are in NDJSON format with "a" tags like "30617:pubkey_hex:repo"
|
||||
# We need to convert hex pubkeys to npub format using nak
|
||||
declare -A DELETED
|
||||
|
||||
# Helper function to process deletion file (NDJSON format)
|
||||
# Extracts unique pubkey_hex:repo pairs and converts to npub
|
||||
process_deletions() {
|
||||
local file="$1"
|
||||
[[ ! -f "$file" ]] && return
|
||||
|
||||
# Extract unique pubkey_hex|repo pairs from NDJSON
|
||||
# Each line is a JSON object, extract "a" tags
|
||||
local pairs
|
||||
pairs=$(jq -r '.tags[] | select(.[0] == "a") | .[1]' "$file" 2>/dev/null | \
|
||||
sed 's/^30617://' | awk -F: '{print $1 "|" $2}' | sort -u)
|
||||
|
||||
# Get unique hex pubkeys for batch conversion
|
||||
local hex_keys
|
||||
hex_keys=$(echo "$pairs" | cut -d'|' -f1 | sort -u)
|
||||
|
||||
# Build hex->npub lookup via batch nak call
|
||||
declare -A HEX_TO_NPUB
|
||||
while read -r hex; do
|
||||
[[ -z "$hex" ]] && continue
|
||||
local npub
|
||||
npub=$(nak encode npub "$hex" 2>/dev/null || echo "")
|
||||
[[ -n "$npub" ]] && HEX_TO_NPUB["$hex"]="$npub"
|
||||
done <<< "$hex_keys"
|
||||
|
||||
# Now process pairs with cached npub values
|
||||
while IFS='|' read -r pubkey_hex repo; do
|
||||
[[ -z "$repo" || -z "$pubkey_hex" ]] && continue
|
||||
local npub="${HEX_TO_NPUB[$pubkey_hex]:-}"
|
||||
[[ -z "$npub" ]] && continue
|
||||
DELETED["$repo|$npub"]=1
|
||||
done <<< "$pairs"
|
||||
}
|
||||
|
||||
# Process prod and archive deletions
|
||||
process_deletions "$PROD_DIR/raw/deletions.json"
|
||||
process_deletions "$ARCHIVE_DIR/raw/deletions.json"
|
||||
DELETED_COUNT=0
|
||||
for _k in "${!DELETED[@]}"; do DELETED_COUNT=$((DELETED_COUNT + 1)); done
|
||||
log_info "Loaded $DELETED_COUNT deletion entries"
|
||||
|
||||
# Build git ancestry lookup: repo|npub -> relationship (archive-ahead, prod-ahead, diverged, etc.)
|
||||
# This data comes from 22-compare-git-data.sh which compares actual git commits
|
||||
declare -A GIT_ANCESTRY
|
||||
GIT_ANCESTRY_COUNT=0
|
||||
if [[ -f "$COMPARISON_DIR/git-ancestry.tsv" ]]; then
|
||||
while IFS=$'\t' read -r repo npub relationship details || [[ -n "$repo" ]]; do
|
||||
# Skip header and comments
|
||||
[[ "$repo" == "repo" ]] && continue
|
||||
[[ "$repo" =~ ^# ]] && continue
|
||||
[[ -z "$repo" || -z "$npub" ]] && continue
|
||||
GIT_ANCESTRY["$repo|$npub"]="$relationship"
|
||||
GIT_ANCESTRY_COUNT=$((GIT_ANCESTRY_COUNT + 1))
|
||||
done < "$COMPARISON_DIR/git-ancestry.tsv"
|
||||
log_info "Loaded $GIT_ANCESTRY_COUNT git ancestry entries"
|
||||
else
|
||||
log_warn "No git-ancestry.tsv found - will not check if archive is ahead of prod"
|
||||
log_warn "Run 22-compare-git-data.sh to enable archive-ahead detection"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Phase 2: Build unique repo list from all sources
|
||||
# ============================================================================
|
||||
|
||||
log_info "Building unique repo list..."
|
||||
|
||||
declare -A ALL_REPOS
|
||||
ALL_REPOS_COUNT=0
|
||||
set +u
|
||||
for key in "${!PROD_CAT[@]}"; do
|
||||
[[ -z "${ALL_REPOS[$key]+x}" ]] && { ALL_REPOS["$key"]=1; ALL_REPOS_COUNT=$((ALL_REPOS_COUNT + 1)); }
|
||||
done
|
||||
for key in "${!ARCHIVE_CAT[@]}"; do
|
||||
[[ -z "${ALL_REPOS[$key]+x}" ]] && { ALL_REPOS["$key"]=1; ALL_REPOS_COUNT=$((ALL_REPOS_COUNT + 1)); }
|
||||
done
|
||||
for key in "${!PURGATORY[@]}"; do
|
||||
[[ -z "${ALL_REPOS[$key]+x}" ]] && { ALL_REPOS["$key"]=1; ALL_REPOS_COUNT=$((ALL_REPOS_COUNT + 1)); }
|
||||
done
|
||||
set -u
|
||||
|
||||
log_info "Total unique repos: $ALL_REPOS_COUNT"
|
||||
|
||||
# ============================================================================
|
||||
# Phase 3: Classify each repo according to revised decision tree
|
||||
# ============================================================================
|
||||
|
||||
log_info "Classifying repos..."
|
||||
|
||||
# Counters for summary
|
||||
declare -A COUNTS
|
||||
COUNTS[ready_complete_both]=0
|
||||
COUNTS[ready_deleted]=0
|
||||
COUNTS[ready_empty_prod]=0
|
||||
COUNTS[ready_archive_only]=0
|
||||
COUNTS[ready_not_in_prod]=0
|
||||
COUNTS[ready_archive_ahead]=0
|
||||
COUNTS[resync_missing_archive]=0
|
||||
COUNTS[resync_incomplete_archive]=0
|
||||
COUNTS[review_partial_prod]=0
|
||||
COUNTS[review_nomatch_prod]=0
|
||||
COUNTS[review_parse_failure]=0
|
||||
COUNTS[review_conflicting]=0
|
||||
COUNTS[review_diverged]=0
|
||||
|
||||
# Output arrays
|
||||
declare -a READY_LINES
|
||||
declare -a RESYNC_LINES
|
||||
declare -a REVIEW_LINES
|
||||
|
||||
# Helper function to get context string
|
||||
get_context() {
|
||||
local key="$1"
|
||||
local prod_status="$2"
|
||||
local archive_status="$3"
|
||||
local context=""
|
||||
|
||||
# Check purgatory
|
||||
if [[ -n "${PURGATORY[$key]:-}" ]]; then
|
||||
context="purgatory-expired"
|
||||
fi
|
||||
|
||||
# Check parse failure
|
||||
if [[ -n "${PARSE_FAIL[$key]:-}" ]]; then
|
||||
if [[ -n "$context" ]]; then
|
||||
context="$context, parse-failure"
|
||||
else
|
||||
context="parse-failure"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add archive context for unexpected states
|
||||
if [[ "$prod_status" == "empty" && "$archive_status" != "missing" && "$archive_status" != "empty" ]]; then
|
||||
if [[ -n "$context" ]]; then
|
||||
context="$context, archive-has-data"
|
||||
else
|
||||
context="archive-has-data"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${context:-none}"
|
||||
}
|
||||
|
||||
# Helper to convert category to human-readable status
|
||||
cat_to_status() {
|
||||
case "$1" in
|
||||
cat1) echo "complete" ;;
|
||||
cat2) echo "empty" ;;
|
||||
cat3) echo "partial" ;;
|
||||
cat4) echo "no-match" ;;
|
||||
missing) echo "missing" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
LOOP_COUNT=0
|
||||
set +u
|
||||
for key in "${!ALL_REPOS[@]}"; do
|
||||
LOOP_COUNT=$((LOOP_COUNT + 1))
|
||||
[[ $((LOOP_COUNT % 100)) -eq 0 ]] && log_info "Processed $LOOP_COUNT repos..."
|
||||
IFS='|' read -r repo npub <<< "$key"
|
||||
|
||||
prod_cat="${PROD_CAT[$key]:-missing}"
|
||||
archive_cat="${ARCHIVE_CAT[$key]:-missing}"
|
||||
prod_status=$(cat_to_status "$prod_cat")
|
||||
archive_status=$(cat_to_status "$archive_cat")
|
||||
|
||||
# Decision tree implementation
|
||||
|
||||
# 1. Is there a kind 5 deletion event?
|
||||
if [[ -n "${DELETED[$key]:-}" ]]; then
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | deleted by user")
|
||||
COUNTS[ready_deleted]=$((COUNTS[ready_deleted] + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# 2. What is the prod status?
|
||||
case "$prod_cat" in
|
||||
missing)
|
||||
# Not in prod
|
||||
if [[ "$archive_cat" != "missing" ]]; then
|
||||
# In archive but not in prod -> no action (archive-only)
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | archive-only (not in prod)")
|
||||
COUNTS[ready_archive_only]=$((COUNTS[ready_archive_only] + 1))
|
||||
elif [[ -n "${PURGATORY[$key]:-}" ]]; then
|
||||
# Purgatory only, not in prod -> no action
|
||||
context="purgatory-expired"
|
||||
READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | purgatory-only (not in prod)")
|
||||
COUNTS[ready_not_in_prod]=$((COUNTS[ready_not_in_prod] + 1))
|
||||
fi
|
||||
# Otherwise skip (not a real repo - no data anywhere)
|
||||
;;
|
||||
|
||||
cat2)
|
||||
# Empty in prod -> ALWAYS no action required
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | empty in prod (user never pushed)")
|
||||
COUNTS[ready_empty_prod]=$((COUNTS[ready_empty_prod] + 1))
|
||||
;;
|
||||
|
||||
cat1)
|
||||
# Complete in prod
|
||||
if [[ "$archive_cat" == "cat1" ]]; then
|
||||
# Complete in both -> no action
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | complete in both")
|
||||
COUNTS[ready_complete_both]=$((COUNTS[ready_complete_both] + 1))
|
||||
else
|
||||
# Complete in prod, missing/incomplete in archive
|
||||
# Check for parse failure - if so, needs manual review
|
||||
if [[ -n "${PARSE_FAIL[$key]:-}" ]]; then
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
REVIEW_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | complete in prod with parse failure")
|
||||
COUNTS[review_parse_failure]=$((COUNTS[review_parse_failure] + 1))
|
||||
else
|
||||
# Check git ancestry to see if archive is actually ahead
|
||||
git_relationship="${GIT_ANCESTRY[$key]:-unknown}"
|
||||
|
||||
if [[ "$git_relationship" == "archive-ahead" || "$git_relationship" == "in-sync" ]]; then
|
||||
# Archive has newer/same git data - this is GOOD
|
||||
# Archive's git data was authorized by a state event (GRASP enforced)
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
if [[ -n "$context" && "$context" != "none" ]]; then
|
||||
context="$context, git=$git_relationship"
|
||||
else
|
||||
context="git=$git_relationship"
|
||||
fi
|
||||
READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | archive ahead (use archive data)")
|
||||
COUNTS[ready_archive_ahead]=$((COUNTS[ready_archive_ahead] + 1))
|
||||
elif [[ "$git_relationship" == "diverged" ]]; then
|
||||
# Git histories diverged - needs manual review
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
if [[ -n "$context" && "$context" != "none" ]]; then
|
||||
context="$context, git=diverged"
|
||||
else
|
||||
context="git=diverged"
|
||||
fi
|
||||
REVIEW_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | git histories diverged (manual review)")
|
||||
COUNTS[review_diverged]=$((COUNTS[review_diverged] + 1))
|
||||
else
|
||||
# prod-ahead, archive-only, prod-only, both-empty, or unknown
|
||||
# These need resync - include purgatory context
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
if [[ "$git_relationship" != "unknown" ]]; then
|
||||
if [[ -n "$context" && "$context" != "none" ]]; then
|
||||
context="$context, git=$git_relationship"
|
||||
else
|
||||
context="git=$git_relationship"
|
||||
fi
|
||||
fi
|
||||
if [[ "$archive_cat" == "missing" ]]; then
|
||||
RESYNC_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | trigger re-sync to archive")
|
||||
COUNTS[resync_missing_archive]=$((COUNTS[resync_missing_archive] + 1))
|
||||
else
|
||||
RESYNC_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | trigger re-sync (archive incomplete)")
|
||||
COUNTS[resync_incomplete_archive]=$((COUNTS[resync_incomplete_archive] + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
cat3)
|
||||
# Partial in prod -> ALWAYS manual investigation
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
REVIEW_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | partial in prod (investigate git data)")
|
||||
COUNTS[review_partial_prod]=$((COUNTS[review_partial_prod] + 1))
|
||||
;;
|
||||
|
||||
cat4)
|
||||
# No-match in prod -> ALWAYS manual investigation
|
||||
context=$(get_context "$key" "$prod_status" "$archive_status")
|
||||
REVIEW_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | no-match in prod (git corruption)")
|
||||
COUNTS[review_nomatch_prod]=$((COUNTS[review_nomatch_prod] + 1))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -u
|
||||
|
||||
# ============================================================================
|
||||
# Phase 4: Write output files
|
||||
# ============================================================================
|
||||
|
||||
log_info "Writing output files..."
|
||||
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00")
|
||||
|
||||
# Write ready-for-migration.txt
|
||||
{
|
||||
echo "# Ready for Migration - No action required"
|
||||
echo "# Generated: $TIMESTAMP"
|
||||
echo "# Format: repo | npub | prod_status | archive_status | context | reason"
|
||||
echo "#"
|
||||
set +u
|
||||
for line in "${READY_LINES[@]}"; do
|
||||
echo "$line"
|
||||
done
|
||||
set -u
|
||||
} > "$READY_FILE"
|
||||
|
||||
# Write needs-resync.txt
|
||||
{
|
||||
echo "# Needs Re-sync - Action required"
|
||||
echo "# Generated: $TIMESTAMP"
|
||||
echo "# Format: repo | npub | prod_status | archive_status | context | action"
|
||||
echo "#"
|
||||
echo "# Context meanings:"
|
||||
echo "# purgatory-expired = archive tried to sync but failed (30min timeout)"
|
||||
echo "# none = archive never tried or announcement missing"
|
||||
echo "#"
|
||||
set +u
|
||||
for line in "${RESYNC_LINES[@]}"; do
|
||||
echo "$line"
|
||||
done
|
||||
set -u
|
||||
} > "$RESYNC_FILE"
|
||||
|
||||
# Write manual-review.txt
|
||||
{
|
||||
echo "# Manual Review Required - Investigation needed"
|
||||
echo "# Generated: $TIMESTAMP"
|
||||
echo "# Format: repo | npub | prod_status | archive_status | context | reason"
|
||||
echo "#"
|
||||
set +u
|
||||
for line in "${REVIEW_LINES[@]}"; do
|
||||
echo "$line"
|
||||
done
|
||||
set -u
|
||||
} > "$REVIEW_FILE"
|
||||
|
||||
# ============================================================================
|
||||
# Phase 5: Generate summary
|
||||
# ============================================================================
|
||||
|
||||
log_info "Generating summary..."
|
||||
|
||||
set +u
|
||||
TOTAL_READY="${#READY_LINES[@]}"
|
||||
TOTAL_RESYNC="${#RESYNC_LINES[@]}"
|
||||
TOTAL_REVIEW="${#REVIEW_LINES[@]}"
|
||||
set -u
|
||||
TOTAL=$((TOTAL_READY + TOTAL_RESYNC + TOTAL_REVIEW))
|
||||
|
||||
# Calculate percentages
|
||||
if [[ $TOTAL -gt 0 ]]; then
|
||||
PCT_READY=$(awk "BEGIN {printf \"%.1f\", ($TOTAL_READY / $TOTAL) * 100}")
|
||||
PCT_RESYNC=$(awk "BEGIN {printf \"%.1f\", ($TOTAL_RESYNC / $TOTAL) * 100}")
|
||||
PCT_REVIEW=$(awk "BEGIN {printf \"%.1f\", ($TOTAL_REVIEW / $TOTAL) * 100}")
|
||||
else
|
||||
PCT_READY="0.0"
|
||||
PCT_RESYNC="0.0"
|
||||
PCT_REVIEW="0.0"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Migration Classification Summary"
|
||||
echo "Generated: $TIMESTAMP"
|
||||
echo "Analysis Directory: $ANALYSIS_DIR"
|
||||
echo ""
|
||||
echo "## Overview"
|
||||
echo ""
|
||||
echo "| Category | Count | Percentage |"
|
||||
echo "|----------|-------|------------|"
|
||||
echo "| Ready for Migration | $TOTAL_READY | $PCT_READY% |"
|
||||
echo "| Needs Re-sync | $TOTAL_RESYNC | $PCT_RESYNC% |"
|
||||
echo "| Manual Review | $TOTAL_REVIEW | $PCT_REVIEW% |"
|
||||
echo "| **Total** | **$TOTAL** | **100%** |"
|
||||
echo ""
|
||||
echo "## Tier 1: Ready for Migration ($TOTAL_READY repos)"
|
||||
echo ""
|
||||
echo "These repositories are ready for migration or don't need migration:"
|
||||
echo ""
|
||||
echo "| Reason | Count |"
|
||||
echo "|--------|-------|"
|
||||
echo "| complete in both prod and archive | ${COUNTS[ready_complete_both]} |"
|
||||
echo "| archive ahead (has newer git data) | ${COUNTS[ready_archive_ahead]} |"
|
||||
echo "| deleted by user | ${COUNTS[ready_deleted]} |"
|
||||
echo "| empty in prod (user never pushed) | ${COUNTS[ready_empty_prod]} |"
|
||||
echo "| archive-only (not in prod) | ${COUNTS[ready_archive_only]} |"
|
||||
echo "| purgatory-only (not in prod) | ${COUNTS[ready_not_in_prod]} |"
|
||||
echo ""
|
||||
echo "## Tier 2: Needs Re-sync ($TOTAL_RESYNC repos)"
|
||||
echo ""
|
||||
echo "These repositories need re-sync to archive before migration:"
|
||||
echo ""
|
||||
echo "| Reason | Count | Action |"
|
||||
echo "|--------|-------|--------|"
|
||||
echo "| complete in prod, missing from archive | ${COUNTS[resync_missing_archive]} | trigger re-sync |"
|
||||
echo "| complete in prod, incomplete in archive | ${COUNTS[resync_incomplete_archive]} | trigger re-sync |"
|
||||
echo ""
|
||||
echo "### Purgatory Context"
|
||||
echo ""
|
||||
echo "Repos in needs-resync.txt include purgatory context:"
|
||||
echo "- **purgatory-expired**: Archive tried to sync but failed (30min timeout)"
|
||||
echo "- **none**: Archive never tried or announcement missing"
|
||||
echo ""
|
||||
echo "## Tier 3: Manual Review ($TOTAL_REVIEW repos)"
|
||||
echo ""
|
||||
echo "These repositories require human investigation:"
|
||||
echo ""
|
||||
echo "| Reason | Count |"
|
||||
echo "|--------|-------|"
|
||||
echo "| partial in prod (cat3) | ${COUNTS[review_partial_prod]} |"
|
||||
echo "| no-match in prod (cat4) | ${COUNTS[review_nomatch_prod]} |"
|
||||
echo "| complete in prod with parse failure | ${COUNTS[review_parse_failure]} |"
|
||||
echo "| git histories diverged | ${COUNTS[review_diverged]} |"
|
||||
echo ""
|
||||
echo "## Input Data Summary"
|
||||
echo ""
|
||||
echo "### Prod Categories"
|
||||
echo "- Category 1 (complete): $(wc -l < "$PROD_DIR/category1-complete-match.txt")"
|
||||
echo "- Category 2 (empty): $(wc -l < "$PROD_DIR/category2-empty-blank.txt")"
|
||||
echo "- Category 3 (partial): $(wc -l < "$PROD_DIR/category3-partial-match.txt")"
|
||||
echo "- Category 4 (no match): $(wc -l < "$PROD_DIR/category4-no-match.txt")"
|
||||
echo ""
|
||||
echo "### Archive Categories"
|
||||
echo "- Category 1 (complete): $(wc -l < "$ARCHIVE_DIR/category1-complete-match.txt")"
|
||||
echo "- Category 2 (empty): $(wc -l < "$ARCHIVE_DIR/category2-empty-blank.txt")"
|
||||
echo "- Category 3 (partial): $(wc -l < "$ARCHIVE_DIR/category3-partial-match.txt")"
|
||||
echo "- Category 4 (no match): $(wc -l < "$ARCHIVE_DIR/category4-no-match.txt")"
|
||||
echo ""
|
||||
echo "### Logs"
|
||||
echo "- Parse failures: $(grep -c -v '^#' "$LOGS_DIR/parse-failures.txt" 2>/dev/null || echo 0)"
|
||||
echo "- Purgatory expired: $(grep -c -v '^#' "$LOGS_DIR/purgatory-expired.txt" 2>/dev/null || echo 0)"
|
||||
echo ""
|
||||
echo "## Output Files"
|
||||
echo ""
|
||||
echo "- \`results/ready-for-migration.txt\` - $TOTAL_READY repos ready for migration"
|
||||
echo "- \`results/needs-resync.txt\` - $TOTAL_RESYNC repos needing re-sync"
|
||||
echo "- \`results/manual-review.txt\` - $TOTAL_REVIEW repos needing investigation"
|
||||
echo "- \`results/summary.txt\` - This summary file"
|
||||
echo ""
|
||||
echo "## Recommended Next Steps"
|
||||
echo ""
|
||||
echo "1. **Review needs-resync.txt** - Trigger re-sync for these repos"
|
||||
echo "2. **Review manual-review.txt** - Investigate unusual states"
|
||||
echo "3. **Verify ready-for-migration.txt** - Spot-check a few repos"
|
||||
echo "4. **Plan migration window** - Schedule cutover when action items resolved"
|
||||
} > "$SUMMARY_FILE"
|
||||
|
||||
# ============================================================================
|
||||
# Phase 6: Print summary to console
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
log_success "Classification complete!"
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
echo "Ready for Migration: $TOTAL_READY ($PCT_READY%)"
|
||||
echo " - Complete in both: ${COUNTS[ready_complete_both]}"
|
||||
echo " - Archive ahead: ${COUNTS[ready_archive_ahead]}"
|
||||
echo " - Deleted by user: ${COUNTS[ready_deleted]}"
|
||||
echo " - Empty in prod: ${COUNTS[ready_empty_prod]}"
|
||||
echo " - Archive-only: ${COUNTS[ready_archive_only]}"
|
||||
echo " - Purgatory-only: ${COUNTS[ready_not_in_prod]}"
|
||||
echo ""
|
||||
echo "Needs Re-sync: $TOTAL_RESYNC ($PCT_RESYNC%)"
|
||||
echo " - Missing from archive: ${COUNTS[resync_missing_archive]}"
|
||||
echo " - Incomplete in archive: ${COUNTS[resync_incomplete_archive]}"
|
||||
echo ""
|
||||
echo "Manual Review: $TOTAL_REVIEW ($PCT_REVIEW%)"
|
||||
echo " - Partial in prod: ${COUNTS[review_partial_prod]}"
|
||||
echo " - No-match in prod: ${COUNTS[review_nomatch_prod]}"
|
||||
echo " - Parse failures: ${COUNTS[review_parse_failure]}"
|
||||
echo " - Git diverged: ${COUNTS[review_diverged]}"
|
||||
echo ""
|
||||
echo "Total: $TOTAL repos"
|
||||
echo ""
|
||||
echo "Output files:"
|
||||
echo " $READY_FILE"
|
||||
echo " $RESYNC_FILE"
|
||||
echo " $REVIEW_FILE"
|
||||
echo " $SUMMARY_FILE"
|
||||
@@ -1,779 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-migration-analysis.sh - Orchestrate the complete GRASP relay to ngit-grasp migration analysis
|
||||
#
|
||||
# This script runs all 5 phases of the migration analysis pipeline in sequence,
|
||||
# with proper error handling, progress reporting, and timing information.
|
||||
#
|
||||
# QUICK START:
|
||||
# # Basic usage (local analysis only - Phases 1, 3, 5)
|
||||
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev
|
||||
#
|
||||
# # Full analysis including git sync check (requires VPS access)
|
||||
# ./run-migration-analysis.sh \
|
||||
# --prod-relay wss://relay.ngit.dev \
|
||||
# --archive-relay wss://archive.relay.ngit.dev \
|
||||
# --prod-git /var/lib/grasp-relay/git \
|
||||
# --archive-git /var/lib/ngit-grasp/git
|
||||
#
|
||||
# USAGE:
|
||||
# ./run-migration-analysis.sh [options]
|
||||
#
|
||||
# REQUIRED OPTIONS:
|
||||
# --prod-relay <url> Production relay WebSocket URL (e.g., wss://relay.ngit.dev)
|
||||
# --archive-relay <url> Archive relay WebSocket URL (e.g., wss://archive.relay.ngit.dev)
|
||||
#
|
||||
# OPTIONAL OPTIONS:
|
||||
# --prod-git <path> Git base directory for prod (enables Phase 2)
|
||||
# --archive-git <path> Git base directory for archive (enables Phase 2)
|
||||
# --service <name> Systemd service name for log extraction (enables Phase 4)
|
||||
# --output <dir> Output directory (default: work/migration-analysis-YYYYMMDD-HHMM)
|
||||
# --since <date> Start date for log extraction (default: 30 days ago)
|
||||
# --until <date> End date for log extraction (default: now)
|
||||
#
|
||||
# PHASE CONTROL:
|
||||
# --skip-phase-1 Skip event fetching (use existing data)
|
||||
# --skip-phase-2 Skip git sync check (use existing data)
|
||||
# --skip-phase-3 Skip categorization (use existing data)
|
||||
# --skip-phase-4 Skip log extraction (use existing data)
|
||||
# --skip-phase-5 Skip final classification
|
||||
# --only-phase-N Run only phase N (1-5)
|
||||
# --from-phase-N Start from phase N (skip earlier phases)
|
||||
#
|
||||
# OTHER OPTIONS:
|
||||
# --dry-run Show what would be executed without running
|
||||
# --continue-on-error Continue to next phase even if current phase fails
|
||||
# --help Show this help message
|
||||
#
|
||||
# PHASES:
|
||||
# Phase 1: Fetch events from both relays (~30s each, local)
|
||||
# Phase 2: Check git sync status (~20 min each, requires VPS)
|
||||
# Phase 3: Categorize and compare results (fast, local)
|
||||
# Phase 4: Extract logs from systemd (requires VPS)
|
||||
# Phase 5: Final classification (fast, local)
|
||||
#
|
||||
# EXAMPLES:
|
||||
# # Dry run to see what would happen
|
||||
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev --dry-run
|
||||
#
|
||||
# # Run only Phase 1 (fetch events)
|
||||
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev --only-phase-1
|
||||
#
|
||||
# # Resume from Phase 3 using existing Phase 1-2 data
|
||||
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev --from-phase-3 --output work/migration-analysis-20260122-1430
|
||||
#
|
||||
# # Full analysis on VPS with all features
|
||||
# ./run-migration-analysis.sh \
|
||||
# --prod-relay wss://relay.ngit.dev \
|
||||
# --archive-relay wss://archive.relay.ngit.dev \
|
||||
# --prod-git /var/lib/grasp-relay/git \
|
||||
# --archive-git /var/lib/ngit-grasp/git \
|
||||
# --service ngit-grasp.service
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get script directory for finding other scripts
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
CYAN=''
|
||||
BOLD=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
# Logging functions
|
||||
log_header() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD}${CYAN} $*${NC}"
|
||||
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
log_phase() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${BLUE}┌──────────────────────────────────────────────────────────────┐${NC}"
|
||||
echo -e "${BOLD}${BLUE}│ $*${NC}"
|
||||
echo -e "${BOLD}${BLUE}└──────────────────────────────────────────────────────────────┘${NC}"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN} →${NC} $*" >&2
|
||||
}
|
||||
|
||||
# Default values
|
||||
PROD_RELAY=""
|
||||
ARCHIVE_RELAY=""
|
||||
PROD_GIT=""
|
||||
ARCHIVE_GIT=""
|
||||
SERVICE_NAME=""
|
||||
OUTPUT_DIR=""
|
||||
DRY_RUN=false
|
||||
CONTINUE_ON_ERROR=false
|
||||
LOG_SINCE=""
|
||||
LOG_UNTIL=""
|
||||
|
||||
# Phase control
|
||||
SKIP_PHASE_1=false
|
||||
SKIP_PHASE_2=false
|
||||
SKIP_PHASE_3=false
|
||||
SKIP_PHASE_4=false
|
||||
SKIP_PHASE_5=false
|
||||
ONLY_PHASE=""
|
||||
FROM_PHASE=""
|
||||
|
||||
# Timing
|
||||
declare -A PHASE_TIMES
|
||||
|
||||
usage() {
|
||||
head -73 "$0" | tail -n +3 | sed 's/^# //' | sed 's/^#//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--prod-relay)
|
||||
PROD_RELAY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--archive-relay)
|
||||
ARCHIVE_RELAY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--prod-git)
|
||||
PROD_GIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--archive-git)
|
||||
ARCHIVE_GIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--service)
|
||||
SERVICE_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-phase-1)
|
||||
SKIP_PHASE_1=true
|
||||
shift
|
||||
;;
|
||||
--skip-phase-2)
|
||||
SKIP_PHASE_2=true
|
||||
shift
|
||||
;;
|
||||
--skip-phase-3)
|
||||
SKIP_PHASE_3=true
|
||||
shift
|
||||
;;
|
||||
--skip-phase-4)
|
||||
SKIP_PHASE_4=true
|
||||
shift
|
||||
;;
|
||||
--skip-phase-5)
|
||||
SKIP_PHASE_5=true
|
||||
shift
|
||||
;;
|
||||
--only-phase-1|--only-phase-2|--only-phase-3|--only-phase-4|--only-phase-5)
|
||||
ONLY_PHASE="${1#--only-phase-}"
|
||||
shift
|
||||
;;
|
||||
--from-phase-1|--from-phase-2|--from-phase-3|--from-phase-4|--from-phase-5)
|
||||
FROM_PHASE="${1#--from-phase-}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--continue-on-error)
|
||||
CONTINUE_ON_ERROR=true
|
||||
shift
|
||||
;;
|
||||
--since)
|
||||
LOG_SINCE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--until)
|
||||
LOG_UNTIL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
echo "Use --help for usage information."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Validate required arguments
|
||||
validate_args() {
|
||||
local errors=0
|
||||
|
||||
if [[ -z "$PROD_RELAY" ]]; then
|
||||
log_error "Missing required option: --prod-relay"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
if [[ -z "$ARCHIVE_RELAY" ]]; then
|
||||
log_error "Missing required option: --archive-relay"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
# Validate relay URLs
|
||||
if [[ -n "$PROD_RELAY" && ! "$PROD_RELAY" =~ ^wss?:// ]]; then
|
||||
log_error "Invalid prod relay URL: $PROD_RELAY (must start with ws:// or wss://)"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
if [[ -n "$ARCHIVE_RELAY" && ! "$ARCHIVE_RELAY" =~ ^wss?:// ]]; then
|
||||
log_error "Invalid archive relay URL: $ARCHIVE_RELAY (must start with ws:// or wss://)"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
# Validate git paths if provided
|
||||
if [[ -n "$PROD_GIT" && ! -d "$PROD_GIT" ]]; then
|
||||
log_warn "Prod git directory not found: $PROD_GIT"
|
||||
log_warn "Phase 2 will fail unless running on VPS with access to this path."
|
||||
fi
|
||||
|
||||
if [[ -n "$ARCHIVE_GIT" && ! -d "$ARCHIVE_GIT" ]]; then
|
||||
log_warn "Archive git directory not found: $ARCHIVE_GIT"
|
||||
log_warn "Phase 2 will fail unless running on VPS with access to this path."
|
||||
fi
|
||||
|
||||
if [[ $errors -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "Use --help for usage information."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
local missing=0
|
||||
|
||||
log_info "Checking prerequisites..."
|
||||
|
||||
# Required tools
|
||||
for tool in git nak jq awk sort; do
|
||||
if command -v "$tool" &> /dev/null; then
|
||||
log_step "$tool: found"
|
||||
else
|
||||
log_error "$tool: NOT FOUND"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Optional tools
|
||||
if command -v journalctl &> /dev/null; then
|
||||
log_step "journalctl: found (Phase 4 available)"
|
||||
else
|
||||
log_step "journalctl: not found (Phase 4 will be skipped)"
|
||||
SKIP_PHASE_4=true
|
||||
fi
|
||||
|
||||
if [[ $missing -eq 1 ]]; then
|
||||
log_error "Missing required tools. Install them and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check scripts exist
|
||||
for script in 01-fetch-events.sh 10-check-git-sync.sh 20-categorize.sh 21-compare-relays.sh 22-compare-git-data.sh 30-extract-parse-failures.sh 31-extract-purgatory-expiry.sh 40-classify-actions.sh; do
|
||||
if [[ ! -x "$SCRIPT_DIR/$script" ]]; then
|
||||
log_error "Script not found or not executable: $SCRIPT_DIR/$script"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $missing -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "All prerequisites satisfied"
|
||||
}
|
||||
|
||||
# Determine which phases to run
|
||||
determine_phases() {
|
||||
# Handle --only-phase-N
|
||||
if [[ -n "$ONLY_PHASE" ]]; then
|
||||
for i in 1 2 3 4 5; do
|
||||
if [[ "$i" != "$ONLY_PHASE" ]]; then
|
||||
eval "SKIP_PHASE_$i=true"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Handle --from-phase-N
|
||||
if [[ -n "$FROM_PHASE" ]]; then
|
||||
for i in 1 2 3 4 5; do
|
||||
if [[ "$i" -lt "$FROM_PHASE" ]]; then
|
||||
eval "SKIP_PHASE_$i=true"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Auto-skip Phase 2 if git paths not provided
|
||||
if [[ -z "$PROD_GIT" && -z "$ARCHIVE_GIT" ]]; then
|
||||
if [[ "$SKIP_PHASE_2" != "true" ]]; then
|
||||
log_warn "No git paths provided. Phase 2 (git sync check) will be skipped."
|
||||
log_warn "Use --prod-git and --archive-git to enable Phase 2."
|
||||
SKIP_PHASE_2=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Auto-skip Phase 4 if service not provided
|
||||
if [[ -z "$SERVICE_NAME" ]]; then
|
||||
if [[ "$SKIP_PHASE_4" != "true" ]]; then
|
||||
log_warn "No service name provided. Phase 4 (log extraction) will be skipped."
|
||||
log_warn "Use --service to enable Phase 4."
|
||||
SKIP_PHASE_4=true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup output directory
|
||||
setup_output_dir() {
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
OUTPUT_DIR="work/migration-analysis-$(date +%Y%m%d-%H%M)"
|
||||
fi
|
||||
|
||||
log_info "Output directory: $OUTPUT_DIR"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY RUN] Would create directory structure"
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"/{prod/raw,archive/raw,comparison,logs,results}
|
||||
|
||||
# Save configuration
|
||||
cat > "$OUTPUT_DIR/config.txt" << EOF
|
||||
# Migration Analysis Configuration
|
||||
# Generated: $(date -Iseconds)
|
||||
|
||||
PROD_RELAY=$PROD_RELAY
|
||||
ARCHIVE_RELAY=$ARCHIVE_RELAY
|
||||
PROD_GIT=$PROD_GIT
|
||||
ARCHIVE_GIT=$ARCHIVE_GIT
|
||||
SERVICE_NAME=$SERVICE_NAME
|
||||
OUTPUT_DIR=$OUTPUT_DIR
|
||||
EOF
|
||||
|
||||
log_success "Created output directory structure"
|
||||
}
|
||||
|
||||
# Run a phase with timing and error handling
|
||||
run_phase() {
|
||||
local phase_num="$1"
|
||||
local phase_name="$2"
|
||||
shift 2
|
||||
local cmd=("$@")
|
||||
|
||||
local skip_var="SKIP_PHASE_$phase_num"
|
||||
if [[ "${!skip_var}" == "true" ]]; then
|
||||
log_phase "Phase $phase_num: $phase_name [SKIPPED]"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_phase "Phase $phase_num: $phase_name"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY RUN] Would execute:"
|
||||
for c in "${cmd[@]}"; do
|
||||
echo " $c"
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
local exit_code=0
|
||||
|
||||
# Execute the command(s)
|
||||
for c in "${cmd[@]}"; do
|
||||
log_step "Running: $c"
|
||||
if ! eval "$c"; then
|
||||
exit_code=1
|
||||
if [[ "$CONTINUE_ON_ERROR" == "true" ]]; then
|
||||
log_warn "Command failed, continuing due to --continue-on-error"
|
||||
else
|
||||
log_error "Command failed"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
PHASE_TIMES[$phase_num]=$duration
|
||||
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
log_success "Phase $phase_num completed in ${duration}s"
|
||||
else
|
||||
log_error "Phase $phase_num failed after ${duration}s"
|
||||
if [[ "$CONTINUE_ON_ERROR" != "true" ]]; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# Phase 1: Fetch events
|
||||
run_phase_1() {
|
||||
local cmds=()
|
||||
|
||||
# Fetch from prod relay
|
||||
cmds+=("'$SCRIPT_DIR/01-fetch-events.sh' '$PROD_RELAY' '$OUTPUT_DIR/prod'")
|
||||
|
||||
# Fetch from archive relay
|
||||
cmds+=("'$SCRIPT_DIR/01-fetch-events.sh' '$ARCHIVE_RELAY' '$OUTPUT_DIR/archive'")
|
||||
|
||||
run_phase 1 "Fetch Events (~30s each)" "${cmds[@]}"
|
||||
}
|
||||
|
||||
# Phase 2: Git sync check
|
||||
run_phase_2() {
|
||||
local cmds=()
|
||||
|
||||
if [[ -n "$PROD_GIT" ]]; then
|
||||
cmds+=("'$SCRIPT_DIR/10-check-git-sync.sh' '$OUTPUT_DIR/prod/raw/state-events.json' '$PROD_GIT' '$OUTPUT_DIR/prod' --categorize")
|
||||
else
|
||||
log_warn "Skipping prod git sync check (no --prod-git provided)"
|
||||
fi
|
||||
|
||||
if [[ -n "$ARCHIVE_GIT" ]]; then
|
||||
cmds+=("'$SCRIPT_DIR/10-check-git-sync.sh' '$OUTPUT_DIR/archive/raw/state-events.json' '$ARCHIVE_GIT' '$OUTPUT_DIR/archive' --categorize")
|
||||
else
|
||||
log_warn "Skipping archive git sync check (no --archive-git provided)"
|
||||
fi
|
||||
|
||||
if [[ ${#cmds[@]} -eq 0 ]]; then
|
||||
log_warn "No git paths provided, skipping Phase 2"
|
||||
return 0
|
||||
fi
|
||||
|
||||
run_phase 2 "Git Sync Check (~20 min each)" "${cmds[@]}"
|
||||
}
|
||||
|
||||
# Phase 3: Categorize and compare
|
||||
run_phase_3() {
|
||||
local cmds=()
|
||||
|
||||
# Check if we have git-sync-status.tsv files (from Phase 2)
|
||||
# If not, we can't run categorization
|
||||
local has_prod_sync=false
|
||||
local has_archive_sync=false
|
||||
|
||||
if [[ -f "$OUTPUT_DIR/prod/git-sync-status.tsv" ]]; then
|
||||
has_prod_sync=true
|
||||
fi
|
||||
|
||||
if [[ -f "$OUTPUT_DIR/archive/git-sync-status.tsv" ]]; then
|
||||
has_archive_sync=true
|
||||
fi
|
||||
|
||||
# Run categorization if we have sync data but no category files
|
||||
if [[ "$has_prod_sync" == "true" && ! -f "$OUTPUT_DIR/prod/category1-complete-match.txt" ]]; then
|
||||
cmds+=("'$SCRIPT_DIR/20-categorize.sh' '$OUTPUT_DIR/prod/git-sync-status.tsv' '$OUTPUT_DIR/prod'")
|
||||
fi
|
||||
|
||||
if [[ "$has_archive_sync" == "true" && ! -f "$OUTPUT_DIR/archive/category1-complete-match.txt" ]]; then
|
||||
cmds+=("'$SCRIPT_DIR/20-categorize.sh' '$OUTPUT_DIR/archive/git-sync-status.tsv' '$OUTPUT_DIR/archive'")
|
||||
fi
|
||||
|
||||
# Run comparison if we have category files
|
||||
if [[ -f "$OUTPUT_DIR/prod/category1-complete-match.txt" && -f "$OUTPUT_DIR/archive/category1-complete-match.txt" ]]; then
|
||||
cmds+=("'$SCRIPT_DIR/21-compare-relays.sh' '$OUTPUT_DIR/prod' '$OUTPUT_DIR/archive' '$OUTPUT_DIR/comparison'")
|
||||
else
|
||||
log_warn "Missing category files for comparison."
|
||||
log_warn "Phase 2 must complete successfully before Phase 3 can compare relays."
|
||||
|
||||
# Create placeholder comparison files if they don't exist
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
mkdir -p "$OUTPUT_DIR/comparison"
|
||||
for f in complete-in-both.txt complete-prod-missing-archive.txt complete-prod-incomplete-archive.txt incomplete-in-both.txt in-archive-not-prod.txt; do
|
||||
if [[ ! -f "$OUTPUT_DIR/comparison/$f" ]]; then
|
||||
echo "# Placeholder - Phase 2 data not available" > "$OUTPUT_DIR/comparison/$f"
|
||||
fi
|
||||
done
|
||||
echo "# Comparison not available - Phase 2 data missing" > "$OUTPUT_DIR/comparison/summary.txt"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#cmds[@]} -eq 0 ]]; then
|
||||
log_warn "No categorization or comparison needed (already done or missing input)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
run_phase 3 "Categorize & Compare (fast)" "${cmds[@]}"
|
||||
|
||||
# Phase 3c: Compare git data between relays (requires git paths)
|
||||
# This determines if archive is ahead of prod for repos with mismatched state
|
||||
if [[ -n "$PROD_GIT" && -n "$ARCHIVE_GIT" ]]; then
|
||||
# Build list of repos to compare: those where prod=complete but archive is not
|
||||
local repos_to_compare="$OUTPUT_DIR/comparison/complete-prod-incomplete-archive.txt"
|
||||
if [[ -f "$repos_to_compare" ]] && [[ ! -f "$OUTPUT_DIR/comparison/git-ancestry.tsv" ]]; then
|
||||
log_info "Running git ancestry comparison (Phase 3c)..."
|
||||
run_phase 3 "Git Ancestry Comparison" "'$SCRIPT_DIR/22-compare-git-data.sh' '$PROD_GIT' '$ARCHIVE_GIT' '$repos_to_compare' '$OUTPUT_DIR/comparison'"
|
||||
fi
|
||||
else
|
||||
log_warn "Git paths not provided - skipping git ancestry comparison"
|
||||
log_warn "Without git comparison, repos where archive is ahead will be incorrectly flagged as needing re-sync"
|
||||
fi
|
||||
}
|
||||
|
||||
# Phase 4: Extract logs
|
||||
run_phase_4() {
|
||||
if [[ -z "$SERVICE_NAME" ]]; then
|
||||
log_warn "No service name provided, skipping Phase 4"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Validate service name before running Phase 4
|
||||
# Structured logging only exists in ngit-grasp, not ngit-relay
|
||||
if [[ "$SERVICE_NAME" == *"ngit-relay"* ]]; then
|
||||
log_error "SERVICE_NAME appears to be ngit-relay: $SERVICE_NAME"
|
||||
log_error ""
|
||||
log_error "Phase 4 requires an ngit-grasp service with structured logging."
|
||||
log_error "Structured logging ([PARSE_FAIL], [PURGATORY_EXPIRED]) only exists"
|
||||
log_error "in ngit-grasp services, NOT in ngit-relay services."
|
||||
log_error ""
|
||||
log_error "Please update --service to use the ngit-grasp archive service."
|
||||
log_error ""
|
||||
log_error "To find the correct service name:"
|
||||
log_error " systemctl list-units 'ngit-grasp*' --all"
|
||||
log_error ""
|
||||
log_error "Common ngit-grasp service names:"
|
||||
log_error " - ngit-grasp.service"
|
||||
log_error " - ngit-grasp-relay-ngit-dev.service (NixOS multi-instance)"
|
||||
log_error " - ngit-grasp-archive.service"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Warn if service name doesn't look like ngit-grasp
|
||||
if [[ "$SERVICE_NAME" != *"ngit-grasp"* && "$SERVICE_NAME" != *"grasp"* ]]; then
|
||||
log_warn "SERVICE_NAME doesn't contain 'ngit-grasp': $SERVICE_NAME"
|
||||
log_warn "Structured logging only exists in ngit-grasp services."
|
||||
log_warn "If this is not an ngit-grasp service, Phase 4 will find no logs."
|
||||
fi
|
||||
|
||||
local cmds=()
|
||||
|
||||
# Build log extraction options
|
||||
local log_opts=""
|
||||
if [[ -n "$LOG_SINCE" ]]; then
|
||||
log_opts="$log_opts --since '$LOG_SINCE'"
|
||||
fi
|
||||
if [[ -n "$LOG_UNTIL" ]]; then
|
||||
log_opts="$log_opts --until '$LOG_UNTIL'"
|
||||
fi
|
||||
|
||||
cmds+=("'$SCRIPT_DIR/30-extract-parse-failures.sh' '$SERVICE_NAME' '$OUTPUT_DIR/logs' $log_opts")
|
||||
cmds+=("'$SCRIPT_DIR/31-extract-purgatory-expiry.sh' '$SERVICE_NAME' '$OUTPUT_DIR/logs' $log_opts")
|
||||
|
||||
run_phase 4 "Extract Logs (VPS required)" "${cmds[@]}"
|
||||
}
|
||||
|
||||
# Phase 5: Final classification
|
||||
run_phase_5() {
|
||||
# Check if we have the minimum required files
|
||||
local can_run=true
|
||||
|
||||
if [[ ! -d "$OUTPUT_DIR/prod" ]]; then
|
||||
log_warn "Missing prod directory"
|
||||
can_run=false
|
||||
fi
|
||||
|
||||
if [[ ! -d "$OUTPUT_DIR/archive" ]]; then
|
||||
log_warn "Missing archive directory"
|
||||
can_run=false
|
||||
fi
|
||||
|
||||
if [[ ! -d "$OUTPUT_DIR/comparison" ]]; then
|
||||
log_warn "Missing comparison directory"
|
||||
can_run=false
|
||||
fi
|
||||
|
||||
# Create logs directory with empty files if missing
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
mkdir -p "$OUTPUT_DIR/logs"
|
||||
for f in parse-failures.txt purgatory-expired.txt; do
|
||||
if [[ ! -f "$OUTPUT_DIR/logs/$f" ]]; then
|
||||
echo "# No data - Phase 4 not run" > "$OUTPUT_DIR/logs/$f"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$can_run" == "false" ]]; then
|
||||
log_error "Cannot run Phase 5 - missing required input directories"
|
||||
return 1
|
||||
fi
|
||||
|
||||
run_phase 5 "Final Classification (fast)" "'$SCRIPT_DIR/40-classify-actions.sh' '$OUTPUT_DIR'"
|
||||
}
|
||||
|
||||
# Display summary
|
||||
display_summary() {
|
||||
log_header "Migration Analysis Complete"
|
||||
|
||||
echo "Output Directory: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Phase timing summary
|
||||
echo "Phase Timing:"
|
||||
local total_time=0
|
||||
for phase in 1 2 3 4 5; do
|
||||
local skip_var="SKIP_PHASE_$phase"
|
||||
if [[ "${!skip_var}" == "true" ]]; then
|
||||
echo " Phase $phase: SKIPPED"
|
||||
elif [[ -n "${PHASE_TIMES[$phase]:-}" ]]; then
|
||||
local t="${PHASE_TIMES[$phase]}"
|
||||
echo " Phase $phase: ${t}s"
|
||||
total_time=$((total_time + t))
|
||||
else
|
||||
echo " Phase $phase: N/A"
|
||||
fi
|
||||
done
|
||||
echo " ─────────────"
|
||||
echo " Total: ${total_time}s"
|
||||
echo ""
|
||||
|
||||
# Results summary
|
||||
if [[ -f "$OUTPUT_DIR/results/summary.txt" ]]; then
|
||||
echo "Results Summary:"
|
||||
echo ""
|
||||
# Extract key metrics from summary
|
||||
if grep -q "No Action Required" "$OUTPUT_DIR/results/summary.txt"; then
|
||||
grep -A1 "No Action Required" "$OUTPUT_DIR/results/summary.txt" | head -2
|
||||
fi
|
||||
if grep -q "Action Required" "$OUTPUT_DIR/results/summary.txt"; then
|
||||
grep -A1 "Action Required" "$OUTPUT_DIR/results/summary.txt" | head -2
|
||||
fi
|
||||
if grep -q "Manual Investigation" "$OUTPUT_DIR/results/summary.txt"; then
|
||||
grep -A1 "Manual Investigation" "$OUTPUT_DIR/results/summary.txt" | head -2
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Output files
|
||||
echo "Output Files:"
|
||||
echo " $OUTPUT_DIR/results/no-action-required.txt"
|
||||
echo " $OUTPUT_DIR/results/action-required.txt"
|
||||
echo " $OUTPUT_DIR/results/manual-investigation.txt"
|
||||
echo " $OUTPUT_DIR/results/summary.txt"
|
||||
echo ""
|
||||
|
||||
# Next steps
|
||||
echo "Next Steps:"
|
||||
echo " 1. Review results/summary.txt for overview"
|
||||
echo " 2. Address items in results/action-required.txt"
|
||||
echo " 3. Investigate items in results/manual-investigation.txt"
|
||||
echo " 4. Plan migration window when action items are resolved"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
log_header "GRASP Relay to ngit-grasp Migration Analysis"
|
||||
|
||||
validate_args
|
||||
check_prerequisites
|
||||
determine_phases
|
||||
setup_output_dir
|
||||
|
||||
# Show configuration
|
||||
log_info "Configuration:"
|
||||
log_step "Prod relay: $PROD_RELAY"
|
||||
log_step "Archive relay: $ARCHIVE_RELAY"
|
||||
[[ -n "$PROD_GIT" ]] && log_step "Prod git: $PROD_GIT"
|
||||
[[ -n "$ARCHIVE_GIT" ]] && log_step "Archive git: $ARCHIVE_GIT"
|
||||
[[ -n "$SERVICE_NAME" ]] && log_step "Service: $SERVICE_NAME"
|
||||
log_step "Output: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Show phase plan
|
||||
log_info "Phase Plan:"
|
||||
for phase in 1 2 3 4 5; do
|
||||
local skip_var="SKIP_PHASE_$phase"
|
||||
if [[ "${!skip_var}" == "true" ]]; then
|
||||
log_step "Phase $phase: SKIP"
|
||||
else
|
||||
log_step "Phase $phase: RUN"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_warn "DRY RUN MODE - No changes will be made"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Run phases
|
||||
local overall_exit=0
|
||||
|
||||
run_phase_1 || overall_exit=1
|
||||
run_phase_2 || overall_exit=1
|
||||
run_phase_3 || overall_exit=1
|
||||
run_phase_4 || overall_exit=1
|
||||
run_phase_5 || overall_exit=1
|
||||
|
||||
# Display summary
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
display_summary
|
||||
fi
|
||||
|
||||
if [[ $overall_exit -ne 0 ]]; then
|
||||
log_warn "Some phases failed. Review output for details."
|
||||
fi
|
||||
|
||||
exit $overall_exit
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# validate-service.sh - Validate service name for structured logging
|
||||
#
|
||||
# This helper script validates that a service name is appropriate for
|
||||
# Phase 4 log extraction. Structured logging ([PARSE_FAIL], [PURGATORY_EXPIRED])
|
||||
# only exists in ngit-grasp services, NOT in ngit-relay services.
|
||||
#
|
||||
# USAGE:
|
||||
# Source this script and call the validation function:
|
||||
#
|
||||
# source validate-service.sh
|
||||
# validate_service_for_structured_logging "$SERVICE_NAME" || exit 1
|
||||
#
|
||||
# BACKGROUND:
|
||||
# Phase 4 of the migration analysis extracts structured log entries from
|
||||
# journald. These log entries only exist in ngit-grasp services. If you
|
||||
# accidentally specify an ngit-relay service, Phase 4 will find no logs
|
||||
# and produce empty results.
|
||||
#
|
||||
# This validation prevents that common mistake by:
|
||||
# 1. Checking if the service name contains "ngit-relay" (error)
|
||||
# 2. Warning if the service name doesn't contain "ngit-grasp"
|
||||
# 3. Optionally checking if structured logs actually exist
|
||||
#
|
||||
# SEE ALSO:
|
||||
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
|
||||
# 30-extract-parse-failures.sh - Uses this validation
|
||||
# 31-extract-purgatory-expiry.sh - Uses this validation
|
||||
#
|
||||
|
||||
# Colors for output (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
_VS_RED='\033[0;31m'
|
||||
_VS_YELLOW='\033[0;33m'
|
||||
_VS_NC='\033[0m'
|
||||
else
|
||||
_VS_RED=''
|
||||
_VS_YELLOW=''
|
||||
_VS_NC=''
|
||||
fi
|
||||
|
||||
# Validates that the service name is appropriate for structured logging
|
||||
#
|
||||
# Arguments:
|
||||
# $1 - service_name: The systemd service name to validate
|
||||
# $2 - check_logs: Whether to check if logs actually exist (default: "true")
|
||||
# $3 - interactive: Whether to prompt for confirmation (default: "true")
|
||||
#
|
||||
# Returns:
|
||||
# 0 - Service is valid for structured logging
|
||||
# 1 - Service is invalid or user declined to continue
|
||||
#
|
||||
# Example:
|
||||
# validate_service_for_structured_logging "ngit-grasp.service" || exit 1
|
||||
# validate_service_for_structured_logging "ngit-grasp.service" "false" # Skip log check
|
||||
# validate_service_for_structured_logging "ngit-grasp.service" "true" "false" # Non-interactive
|
||||
#
|
||||
validate_service_for_structured_logging() {
|
||||
local service_name="$1"
|
||||
local check_logs="${2:-true}"
|
||||
local interactive="${3:-true}"
|
||||
|
||||
# Check if service name looks like ngit-relay (ERROR - wrong service type)
|
||||
if [[ "$service_name" == *"ngit-relay"* ]]; then
|
||||
echo -e "${_VS_RED}ERROR: Service name appears to be ngit-relay: $service_name${_VS_NC}" >&2
|
||||
echo "" >&2
|
||||
echo "Structured logging ([PARSE_FAIL], [PURGATORY_EXPIRED]) only exists in" >&2
|
||||
echo "ngit-grasp services, NOT in ngit-relay services." >&2
|
||||
echo "" >&2
|
||||
echo "Please use the ngit-grasp archive service instead." >&2
|
||||
echo "" >&2
|
||||
echo "To find the correct service name:" >&2
|
||||
echo " systemctl list-units 'ngit-grasp*' --all" >&2
|
||||
echo "" >&2
|
||||
echo "Common ngit-grasp service names:" >&2
|
||||
echo " - ngit-grasp.service" >&2
|
||||
echo " - ngit-grasp-relay-ngit-dev.service (NixOS multi-instance)" >&2
|
||||
echo " - ngit-grasp-archive.service" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if service name looks like ngit-grasp (WARNING if not)
|
||||
if [[ "$service_name" != *"ngit-grasp"* && "$service_name" != *"grasp"* ]]; then
|
||||
echo -e "${_VS_YELLOW}WARNING: Service name doesn't contain 'ngit-grasp': $service_name${_VS_NC}" >&2
|
||||
echo "" >&2
|
||||
echo "Structured logging ([PARSE_FAIL], [PURGATORY_EXPIRED]) only exists in" >&2
|
||||
echo "ngit-grasp services." >&2
|
||||
echo "" >&2
|
||||
|
||||
if [[ "$interactive" == "true" ]]; then
|
||||
read -p "Continue anyway? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "Non-interactive mode: proceeding despite warning" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Optionally check if structured logs actually exist
|
||||
if [[ "$check_logs" == "true" ]]; then
|
||||
# Check if journalctl is available
|
||||
if ! command -v journalctl &> /dev/null; then
|
||||
echo -e "${_VS_YELLOW}WARNING: journalctl not available, cannot verify logs exist${_VS_NC}" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for structured log entries
|
||||
# IMPORTANT: Use --no-pager to prevent hanging when run non-interactively (e.g., via SSH)
|
||||
local has_parse_fail has_purgatory
|
||||
has_parse_fail=$(journalctl --no-pager -u "$service_name" --since "7 days ago" 2>/dev/null | grep -c '\[PARSE_FAIL\]' || echo "0")
|
||||
has_purgatory=$(journalctl --no-pager -u "$service_name" --since "7 days ago" 2>/dev/null | grep -c '\[PURGATORY_EXPIRED\]' || echo "0")
|
||||
|
||||
# Strip any non-numeric characters (grep -c can have trailing whitespace)
|
||||
has_parse_fail="${has_parse_fail//[^0-9]/}"
|
||||
has_purgatory="${has_purgatory//[^0-9]/}"
|
||||
has_parse_fail="${has_parse_fail:-0}"
|
||||
has_purgatory="${has_purgatory:-0}"
|
||||
|
||||
if [[ "$has_parse_fail" -eq 0 && "$has_purgatory" -eq 0 ]]; then
|
||||
echo -e "${_VS_YELLOW}WARNING: No structured logs found in $service_name (last 7 days)${_VS_NC}" >&2
|
||||
echo "" >&2
|
||||
echo "This may indicate:" >&2
|
||||
echo " 1. Wrong service (should be ngit-grasp archive service, not ngit-relay)" >&2
|
||||
echo " 2. Structured logging not yet deployed to this ngit-grasp instance" >&2
|
||||
echo " 3. No parse failures or purgatory expiry events in the time window" >&2
|
||||
echo "" >&2
|
||||
echo "To verify you have the right service:" >&2
|
||||
echo " systemctl list-units 'ngit-grasp*' --all" >&2
|
||||
echo " journalctl -u <service> | grep -E '\\[PARSE_FAIL\\]|\\[PURGATORY_EXPIRED\\]' | head -5" >&2
|
||||
echo "" >&2
|
||||
|
||||
if [[ "$interactive" == "true" ]]; then
|
||||
read -p "Continue anyway? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "Non-interactive mode: proceeding despite warning" >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Export the function so it can be used after sourcing
|
||||
export -f validate_service_for_structured_logging
|
||||
@@ -1,47 +0,0 @@
|
||||
# gitnostr.com sync startup soak — 2026-09-23
|
||||
|
||||
Deployed ngit-grasp `56889bfc1df28282e1edfa229ee0029584fc9353` to
|
||||
nixos-fi1's gitnostr.com instance with `deploy . --remote-build --skip-checks`.
|
||||
Activation was confirmed at 15:21:50 UTC. The deployment pin is nixos-fi1
|
||||
commit `44f3c21`. Other GRASP instances retained their original start times.
|
||||
|
||||
## Observations
|
||||
|
||||
The final sample at 15:37:24 UTC covered 15 minutes 34 seconds:
|
||||
|
||||
- 126 connected relays after 49 seconds; 176 at approximately 14 minutes.
|
||||
Final sample: 168 connected / 305 tracked, with 137 fully caught up,
|
||||
26 syncing history, and 5 connected with historic-sync failures.
|
||||
- No service restarts, panics, nonzero persistence-error windows, or
|
||||
`[PARSE_FAIL]` messages. Public NIP-11 and WebSocket repository reads passed.
|
||||
- 26 transient watchdog expirations and 66 incomplete-batch retirements.
|
||||
Timeout cleanup appeared promptly in the actor while other relays continued
|
||||
syncing. Not every watchdog needs a batch-retirement log: a batch may already
|
||||
have been removed by another terminal or disconnect.
|
||||
- 173,153 hydration deliveries, predominantly duplicates; 85 invalid outcomes
|
||||
and no retryable rejection or persistence-error outcomes in the sampled
|
||||
hydration metrics.
|
||||
- Service cgroup memory grew from 1.90 GiB at 49 seconds to 3.03 GiB at the
|
||||
final sample. Discovery was still active. This soak does not establish a
|
||||
long-term memory plateau or demonstrate a leak.
|
||||
- Startup integrity reported the same 60 unique PR references requiring manual
|
||||
inspection as the previous deployment; no new references appeared.
|
||||
|
||||
## Remaining issues
|
||||
|
||||
1. Forty `filter item too large` refusals across nos.lol, relay.damus.io,
|
||||
and strfry.bonsai.com. The refusal classifier does not recognize this
|
||||
deterministic filter incompatibility, so incomplete-batch retirement allows
|
||||
unchanged work to be retried. Add classification/cooldown coverage before
|
||||
considering the soak completely clean.
|
||||
2. nos.lol twice failed complete live extension because it needed 20 and then
|
||||
22 slots against 18 usable slots. Investigate budget-aware regrouping or
|
||||
fallback; a connected transport alone does not prove complete coverage.
|
||||
3. Continue observing memory beyond startup and the existing integrity findings.
|
||||
|
||||
The global startup stall is substantially improved, and timeout/invalid-PR
|
||||
behavior matches the intended fixes. The deployment remains active. No further
|
||||
application changes were made during this soak.
|
||||
|
||||
Raw local evidence is under `work/soak-56889bf/` (gitignored): timestamped
|
||||
metrics/state snapshots, current and previous startup journals, and read probes.
|
||||
@@ -51,8 +51,6 @@
|
||||
- ✅ **No deletion events**: Direct database cleanup, no KIND 5 events
|
||||
- ✅ **Production safe**: Won't interfere with real events
|
||||
|
||||
**Reference:** See `docs/archive/2025-11-04-tag-migration.md`
|
||||
|
||||
---
|
||||
|
||||
### Standard "t" Tags vs Custom Tags
|
||||
@@ -259,8 +257,6 @@ grasp-audit audit --relay ws://localhost:7000
|
||||
- ✅ Old events auto-expire via cleanup
|
||||
- ✅ No production deployments affected
|
||||
|
||||
**Reference:** `docs/archive/2025-11-04-tag-migration.md`
|
||||
|
||||
---
|
||||
|
||||
### 2. Test Data Lifecycle Matters
|
||||
|
||||
@@ -27,8 +27,6 @@ nix-shell --run "cargo build"
|
||||
- `nix develop` looks for `flake.nix`
|
||||
- We migrated from `shell.nix` to `flake.nix` on November 4, 2025
|
||||
|
||||
**Related:** See `docs/archive/2025-11-04-flake-migration.md`
|
||||
|
||||
---
|
||||
|
||||
## Flake Structure
|
||||
@@ -192,8 +190,6 @@ pkgs.mkShell {
|
||||
5. Remove `shell.nix`
|
||||
6. Commit changes
|
||||
|
||||
**Reference:** See `docs/archive/2025-11-04-flake-migration.md`
|
||||
|
||||
---
|
||||
|
||||
## Benefits of Flakes
|
||||
@@ -401,7 +397,6 @@ nix build
|
||||
- **Nix Flakes Manual**: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html
|
||||
- **rust-overlay**: https://github.com/oxalica/rust-overlay
|
||||
- **flake-utils**: https://github.com/numtide/flake-utils
|
||||
- **Our Migration**: `docs/archive/2025-11-04-flake-migration.md`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -543,8 +543,6 @@ When upgrading from 0.35 to 0.43:
|
||||
- [ ] Verify all builds pass
|
||||
- [ ] Run integration tests
|
||||
|
||||
**Reference:** See `docs/archive/2025-11-04-nostr-sdk-upgrade.md`
|
||||
|
||||
---
|
||||
|
||||
## Useful Resources
|
||||
|
||||
+10
-88
@@ -1,96 +1,18 @@
|
||||
# Work Directory
|
||||
|
||||
**Purpose:** Temporary working documents during development sessions
|
||||
**Lifecycle:** Created during session → Archived at session end
|
||||
**Status:** `.gitignore`d - not committed to version control
|
||||
Use this directory for session notes, investigation reports, deployment checks,
|
||||
soak results, plans, and temporary analysis files. Everything except this README
|
||||
is ignored by Git.
|
||||
|
||||
---
|
||||
Keep operational reports here; do not commit them. Use descriptive filenames
|
||||
or dated subdirectories so retained evidence is easy to find. Delete obsolete
|
||||
files when they are no longer needed.
|
||||
|
||||
## What Goes Here
|
||||
Write lasting product documentation directly in the appropriate directory under
|
||||
`docs/`: tutorials, how-to guides, reference, or explanation. Permanent docs
|
||||
should not link to ignored working files.
|
||||
|
||||
- Session summaries and notes
|
||||
- Status reports and visual summaries
|
||||
- Migration documentation (during migration)
|
||||
- Planning documents
|
||||
- Temporary analysis files
|
||||
- **active-issues/**: Issues discovered during production sync testing (see [docs/how-to/production-sync-testing.md](../docs/how-to/production-sync-testing.md))
|
||||
|
||||
**Rule:** Nothing in this directory should be permanent. Archive or delete at session end.
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### During Session
|
||||
```bash
|
||||
# Create working docs here
|
||||
echo "Session notes..." > work/session-notes.md
|
||||
echo "Status..." > work/status.md
|
||||
```
|
||||
|
||||
### End of Session
|
||||
```bash
|
||||
# Archive important docs
|
||||
mv work/session-notes.md docs/archive/2025-11-04-session-notes.md
|
||||
|
||||
# Delete obsolete docs
|
||||
rm work/status.md
|
||||
|
||||
# Clean up
|
||||
rm -rf work/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## .gitignore
|
||||
|
||||
This directory is ignored by git (except this README):
|
||||
|
||||
```
|
||||
```gitignore
|
||||
work/*
|
||||
!work/README.md
|
||||
```
|
||||
|
||||
**Why:** Working documents are session-specific and shouldn't clutter the repository.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
**DO:**
|
||||
- ✅ Use for temporary session work
|
||||
- ✅ Use descriptive names
|
||||
- ✅ Archive valuable content before deleting
|
||||
- ✅ Clean up at session end
|
||||
|
||||
**DON'T:**
|
||||
- ❌ Put permanent documentation here
|
||||
- ❌ Reference work/ docs from permanent docs
|
||||
- ❌ Commit work/ contents to git
|
||||
- ❌ Let it accumulate files
|
||||
|
||||
---
|
||||
|
||||
## Alternative: Session-Specific Directories
|
||||
|
||||
For complex sessions, create dated subdirectories:
|
||||
|
||||
```bash
|
||||
work/
|
||||
├── 2025-11-04-diataxis-migration/
|
||||
│ ├── notes.md
|
||||
│ ├── checklist.md
|
||||
│ └── visual-summary.txt
|
||||
└── 2025-11-05-feature-x/
|
||||
└── plan.md
|
||||
```
|
||||
|
||||
Archive the entire directory when done:
|
||||
```bash
|
||||
tar czf docs/archive/2025-11-04-diataxis-migration.tar.gz work/2025-11-04-diataxis-migration/
|
||||
rm -rf work/2025-11-04-diataxis-migration/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*This README is the only file in work/ committed to git.*
|
||||
|
||||
Reference in New Issue
Block a user