portail 2.1.0

Unified proxy/gateway: AI Gateway + MCP Gateway + CDN cache
Documentation
use std::io::{self, Write};

const BLOCKS: &[(&str, &str)] = &[
    (
        "Overview",
        r#"
This guide sets up branch protection for `peterlodri-sec/portail` to ensure:
- Only maintainers and agents can merge to `main`
- Self-hosted runners are protected from fork PRs
- All changes require review and passing CI
"#,
    ),
    (
        "Step 1: Enable Branch Protection Rules",
        r#"
Go to: Settings → Branches → Add branch protection rule

Branch name pattern: main

✅ Require a pull request before merging
  ✅ Require approvals (1)
  ✅ Dismiss stale pull request approvals when new commits are pushed
  ✅ Require review from Code Owners

✅ Require status checks to pass before merging
  ✅ Require branches to be up to date before merging
  Status checks:
    - check-linux
    - check-macos
    - nix
    - python

✅ Require conversation resolution before merging
✅ Require signed commits
✅ Require linear history
✅ Do not allow bypassing the above settings

✅ Restrict who can push to matching branches
  ✅ Specify actors: peterlodri-sec

❌ Allow force pushes (disabled)
❌ Allow deletions (disabled)
"#,
    ),
    (
        "Step 2: Enable Tag Protection",
        r#"
Go to: Settings → Tags → Add tag protection rule

Tag name pattern: v*

This prevents unauthorized users from pushing version tags.
Only maintainers can create releases.
"#,
    ),
    (
        "Step 3: Configure CODEOWNERS",
        r#"
Create file: .github/CODEOWNERS

Contents:
```
# Default owners for everything
* @peterlodri-sec

# CI/CD workflows
.github/ @peterlodri-sec

# Nix configuration
nix/ @peterlodri-sec

# Security-sensitive files
SECURITY.md @peterlodri-sec
```
"#,
    ),
    (
        "Step 4: Configure Merge Options",
        r#"
Go to: Settings → General → Pull Requests

✅ Allow merge commits (default to merge commit)
✅ Allow squash merging (default to squash merge)
✅ Allow rebase merging (default to rebase merge)
✅ Always suggest updating pull request branches
✅ Allow auto-merge
✅ Automatically delete head branches
"#,
    ),
    (
        "Step 5: Enable Security Features",
        r#"
Go to: Settings → Security

Code security and analysis:
  ✅ Dependabot alerts
  ✅ Dependabot security updates
  ✅ Dependabot version updates
  ✅ Code scanning (CodeQL)
  ✅ Secret scanning
  ✅ Secret scanning push protection
"#,
    ),
    (
        "Step 6: Configure Runner Permissions",
        r#"
Go to: Settings → Actions → General

Actions permissions:
  ● Allow select actions and reusable workflows
    ✅ Allow actions created by GitHub
    ✅ Allow actions by Marketplace verified creators

Workflow permissions:
  ● Read repository contents and packages permissions

Fork pull request workflows:
  ✅ Require approval for first-time contributors
  ✅ Require approval for all outside collaborators

Run workflows from fork pull requests:
  ✅ (enabled - protected by workflow logic)
"#,
    ),
    (
        "Step 7: Security Model",
        r#"
┌─────────────────────────────────────────────────────────────┐
│                    CI Access Control                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Push to main ──────────▶ Self-hosted runner              │
│   (maintainers/agents)      (trusted)                       │
│                                                             │
│   PR from same repo ─────▶ Self-hosted runner              │
│   (collaborators)           (trusted)                       │
│                                                             │
│   PR from fork ──────────▶ GitHub-hosted runner            │
│   (anyone)                  (safe, isolated)                │
│                                                             │
│   Tag push (v*) ─────────▶ Self-hosted runner              │
│   (maintainers only)        (release builds)                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
"#,
    ),
    (
        "Step 8: Verify Protection",
        r#"
Test with a fork PR:
1. Fork the repo to another account
2. Create a PR from the fork
3. Verify: PR uses GitHub-hosted runners (not self-hosted)

Test with a same-repo PR:
1. Create a branch in the main repo
2. Create a PR
3. Verify: PR uses self-hosted runners

Commands to verify:
```bash
gh run list --workflow ci.yml --limit 10
gh run view <run-id> --log | grep "runs-on"
```
"#,
    ),
    (
        "Quick Reference",
        r#"
┌─────────────────────────────────────────────────────────────┐
│                    Settings Summary                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Branch pattern:     main                                  │
│   Required approvals: 1                                     │
│   Status checks:      check-linux, check-macos, nix, python │
│   Tag pattern:        v*                                    │
│   Force push:         Disabled                              │
│   Delete branch:      Disabled                              │
│   Signed commits:     Required                              │
│   Linear history:     Required                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Commands:
```bash
gh api repos/peterlodri-sec/portail/branches/main/protection
gh api repos/peterlodri-sec/portail/branches --jq '.[].name'
gh api repos/peterlodri-sec/portail/tags/protection
```
"#,
    ),
];

