BOHICA-LABS / BOHICA-LABS/vsdd-factory

feat(preflight): validate origin remote, GitHub identity, repo permissions, fork status, and CI rights before pipeline assumes them

Open
#228 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
2
Forks
1
Avg merge
6h 43m
Merged PRs (30d)
29

Description

## Summary

The factory pipeline assumes — across many skills/agents — that the operator:
- Is authenticated to GitHub with the right identity
- Has push rights to origin
- Has PR-create and merge rights
- Can trigger CI / GitHub Actions
- Is operating against the canonical (non-fork) origin, or knows when origin is a fork

**None of these are validated up front.** Failures surface reactively when a `gh pr create` returns 403, when a `gh pr merge` is rejected, when a CI run is silently never triggered, or when a commit is pushed to a fork that the team doesn't realize is a fork.

## Evidence (grep over vsdd-factory@1.0.0-rc.21)

Zero grep matches for:
- `is_fork`, `isFork`, `.fork`, `parent_repo`, `--fork` → no fork detection anywhere
- `actions.*permission`, `workflow.*permission`, `run_workflow`, `workflow_dispatch.*allowed` → no CI rights check
- `gh repo fork`, `mirror.*repo`, `repo.*mirror` → no fork-as-workflow-option

Only present:
- `gh auth status` mentioned 4 times (repo-initialization prereqs, github-ops agent responsibilities, dx-engineer diagnostic) — binary check, not preflight
- Origin URL check exists in `factory-worktree-health` and `repo-initialization` but ONLY to defend against pointing at the dark-factory plugin repo — not to validate identity/permissions
- `pr-manager.md:344` mentions "deletion was rejected by branch-protection rules or insufficient permissions" as a REACTIVE error message, never a preflight gate

## What's missing — concrete checks needed

### 1. Identity verification preflight

```bash
# Who am I on GitHub?
gh_user=$(gh api user --jq '.login')
gh_emails=$(gh api user/emails --jq '.[].email')

# Does my git signing identity match?
git_email=$(git config user.email)
signing_key_path=$(git config user.signingkey)

# Cross-check
echo "$gh_emails" | grep -qF "$git_email" || warn \
"git config user.email ($git_email) does not match any verified GitHub email. \
Commits may not appear as authored by you on GitHub."

# Is the signing key registered with GitHub?
gh_signing_keys=$(gh api user/ssh_signing_keys --jq '.[].key' 2>/dev/null)
local_pub=$(cat "${signing_key_path/#\~/$HOME}" 2>/dev/null | awk '{print $1" "$2}')
echo "$gh_signing_keys" | grep -qF "$local_pub" || warn \
"Local SSH signing key not found in GitHub's registered signing keys. \
Signed commits won't display as 'Verified' on github.com."
```

### 2. Origin / fork / upstream preflight

```bash
origin_url=$(git remote get-url origin)
repo_data=$(gh api "repos/${OWNER}/${REPO}")

is_fork=$(echo "$repo_data" | jq -r '.fork')
parent_full_name=$(echo "$repo_data" | jq -r '.parent.full_name // empty')

if [ "$is_fork" = "true" ]; then
info "Origin is a fork of $parent_full_name"
info " PRs should target $parent_full_name, not $origin_url"
info " Set up an 'upstream' remote: git remote add upstream "
# Auto-add upstream remote if not present
git remote get-url upstream 2>/dev/null || \
info " Suggested: git remote add upstream https://github.com/${parent_full_name}.git"
fi
```

### 3. Permission preflight

```bash
my_perms=$(gh api "repos/${OWNER}/${REPO}" --jq '.permissions')
# Returns: {admin, maintain, push, triage, pull}

can_push=$(echo "$my_perms" | jq -r '.push')
can_admin=$(echo "$my_perms" | jq -r '.admin')

if [ "$can_push" != "true" ]; then
warn "You do not have push rights to ${OWNER}/${REPO}."
warn "Options:"
warn " 1. Fork the repo and PR from your fork: gh repo fork ${OWNER}/${REPO} --clone"
warn " 2. Request push access from a maintainer"
warn " 3. Operate in read-only mode (no pipeline phases that push)"
fi

if [ "$can_admin" != "true" ]; then
info "You don't have admin rights — bypass of branch protection / required reviews requires a maintainer."
fi
```

### 4. CI / Actions rights preflight

```bash
actions_enabled=$(gh api "repos/${OWNER}/${REPO}/actions/permissions" --jq '.enabled' 2>/dev/null || echo "unknown")
if [ "$actions_enabled" = "false" ]; then
fail "GitHub Actions is disabled on ${OWNER}/${REPO}. The pipeline's PR quality gates depend on CI."
fi

# Can the operator dispatch workflows?
workflows=$(gh api "repos/${OWNER}/${REPO}/actions/workflows" --jq '.workflows[] | .name')
if [ -z "$workflows" ]; then
warn "No GitHub Actions workflows configured. PR checks won't run."
fi
```

### 5. Fork-as-a-workflow-option

