1#!/usr/bin/env bash
2# setup-github.sh
3# One-shot script to configure the SophaxChat GitHub repository:
4# 1. GitHub Project v2 (public roadmap with 4 milestones)
5# 2. Branch protection ruleset for main
6# 3. GitHub wiki (Home, Protocol, Cryptography pages)
7#
8# Prerequisites:
9# brew install gh
10# gh auth login (needs repo + project + admin:org scopes)
11#
12# Usage:
13# bash scripts/setup-github.sh
14
15set -euo pipefail
16
17REPO="richardmetzner/SophaxChat"
18OWNER="richardmetzner"
19REPO_NAME="SophaxChat"
20SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21WIKI_DIR="$SCRIPT_DIR/../wiki"
22
23# ── Colours ──────────────────────────────────────────────────────────────────
24RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
25ok() { echo -e "${GREEN}✓${NC} $*"; }
26warn() { echo -e "${YELLOW}⚠${NC} $*"; }
27die() { echo -e "${RED}✗${NC} $*" >&2; exit 1; }
28step() { echo -e "\n${YELLOW}▶ $*${NC}"; }
29
30# ── 0. Preflight ─────────────────────────────────────────────────────────────
31step "Preflight checks"
32
33command -v gh >/dev/null 2>&1 || die "gh not found. Install: brew install gh"
34command -v git >/dev/null 2>&1 || die "git not found."
35
36gh auth status 2>/dev/null | grep -q "Logged in" \
37 || die "Not authenticated. Run: gh auth login --scopes repo,project,admin:org"
38
39ok "gh authenticated"
40
41# Verify required scopes (project scope needed for Projects v2)
42SCOPES=$(gh auth status 2>&1 | grep "Token scopes" || true)
43echo " Token scopes: $SCOPES"
44
45# ── 1. GitHub Project v2 ─────────────────────────────────────────────────────
46step "Creating GitHub Project v2 — SophaxChat Roadmap"
47
48# Get owner node ID (works for both user and org)
49OWNER_ID=$(gh api graphql -f query='
50 query($login: String!) {
51 repositoryOwner(login: $login) { id }
52 }
53' -f login="$OWNER" --jq '.data.repositoryOwner.id')
54
55[[ -n "$OWNER_ID" ]] || die "Could not resolve owner node ID for '$OWNER'"
56ok "Owner ID: $OWNER_ID"
57
58# Check if project already exists
59EXISTING_PROJECT=$(gh api graphql -f query='
60 query($login: String!) {
61 repositoryOwner(login: $login) {
62 ... on Organization { projectsV2(first: 10) { nodes { id url title } } }
63 ... on User { projectsV2(first: 10) { nodes { id url title } } }
64 }
65 }
66' -f login="$OWNER" --jq '[.data.repositoryOwner.projectsV2.nodes[] | select(.title == "SophaxChat Roadmap")] | first' 2>/dev/null || true)
67
68if [[ -n "$EXISTING_PROJECT" && "$EXISTING_PROJECT" != "null" ]]; then
69 PROJECT_ID=$(echo "$EXISTING_PROJECT" | jq -r '.id')
70 PROJECT_URL=$(echo "$EXISTING_PROJECT" | jq -r '.url')
71 warn "Project already exists: $PROJECT_URL"
72else
73 PROJECT_DATA=$(gh api graphql -f query='
74 mutation($ownerId: ID!, $title: String!) {
75 createProjectV2(input: { ownerId: $ownerId, title: $title }) {
76 projectV2 { id number url }
77 }
78 }
79 ' -f ownerId="$OWNER_ID" -f title="SophaxChat Roadmap")
80
81 PROJECT_ID=$(echo "$PROJECT_DATA" | jq -r '.data.createProjectV2.projectV2.id')
82 PROJECT_URL=$(echo "$PROJECT_DATA" | jq -r '.data.createProjectV2.projectV2.url')
83
84 [[ -n "$PROJECT_ID" && "$PROJECT_ID" != "null" ]] \
85 || die "Project creation failed: $(echo "$PROJECT_DATA" | jq -r '.errors')"
86
87 ok "Project created: $PROJECT_URL"
88fi
89
90# Note: custom field creation is not available in the public Projects v2 API.
91# Add the "Phase" single-select field manually:
92# Project → + (add field) → Single select → options: v0.1 Alpha / TestFlight Beta / Independent Audit / v1.0
93warn "Phase field must be added manually in the project UI (GitHub API limitation)"
94
95# Add draft items for each milestone
96declare -a MILESTONES=(
97 "v0.1 Alpha — crypto complete, first tagged release"
98 "TestFlight Beta — obtain Apple Developer account"
99 "Independent Audit — engage third-party security auditor (required before v1.0)"
100 "v1.0 — MLS stable, DHT peer discovery, post-audit hardening"
101)
102
103REPO_NODE_ID=$(gh api "repos/$REPO" --jq '.node_id')
104
105for TITLE in "${MILESTONES[@]}"; do
106 gh api graphql -f query='
107 mutation($projectId: ID!, $title: String!) {
108 addProjectV2DraftIssue(input: { projectId: $projectId, title: $title }) {
109 projectItem { id }
110 }
111 }
112 ' -f projectId="$PROJECT_ID" -f title="$TITLE" >/dev/null
113 ok " Draft item: $TITLE"
114done
115
116# Make project public
117gh api graphql -f query='
118 mutation($projectId: ID!) {
119 updateProjectV2(input: { projectId: $projectId, public: true }) {
120 projectV2 { public }
121 }
122 }
123' -f projectId="$PROJECT_ID" >/dev/null && ok "Project set to public"
124
125echo " → $PROJECT_URL"
126
127# ── 2. Branch protection ruleset ─────────────────────────────────────────────
128step "Configuring branch protection ruleset for 'main'"
129
130# Check if ruleset already exists
131EXISTING=$(gh api "repos/$REPO/rulesets" --jq '[.[] | select(.name == "Protect main")] | length' 2>/dev/null || echo "0")
132if [[ "$EXISTING" -gt 0 ]]; then
133 warn "Ruleset 'Protect main' already exists — skipping"
134else
135 gh api "repos/$REPO/rulesets" \
136 --method POST \
137 --header "Accept: application/vnd.github+json" \
138 --input - <<'JSON' >/dev/null
139{
140 "name": "Protect main",
141 "target": "branch",
142 "enforcement": "active",
143 "conditions": {
144 "ref_name": {
145 "include": ["refs/heads/main"],
146 "exclude": []
147 }
148 },
149 "rules": [
150 { "type": "deletion" },
151 { "type": "non_fast_forward" },
152 {
153 "type": "pull_request",
154 "parameters": {
155 "required_approving_review_count": 0,
156 "dismiss_stale_reviews_on_push": false,
157 "require_code_owner_review": false,
158 "require_last_push_approval": false,
159 "required_review_thread_resolution": false
160 }
161 },
162 {
163 "type": "required_status_checks",
164 "parameters": {
165 "strict_required_status_checks_policy": true,
166 "required_status_checks": [
167 { "context": "Build SophaxChatCore" },
168 { "context": "Build Android debug APK" }
169 ]
170 }
171 }
172 ]
173}
174JSON
175 ok "Branch ruleset 'Protect main' created"
176 ok " • Blocks direct push to main"
177 ok " • Blocks force push and branch deletion"
178 ok " • Requires Swift + Android CI to pass"
179fi
180
181# ── 3. Wiki ───────────────────────────────────────────────────────────────────
182step "Pushing wiki pages"
183
184[[ -d "$WIKI_DIR" ]] || die "wiki/ directory not found at $WIKI_DIR"
185
186WIKI_REPO="https://github.com/${REPO}.wiki.git"
187TMP_WIKI=$(mktemp -d)
188trap 'rm -rf "$TMP_WIKI"' EXIT
189
190# Clone or init the wiki repo
191if git clone "$WIKI_REPO" "$TMP_WIKI" 2>/dev/null; then
192 ok "Cloned existing wiki"
193else
194 warn "Wiki not yet initialised — creating fresh"
195 git init "$TMP_WIKI"
196 git -C "$TMP_WIKI" checkout -b main 2>/dev/null || true
197fi
198
199# Copy pages (wiki filenames = page titles with spaces as hyphens)
200cp "$WIKI_DIR/Home.md" "$TMP_WIKI/Home.md"
201cp "$WIKI_DIR/Protocol.md" "$TMP_WIKI/Protocol.md"
202cp "$WIKI_DIR/Cryptography.md" "$TMP_WIKI/Cryptography.md"
203
204git -C "$TMP_WIKI" add .
205git -C "$TMP_WIKI" diff --cached --quiet \
206 && warn "Wiki already up to date — no changes to push" \
207 || {
208 git -C "$TMP_WIKI" \
209 -c user.name="$(git config user.name)" \
210 -c user.email="$(git config user.email)" \
211 commit -m "docs(wiki): initial pages — Home, Protocol, Cryptography"
212 git -C "$TMP_WIKI" push "$WIKI_REPO" HEAD:master 2>/dev/null \
213 || git -C "$TMP_WIKI" push "$WIKI_REPO" HEAD:main
214 ok "Wiki pages pushed (Home, Protocol, Cryptography)"
215 }
216
217# ── Summary ──────────────────────────────────────────────────────────────────
218echo ""
219echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
220echo -e "${GREEN} Setup complete${NC}"
221echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
222echo ""
223echo " Project (roadmap): $PROJECT_URL"
224echo " Wiki: https://github.com/$REPO/wiki"
225echo " Branch rules: https://github.com/$REPO/settings/rules"
226echo ""