pub fn run_guide() -> io::Result<()> {
    let total = BLOCKS.len();
    let mut current = 0;

    println!("\n╔════════════════════════════════════════════════════════════╗");
    println!("║          Branch Protection E2E Guide                      ║");
    println!(
        "{} blocks — type 'help' for commands              ║",
        total
    );
    println!("╚════════════════════════════════════════════════════════════╝\n");

    print_block(current, total);

    loop {
        print!("\n> ");
        io::stdout().flush()?;

        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        let input = input.trim().to_lowercase();

        match input.as_str() {
            "n" | "next" | "" => {
                if current < total - 1 {
                    current += 1;
                    print_block(current, total);
                } else {
                    println!("\n✓ End of guide. Type 'exit' to quit.\n");
                }
            }
            "p" | "prev" | "back" => {
                if current > 0 {
                    current -= 1;
                    print_block(current, total);
                } else {
                    println!("\n← Already at the beginning.\n");
                }
            }
            "f" | "first" => {
                current = 0;
                print_block(current, total);
            }
            "l" | "last" => {
                current = total - 1;
                print_block(current, total);
            }
            "j" | "jump" => {
                print!("Jump to block (1-{}): ", total);
                io::stdout().flush()?;
                let mut num = String::new();
                io::stdin().read_line(&mut num)?;
                if let Ok(n) = num.trim().parse::<usize>() {
                    if n >= 1 && n <= total {
                        current = n - 1;
                        print_block(current, total);
                    } else {
                        println!("\n⚠ Invalid block number.\n");
                    }
                } else {
                    println!("\n⚠ Enter a number.\n");
                }
            }
            "list" => {
                println!("\nBlocks:");
                for (i, (title, _)) in BLOCKS.iter().enumerate() {
                    let marker = if i == current { "" } else { " " };
                    println!("  {} {:2}. {}", marker, i + 1, title);
                }
                println!();
            }
            "h" | "help" | "?" => {
                print_help();
            }
            "q" | "exit" | "quit" => {
                println!("\n✓ Done. Run 'portail guide' anytime.\n");
                break;
            }
            _ => {
                println!("\n⚠ Unknown command. Type 'help' for options.\n");
            }
        }
    }

    Ok(())
}

fn print_block(current: usize, total: usize) {
    let (title, content) = BLOCKS[current];

    println!("\n┌─────────────────────────────────────────────────────────────┐");
    println!("{:2}/{}  {:<53} │", current + 1, total, title);
    println!("└─────────────────────────────────────────────────────────────┘");
    println!("{}", content);
    println!("─────────────────────────────────────────────────────────────");
    if current < total - 1 {
        println!("  [Enter] next  [p] prev  [j] jump  [l] list  [h] help  [q] exit");
    } else {
        println!("  [p] prev  [j] jump  [l] list  [h] help  [q] exit");
    }
}

fn print_help() {
    println!("\n┌─────────────────────────────────────────────────────────────┐");
    println!("│                    Commands                                 │");
    println!("├─────────────────────────────────────────────────────────────┤");
    println!("│                                                             │");
    println!("│  n, next, Enter  — Next block                               │");
    println!("│  p, prev, back   — Previous block                           │");
    println!("│  f, first        — First block                              │");
    println!("│  l, last         — Last block                               │");
    println!("│  j, jump         — Jump to block number                     │");
    println!("│  l, list         — List all blocks                          │");
    println!("│  h, help, ?      — Show this help                           │");
    println!("│  q, exit, quit   — Exit guide                               │");
    println!("│                                                             │");
    println!("└─────────────────────────────────────────────────────────────┘\n");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn blocks_not_empty() {
        assert!(!BLOCKS.is_empty());
    }

    #[test]
    fn blocks_have_content() {
        for (title, content) in BLOCKS {
            assert!(!title.is_empty());
            assert!(!content.is_empty());
        }
    }
}