When permission preflight finds the operator lacks push rights, the factory should OFFER to fork:

```
You don't have push rights to acme/legacy-app. Options:

[1] Fork acme/legacy-app to skippy/legacy-app, set as origin, set acme/legacy-app as upstream.
Future PRs will target acme/legacy-app from skippy/legacy-app:feature/...

[2] Request push access from a maintainer (I'll draft a message — you send it).

[3] Operate in read-only mode — I can run analysis/spec phases but not PR-creating phases.

[4] Quit.
```

Implementation: `gh repo fork ${OWNER}/${REPO} --clone=false --remote-name=origin --org=skippy`, then `git remote add upstream ${ORIGINAL_URL}`.

### 6. Mirror / sync preflight (when origin is a fork)

When origin IS a fork, the factory should:
- Detect upstream divergence (`git fetch upstream && git log --oneline upstream/develop..origin/develop`)
- Offer to sync the fork before starting work (per the user's global rule "Do NOT begin work on a forked or mirrored repository without first checking if a sync from upstream is needed")
- Validate fork visibility matches upstream (private fork of public repo OK; public fork of private repo = LEAK)

### 7. Single preflight summary output

After all checks, emit a single summary the operator confirms:

```
GitHub Preflight — acme/legacy-app

Authentication: ✅ skippy@arcaven.com (verified GitHub identity)
Signing key: ✅ ED25519 key registered with GitHub
Repo: acme/legacy-app (public, not a fork)
Your permissions: push ✅ admin ❌ (you can PR but not bypass protection)
Default branch: develop (gitflow detected)
Branch protection: ✅ on develop (require reviews=1, signed-commits=true, status-checks=ci.yml)
Actions: ✅ enabled, 3 workflows configured
Open PRs by you: 2 (review pending)

Ready to proceed with pipeline operations.
```

Cache to `.factory/repo-config.yaml` (alongside the P3 branching-strategy cache).

## Why this matters

1. **403s mid-pipeline are user-hostile.** A PR-creating phase fails after hours of work because the operator never had push rights. Detect at session start.
2. **Fork drift produces wrong-target PRs.** Operator works on a fork they didn't realize was a fork; PRs target the fork (their own work), not upstream. Code never reaches the real project.
3. **Identity mismatch produces "Unverified" commits.** Operator's git email doesn't match GitHub; commits don't display as theirs, and signed-commit enforcement fails.
4. **Silent CI absence breaks the pipeline contract.** If Actions is disabled (org policy, repo setting), the pipeline waits forever for checks that don't arrive. Detect at session start.
5. **Personal global rule violation risk.** User CLAUDE.md says "NEVER fork or create a public repository on GitHub or GitLab from a non-public (private/internal) repo source." The factory doesn't currently know about forks at all — it can't enforce this rule.

## Acceptance criteria

- [ ] A preflight skill (extending `factory-health` or new `repo-preflight`) runs at session start and validates:
- `gh auth status` (binary auth)
- `gh api user` identity matches git config user.email
- Local signing key registered with GitHub (for "Verified" display)
- Origin remote URL + whether origin is a fork (set `.parent` if so)
- Operator's `.permissions` on origin (push / admin / etc)
- GitHub Actions enabled + at least one workflow configured
- [ ] If operator lacks push rights, offer fork-or-quit interactively (with `gh repo fork` automation).
- [ ] If origin is a fork, set up upstream remote and require sync-from-upstream before starting work.
- [ ] If origin is a fork of a non-public repo and operator's user-level rule forbids public forks, BLOCK and warn (respect user CLAUDE.md "NEVER fork or create a public repository... from a non-public source").
- [ ] All discovered facts cached to `.factory/repo-config.yaml`; subsequent sessions verify the cache hasn't drifted.
- [ ] Pipeline skills (`code-delivery`, `pr-manager`, etc.) READ from `.factory/repo-config.yaml` instead of hardcoding `origin` / `develop`.

## Found during

`/vsdd-factory:setup-env` + `/vsdd-factory:factory-health` on `switchboard-blue` (Go, 2026-06-23, vsdd-factory@1.0.0-rc.21). Neither skill validates GitHub identity, permissions, fork status, or CI rights. The session would have proceeded to a hypothetical PR-creating phase blind to whether the operator actually had the rights to create / merge / trigger CI.

Specifically:
- `gh auth status` is mentioned as a PREREQUISITE for `repo-initialization` but never enforced as a preflight gate for general session work.
- Origin URL is checked only to prevent pointing at the dark-factory plugin's own repo.
- Zero grep matches across the plugin for fork detection, repo-permissions inspection, or CI rights check.

## Related

- (this session, pending) P3 — branching strategy / branch protection preflight (this issue's natural companion — together they form a complete "session-start preflight")
- (this session, filed) #203-209 — onboarding + factory-obs UX issues; collectively all of these speak to the absence of a coherent session-entry validation
- User's global CLAUDE.md — explicitly forbids forking non-public → public; the factory has no way to enforce this today

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.