From c2d9d7c2b5b73baff82efb0b2a55c91cefb09eef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:58:56 +0000 Subject: [PATCH 1/5] Initial plan From cde310d2545d9c8fbb01aadf6572c28c664bb02e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:04:50 +0000 Subject: [PATCH 2/5] Add sync to orgs and auto-merge functionality with tests Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com> --- .github/workflows/auto-merge.yml | 132 +++++++++++++ .github/workflows/ci.yml | 23 ++- .github/workflows/sync-to-orgs.yml | 167 ++++++++++++++++ README.md | 140 +++++++++++++- docs/SYNC.md | 252 ++++++++++++++++++++++++ templates/workflows/sync-receiver.yml | 97 ++++++++++ tests/test_sync.py | 264 ++++++++++++++++++++++++++ 7 files changed, 1073 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/auto-merge.yml create mode 100644 .github/workflows/sync-to-orgs.yml create mode 100644 docs/SYNC.md create mode 100644 templates/workflows/sync-receiver.yml create mode 100644 tests/test_sync.py diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..d18072f --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,132 @@ +# Auto-merge PRs after CI passes +# Automatically merges approved PRs to main once all checks pass + +name: Auto Merge + +on: + pull_request_review: + types: [submitted] + check_suite: + types: [completed] + status: {} + workflow_run: + workflows: ["CI"] + types: [completed] + +jobs: + auto-merge: + name: Auto Merge PR + runs-on: ubuntu-latest + + # Only run on approved PRs targeting main + if: | + github.event_name == 'pull_request_review' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + + permissions: + contents: write + pull-requests: write + checks: read + + steps: + - uses: actions/checkout@v4 + + - name: Get PR info + id: pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get PR number from the event + if [ "${{ github.event_name }}" == "pull_request_review" ]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + elif [ "${{ github.event_name }}" == "workflow_run" ]; then + # Extract PR number from workflow run + PR_NUMBER=$(gh pr list --json number,headRefName --jq '.[] | select(.headRefName=="${{ github.event.workflow_run.head_branch }}") | .number' | head -1) + else + echo "No PR found" + exit 0 + fi + + if [ -z "$PR_NUMBER" ]; then + echo "No PR number found" + exit 0 + fi + + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + + # Get PR details + PR_DATA=$(gh pr view $PR_NUMBER --json number,title,state,mergeable,reviewDecision,statusCheckRollup) + + echo "$PR_DATA" | jq . + + STATE=$(echo "$PR_DATA" | jq -r .state) + MERGEABLE=$(echo "$PR_DATA" | jq -r .mergeable) + REVIEW_DECISION=$(echo "$PR_DATA" | jq -r .reviewDecision) + + echo "state=$STATE" >> $GITHUB_OUTPUT + echo "mergeable=$MERGEABLE" >> $GITHUB_OUTPUT + echo "review_decision=$REVIEW_DECISION" >> $GITHUB_OUTPUT + + - name: Check merge conditions + id: check + run: | + STATE="${{ steps.pr.outputs.state }}" + MERGEABLE="${{ steps.pr.outputs.mergeable }}" + REVIEW_DECISION="${{ steps.pr.outputs.review_decision }}" + + echo "PR State: $STATE" + echo "Mergeable: $MERGEABLE" + echo "Review Decision: $REVIEW_DECISION" + + # Check conditions + CAN_MERGE=false + + if [ "$STATE" == "OPEN" ] && \ + [ "$MERGEABLE" == "MERGEABLE" ] && \ + [ "$REVIEW_DECISION" == "APPROVED" ]; then + CAN_MERGE=true + fi + + echo "can_merge=$CAN_MERGE" >> $GITHUB_OUTPUT + + if [ "$CAN_MERGE" == "true" ]; then + echo "โœ“ All conditions met for auto-merge" + else + echo "โš ๏ธ Conditions not met:" + [ "$STATE" != "OPEN" ] && echo " - PR is not open" + [ "$MERGEABLE" != "MERGEABLE" ] && echo " - PR has conflicts or is not mergeable" + [ "$REVIEW_DECISION" != "APPROVED" ] && echo " - PR is not approved" + fi + + - name: Auto-merge PR + if: steps.check.outputs.can_merge == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ steps.pr.outputs.pr_number }}" + + echo "๐Ÿš€ Auto-merging PR #$PR_NUMBER to main..." + + # Enable auto-merge with squash + gh pr merge $PR_NUMBER --auto --squash --delete-branch + + echo "โœ“ Auto-merge enabled" + + - name: Comment on PR + if: steps.check.outputs.can_merge == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ steps.pr.outputs.pr_number }}" + + gh pr comment $PR_NUMBER --body "๐Ÿค– Auto-merge enabled. This PR will be merged automatically once all status checks pass." + + - name: Summary + if: always() + run: | + echo "๐ŸŽฏ Auto-merge Summary" + echo "" + echo "PR: #${{ steps.pr.outputs.pr_number }}" + echo "State: ${{ steps.pr.outputs.state }}" + echo "Can merge: ${{ steps.check.outputs.can_merge }}" + echo "Status: ${{ job.status }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72494ea..4c2f970 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,11 +179,31 @@ jobs: print(f'โœ“ registry.yaml: {len(reg[\"orgs\"])} orgs, {len(reg[\"rules\"])} rules') " + # Test sync functionality + test-sync: + name: Test Sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: pip install pyyaml + + - name: Run sync tests + run: | + chmod +x tests/test_sync.py + python tests/test_sync.py + # Signal summary job summary: name: CI Summary runs-on: ubuntu-latest - needs: [lint, test-operator, test-dispatcher, test-webhooks, validate-config] + needs: [lint, test-operator, test-dispatcher, test-webhooks, validate-config, test-sync] if: always() steps: - name: Check results @@ -196,3 +216,4 @@ jobs: echo " Dispatcher: ${{ needs.test-dispatcher.result }}" echo " Webhooks: ${{ needs.test-webhooks.result }}" echo " Config: ${{ needs.validate-config.result }}" + echo " Sync: ${{ needs.test-sync.result }}" diff --git a/.github/workflows/sync-to-orgs.yml b/.github/workflows/sync-to-orgs.yml new file mode 100644 index 0000000..dcd27cd --- /dev/null +++ b/.github/workflows/sync-to-orgs.yml @@ -0,0 +1,167 @@ +# Sync shared workflows and configs to other org repos +# This workflow pushes templates and shared files to target organizations + +name: Sync to Orgs + +on: + push: + branches: [main] + paths: + - 'templates/**' + - '.github/workflows/**' + - 'routes/registry.yaml' + workflow_dispatch: + inputs: + target_orgs: + description: 'Target orgs (comma-separated, or "all")' + required: false + default: 'all' + type: string + dry_run: + description: 'Dry run (test without pushing)' + required: false + type: boolean + default: false + +jobs: + sync: + name: Sync to Organizations + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install pyyaml requests + + - name: Load registry + id: registry + run: | + python -c " + import yaml + import json + + with open('routes/registry.yaml') as f: + registry = yaml.safe_load(f) + + # Extract active orgs + orgs = [] + for code, org in registry.get('orgs', {}).items(): + if org.get('status') == 'active': + orgs.append({ + 'code': code, + 'name': org['name'], + 'github': org['github'], + 'repos': org.get('repos', []) + }) + + print(f'Found {len(orgs)} active orgs') + for org in orgs: + print(f' - {org[\"code\"]}: {org[\"name\"]}') + + # Output for next steps + with open('$GITHUB_OUTPUT', 'a') as f: + f.write(f'orgs={json.dumps(orgs)}\\n') + " + + - name: Dispatch to target orgs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TARGET_ORGS: ${{ inputs.target_orgs || 'all' }} + DRY_RUN: ${{ inputs.dry_run || 'false' }} + ORGS_JSON: ${{ steps.registry.outputs.orgs }} + run: | + echo "๐ŸŽฏ Dispatching sync to organizations..." + echo "" + + python -c " + import os + import json + import requests + + token = os.environ.get('GITHUB_TOKEN') + target_input = os.environ.get('TARGET_ORGS', 'all') + dry_run = os.environ.get('DRY_RUN', 'false').lower() == 'true' + orgs = json.loads(os.environ.get('ORGS_JSON', '[]')) + + # Parse target orgs + if target_input == 'all': + target_codes = [org['code'] for org in orgs] + else: + target_codes = [c.strip() for c in target_input.split(',')] + + print(f'Target orgs: {target_codes}') + print(f'Dry run: {dry_run}') + print('') + + # Dispatch to each target org + for org in orgs: + if org['code'] not in target_codes: + continue + + print(f'๐Ÿ“ก {org[\"code\"]}: {org[\"name\"]}') + + # For each repo in the org, dispatch a workflow + for repo in org.get('repos', []): + repo_name = repo['name'] + repo_url = repo['url'] + + # Extract owner/repo from URL + parts = repo_url.replace('https://github.com/', '').split('/') + if len(parts) < 2: + continue + + owner = parts[0] + repo_slug = parts[1] + + print(f' -> {owner}/{repo_slug}') + + if dry_run: + print(f' [DRY RUN] Would dispatch to {owner}/{repo_slug}') + continue + + # Send repository_dispatch event + url = f'https://api.github.com/repos/{owner}/{repo_slug}/dispatches' + headers = { + 'Authorization': f'token {token}', + 'Accept': 'application/vnd.github.v3+json' + } + payload = { + 'event_type': 'sync_from_bridge', + 'client_payload': { + 'source': 'BlackRoad-OS/.github', + 'ref': os.environ.get('GITHUB_SHA', 'main'), + 'timestamp': '${{ github.event.head_commit.timestamp }}' + } + } + + try: + resp = requests.post(url, json=payload, headers=headers, timeout=10) + if resp.status_code == 204: + print(f' โœ“ Dispatched') + elif resp.status_code == 404: + print(f' โš ๏ธ Repo not found or no dispatch workflow') + else: + print(f' โŒ Failed: {resp.status_code}') + except Exception as e: + print(f' โŒ Error: {e}') + + print('') + print('โœ“ Sync dispatch complete') + " + + - name: Summary + run: | + echo "๐Ÿ“ก Sync Summary" + echo "" + echo "Status: ${{ job.status }}" + echo "Trigger: ${{ github.event_name }}" + echo "Branch: ${{ github.ref_name }}" + echo "Commit: ${{ github.sha }}" diff --git a/README.md b/README.md index 2c962c7..ecf3af3 100644 --- a/README.md +++ b/README.md @@ -1 +1,139 @@ -Enter file contents here +# BlackRoad Bridge + +> **The central coordination hub for the BlackRoad ecosystem** + +This repository (`.github`) serves as **The Bridge** - coordinating workflows, configurations, and updates across all 15 BlackRoad organizations. + +## Key Features + +- ๐ŸŽฏ **Central Routing**: Routes requests across 15 organizations +- ๐Ÿ“ก **Auto-Sync**: Automatically pushes updates to target org repositories +- ๐Ÿค– **Auto-Merge**: PRs automatically merge to main after approval + CI pass +- โœ… **Comprehensive Testing**: Validates sync functionality and configurations +- ๐Ÿ”ง **Prototypes**: Working code for operator, dispatcher, webhooks, and more + +## Quick Start + +### Running Tests + +```bash +# Run all sync tests +python tests/test_sync.py + +# Run CI tests locally +python -m pytest prototypes/operator/tests/ +``` + +### Syncing to Organizations + +Updates are automatically synced when pushed to `main`. To manually trigger: + +```bash +# Sync to all active orgs +gh workflow run sync-to-orgs.yml + +# Sync to specific orgs +gh workflow run sync-to-orgs.yml -f target_orgs=OS,AI + +# Test without actually dispatching (dry run) +gh workflow run sync-to-orgs.yml -f dry_run=true +``` + +See [docs/SYNC.md](docs/SYNC.md) for detailed documentation. + +## Documentation + +- [INDEX.md](INDEX.md) - Navigate the entire ecosystem +- [SYNC.md](docs/SYNC.md) - **How updates sync to other orgs** โœจ +- [SIGNALS.md](SIGNALS.md) - Signal protocol for coordination +- [MEMORY.md](MEMORY.md) - Persistent context for agents +- [REPO_MAP.md](REPO_MAP.md) - All repos across all orgs +- [BLACKROAD_ARCHITECTURE.md](BLACKROAD_ARCHITECTURE.md) - Architecture vision + +## Workflows + +| Workflow | Purpose | Trigger | +|----------|---------|---------| +| **sync-to-orgs.yml** | Syncs updates to target orgs | Push to main, manual | +| **auto-merge.yml** | Auto-merges approved PRs | After CI passes | +| **ci.yml** | Runs tests and validation | Push, PR to main/develop | +| **sync-assets.yml** | Syncs from external sources | Every 6 hours, manual | +| **webhook-dispatch.yml** | Routes incoming webhooks | Repository dispatch | +| **deploy-worker.yml** | Deploys Cloudflare Workers | Push to main, manual | +| **release.yml** | Publishes releases | Push tags | +| **health-check.yml** | Monitors service health | Schedule, manual | + +## Architecture + +``` +BlackRoad-OS/.github (The Bridge) + โ”‚ + โ”œโ”€โ”€โ”€ 15 Organizations + โ”‚ โ”œโ”€ OS (Core Infrastructure) + โ”‚ โ”œโ”€ AI (Intelligence Routing) + โ”‚ โ”œโ”€ CLD (Edge/Cloud) + โ”‚ โ”œโ”€ HW (Hardware/IoT) + โ”‚ โ””โ”€ ... 11 more + โ”‚ + โ”œโ”€โ”€โ”€ Prototypes + โ”‚ โ”œโ”€ operator (routing engine) + โ”‚ โ”œโ”€ dispatcher (org dispatcher) + โ”‚ โ”œโ”€ webhooks (event handling) + โ”‚ โ””โ”€ ... more + โ”‚ + โ””โ”€โ”€โ”€ Routes & Registry + โ””โ”€ routes/registry.yaml (master routing table) +``` + +## Contributing + +1. Create a feature branch +2. Make changes +3. Run tests: `python tests/test_sync.py` +4. Create PR to `main` +5. Get approval +6. CI runs automatically +7. Auto-merge to main (after approval + CI pass) +8. Changes sync to target orgs automatically + +## Testing & Validation + +All PRs must pass: + +- โœ… Lint (Ruff, Black, isort) +- โœ… Operator tests (routing logic) +- โœ… Dispatcher tests (org routing) +- โœ… Webhook tests (event handling) +- โœ… Config validation (YAML) +- โœ… **Sync tests (sync functionality)** โœจ + +## Organizations + +15 orgs, 1 active (OS), 14 planned. See [routes/registry.yaml](routes/registry.yaml) for details. + +**Active:** +- BlackRoad-OS (OS) - Core infrastructure, The Bridge + +**Planned:** +- BlackRoad-AI (AI) - Intelligence routing +- BlackRoad-Cloud (CLD) - Edge/cloud computing +- BlackRoad-Hardware (HW) - Pi cluster, IoT +- BlackRoad-Security (SEC) - Auth, secrets +- BlackRoad-Labs (LAB) - R&D, experiments +- BlackRoad-Foundation (FND) - CRM, billing +- BlackRoad-Media (MED) - Content, social +- BlackRoad-Studio (STU) - Design, Figma +- BlackRoad-Interactive (INT) - Gaming, metaverse +- BlackRoad-Education (EDU) - Learning, tutorials +- BlackRoad-Gov (GOV) - Governance, voting +- BlackRoad-Archive (ARC) - Storage, backups +- BlackRoad-Ventures (VEN) - Marketplace +- Blackbox-Enterprises (BBX) - Enterprise + +## Status + +See [.STATUS](.STATUS) for real-time beacon. + +--- + +**The Bridge is live. All systems nominal.** diff --git a/docs/SYNC.md b/docs/SYNC.md new file mode 100644 index 0000000..efe54a0 --- /dev/null +++ b/docs/SYNC.md @@ -0,0 +1,252 @@ +# Sync to Organizations + +This document explains how updates in the `.github` repository are synced to other BlackRoad organizations and repositories. + +## Overview + +The `.github` repository serves as the **central coordination hub** for all BlackRoad organizations. When changes are made here, they are automatically propagated to the appropriate target repositories through GitHub's repository dispatch system. + +## How It Works + +### 1. Workflow Triggers + +The sync process is triggered in two ways: + +**Automatic (on push to main):** +```yaml +on: + push: + branches: [main] + paths: + - 'templates/**' + - '.github/workflows/**' + - 'routes/registry.yaml' +``` + +**Manual dispatch:** +```bash +# Via GitHub UI: Actions โ†’ Sync to Orgs โ†’ Run workflow +# Or via gh CLI: +gh workflow run sync-to-orgs.yml -f target_orgs=OS,AI -f dry_run=false +``` + +### 2. Target Organizations + +Organizations are defined in `routes/registry.yaml`. Only organizations with `status: active` receive sync updates. + +Currently active orgs: +- **OS** (BlackRoad-OS) - Core infrastructure + +To activate additional orgs, update their status in `routes/registry.yaml`: +```yaml +AI: + name: BlackRoad-AI + status: active # Change from 'planned' to 'active' + ... +``` + +### 3. Dispatch Process + +For each active organization: + +1. Load the organization's repository list from `routes/registry.yaml` +2. Send a `repository_dispatch` event to each repo: + ```json + { + "event_type": "sync_from_bridge", + "client_payload": { + "source": "BlackRoad-OS/.github", + "ref": "", + "timestamp": "" + } + } + ``` +3. Target repositories listen for this event and pull updates + +### 4. Repository Setup + +Target repositories must: + +1. Have a workflow that listens for the dispatch event: + ```yaml + on: + repository_dispatch: + types: [sync_from_bridge] + ``` + +2. Pull and apply updates from the bridge repository: + ```yaml + - name: Sync from bridge + run: | + # Pull workflow templates + curl -o .github/workflows/shared.yml \ + https://raw.githubusercontent.com/BlackRoad-OS/.github/main/templates/workflows/shared.yml + ``` + +## Testing + +### Run Tests Locally + +```bash +# Run all sync tests +python tests/test_sync.py + +# Test with dry run (no actual dispatch) +gh workflow run sync-to-orgs.yml -f dry_run=true +``` + +### Verify Sync + +After syncing, check: + +1. **Workflow run logs**: Actions โ†’ Sync to Orgs โ†’ [latest run] +2. **Target repo webhooks**: Settings โ†’ Webhooks โ†’ Recent Deliveries +3. **Target repo workflows**: Should show triggered runs from dispatch + +### Common Issues + +**Issue**: "404 Repo not found or no dispatch workflow" +- **Solution**: Target repo either doesn't exist or hasn't set up a dispatch workflow + +**Issue**: "401 Unauthorized" +- **Solution**: `GITHUB_TOKEN` lacks permission to dispatch to target org. Use a PAT with `repo` scope. + +**Issue**: Sync runs but target repos don't update +- **Solution**: Target repos need to implement the dispatch handler workflow + +## Auto-Merge to Main + +PRs are automatically merged to `main` when: + +1. โœ… All CI checks pass +2. โœ… PR is approved by a reviewer +3. โœ… PR has no merge conflicts + +The auto-merge workflow: +- Triggers after CI completes successfully +- Checks PR approval status +- Enables auto-merge with squash commit +- Deletes the branch after merge + +## CI Pipeline + +Before any PR can be merged, it must pass: + +- **Lint**: Ruff, Black, isort checks +- **Test Operator**: Routing logic tests +- **Test Dispatcher**: Registry and routing tests +- **Test Webhooks**: Webhook handling tests +- **Validate Config**: YAML validation +- **Test Sync**: Sync functionality validation โœจ (new) + +## Monitoring + +### Check Sync Status + +```bash +# List recent workflow runs +gh run list --workflow=sync-to-orgs.yml + +# View specific run details +gh run view + +# Watch a run in real-time +gh run watch +``` + +### Check Active Orgs + +```bash +# List active organizations +python -c " +import yaml +with open('routes/registry.yaml') as f: + reg = yaml.safe_load(f) + active = [code for code, org in reg['orgs'].items() if org.get('status') == 'active'] + print(f'Active orgs: {', '.join(active)}') +" +``` + +## Architecture + +``` +BlackRoad-OS/.github (Bridge) + โ”‚ + โ”œโ”€ Push to main + โ”‚ โ”‚ + โ”‚ โ””โ”€ Triggers sync-to-orgs.yml + โ”‚ โ”‚ + โ”‚ โ”œโ”€ Load routes/registry.yaml + โ”‚ โ”‚ + โ”‚ โ””โ”€ For each active org: + โ”‚ โ”‚ + โ”‚ โ””โ”€ For each repo: + โ”‚ โ”‚ + โ”‚ โ””โ”€ Send repository_dispatch + โ”‚ โ”‚ + โ”‚ โ””โ”€ Target repo receives event + โ”‚ โ”‚ + โ”‚ โ””โ”€ Pulls and applies updates + โ”‚ + โ””โ”€ PR approved + CI passes + โ”‚ + โ””โ”€ Triggers auto-merge.yml + โ”‚ + โ””โ”€ Merges to main + โ”‚ + โ””โ”€ Cycle continues... +``` + +## Contributing + +When making changes that affect other orgs: + +1. Create a feature branch +2. Make changes to templates, workflows, or configs +3. Run tests: `python tests/test_sync.py` +4. Create a PR to `main` +5. Get approval from a reviewer +6. CI will run automatically +7. Once approved + CI passes โ†’ auto-merge to main +8. Sync workflow dispatches to target orgs +9. Monitor target repos for successful application + +## Security + +- Use `GITHUB_TOKEN` for same-org dispatches +- Use PAT with minimal scope for cross-org dispatches +- Validate all payloads in target repos +- Never sync secrets or credentials +- Use dry-run mode when testing + +## Troubleshooting + +### Debug Mode + +Enable verbose logging: +```yaml +env: + ACTIONS_STEP_DEBUG: true + ACTIONS_RUNNER_DEBUG: true +``` + +### Manual Dispatch + +To sync specific orgs: +```bash +gh workflow run sync-to-orgs.yml -f target_orgs=OS,AI,CLD +``` + +To test without dispatching: +```bash +gh workflow run sync-to-orgs.yml -f dry_run=true +``` + +## Related Files + +- `.github/workflows/sync-to-orgs.yml` - Main sync workflow +- `.github/workflows/auto-merge.yml` - Auto-merge workflow +- `.github/workflows/ci.yml` - CI pipeline with sync tests +- `routes/registry.yaml` - Organization registry +- `tests/test_sync.py` - Sync functionality tests +- `templates/` - Shared templates to sync diff --git a/templates/workflows/sync-receiver.yml b/templates/workflows/sync-receiver.yml new file mode 100644 index 0000000..553321a --- /dev/null +++ b/templates/workflows/sync-receiver.yml @@ -0,0 +1,97 @@ +# Shared workflow template for receiving sync updates from the bridge +# This workflow should be added to target org repositories + +name: Sync from Bridge + +on: + repository_dispatch: + types: [sync_from_bridge] + workflow_dispatch: + inputs: + force: + description: 'Force sync (ignore cache)' + required: false + type: boolean + default: false + +jobs: + sync: + name: Sync Updates + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Log sync event + run: | + echo "๐Ÿ“ก Sync from Bridge received" + echo "" + echo "Source: ${{ github.event.client_payload.source || 'manual' }}" + echo "Ref: ${{ github.event.client_payload.ref || 'main' }}" + echo "Timestamp: ${{ github.event.client_payload.timestamp || github.event.head_commit.timestamp }}" + + - name: Fetch shared workflows + run: | + echo "โฌ‡๏ธ Fetching shared workflows from bridge..." + + # Create .github/workflows if it doesn't exist + mkdir -p .github/workflows + + # Example: Fetch a shared CI workflow template + # Uncomment and customize for your needs: + # curl -o .github/workflows/ci.yml \ + # https://raw.githubusercontent.com/BlackRoad-OS/.github/main/templates/workflows/ci-template.yml + + echo "โœ“ Workflows fetched" + + - name: Fetch shared configs + run: | + echo "โš™๏ธ Fetching shared configurations..." + + # Example: Fetch shared configs + # curl -o .editorconfig \ + # https://raw.githubusercontent.com/BlackRoad-OS/.github/main/templates/configs/.editorconfig + + echo "โœ“ Configs fetched" + + - name: Apply updates + run: | + echo "๐Ÿ”„ Applying updates..." + + # Add any custom sync logic here + # Examples: + # - Update package.json scripts + # - Sync shared dependencies + # - Update documentation templates + + echo "โœ“ Updates applied" + + - name: Commit changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add -A + + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "๐Ÿ”„ Sync from bridge + +Synced from: ${{ github.event.client_payload.source || 'manual' }} +Ref: ${{ github.event.client_payload.ref || 'main' }} +Timestamp: ${{ github.event.client_payload.timestamp || github.event.head_commit.timestamp }}" + + git push + echo "โœ“ Changes committed and pushed" + fi + + - name: Summary + run: | + echo "๐Ÿ“ก Sync Complete" + echo "" + echo "Status: ${{ job.status }}" + echo "Repository: ${{ github.repository }}" + echo "Branch: ${{ github.ref_name }}" diff --git a/tests/test_sync.py b/tests/test_sync.py new file mode 100644 index 0000000..3912e5c --- /dev/null +++ b/tests/test_sync.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +""" +Test suite for sync functionality +Tests that updates are properly dispatched to target orgs +""" + +import json +import os +import sys +import yaml +from pathlib import Path + + +class TestSyncToOrgs: + """Test sync-to-orgs workflow functionality""" + + def __init__(self): + self.root = Path(__file__).parent.parent + self.errors = [] + + def error(self, msg): + """Record an error""" + self.errors.append(msg) + print(f"โŒ {msg}") + + def success(self, msg): + """Record success""" + print(f"โœ“ {msg}") + + def test_workflow_exists(self): + """Test that sync-to-orgs workflow file exists""" + workflow_path = self.root / ".github/workflows/sync-to-orgs.yml" + if not workflow_path.exists(): + self.error("sync-to-orgs.yml workflow not found") + return False + + self.success("sync-to-orgs.yml workflow exists") + return True + + def test_workflow_valid_yaml(self): + """Test that workflow is valid YAML""" + workflow_path = self.root / ".github/workflows/sync-to-orgs.yml" + + try: + with open(workflow_path) as f: + workflow = yaml.safe_load(f) + + assert "name" in workflow, "Missing 'name' field" + # 'on' gets parsed as True by PyYAML, so check for True or 'on' + assert True in workflow or "on" in workflow, "Missing 'on' field" + assert "jobs" in workflow, "Missing 'jobs' field" + + self.success("Workflow YAML is valid") + return True + except Exception as e: + self.error(f"Invalid workflow YAML: {e}") + return False + + def test_workflow_triggers(self): + """Test that workflow has correct triggers""" + workflow_path = self.root / ".github/workflows/sync-to-orgs.yml" + + with open(workflow_path) as f: + workflow = yaml.safe_load(f) + + # 'on' gets parsed as True by PyYAML + triggers = workflow.get(True, workflow.get("on", {})) + + # Should trigger on push to main + if "push" in triggers: + branches = triggers["push"].get("branches", []) + if "main" in branches: + self.success("Workflow triggers on push to main") + else: + self.error("Workflow does not trigger on push to main") + + # Should have manual dispatch + if "workflow_dispatch" in triggers: + self.success("Workflow has manual dispatch") + else: + self.error("Workflow missing workflow_dispatch") + + def test_registry_loads(self): + """Test that registry.yaml loads successfully""" + registry_path = self.root / "routes/registry.yaml" + + if not registry_path.exists(): + self.error("routes/registry.yaml not found") + return False + + try: + with open(registry_path) as f: + registry = yaml.safe_load(f) + + assert "orgs" in registry, "Missing 'orgs' in registry" + assert "rules" in registry, "Missing 'rules' in registry" + + orgs = registry["orgs"] + self.success(f"Registry loads with {len(orgs)} orgs") + return True + except Exception as e: + self.error(f"Failed to load registry: {e}") + return False + + def test_active_orgs(self): + """Test that active orgs are properly configured""" + registry_path = self.root / "routes/registry.yaml" + + with open(registry_path) as f: + registry = yaml.safe_load(f) + + active_orgs = [] + for code, org in registry["orgs"].items(): + if org.get("status") == "active": + active_orgs.append(code) + + # Validate org structure + if "name" not in org: + self.error(f"Org {code} missing 'name'") + if "github" not in org: + self.error(f"Org {code} missing 'github'") + if "repos" not in org: + self.error(f"Org {code} missing 'repos'") + + if active_orgs: + self.success(f"Found {len(active_orgs)} active orgs: {', '.join(active_orgs)}") + else: + self.error("No active orgs found in registry") + + def test_org_repos_valid(self): + """Test that org repos have valid structure""" + registry_path = self.root / "routes/registry.yaml" + + with open(registry_path) as f: + registry = yaml.safe_load(f) + + total_repos = 0 + for code, org in registry["orgs"].items(): + if org.get("status") != "active": + continue + + repos = org.get("repos", []) + for repo in repos: + total_repos += 1 + + if "name" not in repo: + self.error(f"Repo in {code} missing 'name'") + if "url" not in repo: + self.error(f"Repo in {code} missing 'url'") + if not repo.get("url", "").startswith("https://github.com/"): + self.error(f"Invalid repo URL in {code}: {repo.get('url')}") + + self.success(f"Validated {total_repos} repo configurations") + + def test_dispatch_payload_format(self): + """Test that dispatch payload format is correct""" + workflow_path = self.root / ".github/workflows/sync-to-orgs.yml" + + with open(workflow_path) as f: + content = f.read() + + # Check for dispatch event structure + required_fields = ["event_type", "client_payload"] + for field in required_fields: + if field in content: + self.success(f"Dispatch includes '{field}'") + else: + self.error(f"Dispatch missing '{field}'") + + def test_auto_merge_workflow_exists(self): + """Test that auto-merge workflow exists""" + workflow_path = self.root / ".github/workflows/auto-merge.yml" + if not workflow_path.exists(): + self.error("auto-merge.yml workflow not found") + return False + + self.success("auto-merge.yml workflow exists") + return True + + def test_auto_merge_triggers(self): + """Test that auto-merge has correct triggers""" + workflow_path = self.root / ".github/workflows/auto-merge.yml" + + if not workflow_path.exists(): + return + + with open(workflow_path) as f: + workflow = yaml.safe_load(f) + + # 'on' gets parsed as True by PyYAML + triggers = workflow.get(True, workflow.get("on", {})) + + # Should trigger on workflow_run for CI + if "workflow_run" in triggers: + workflows = triggers["workflow_run"].get("workflows", []) + if "CI" in workflows: + self.success("Auto-merge triggers after CI workflow") + else: + self.error("Auto-merge does not trigger after CI") + else: + self.error("Auto-merge missing workflow_run trigger") + + def test_ci_workflow_valid(self): + """Test that CI workflow is properly configured""" + workflow_path = self.root / ".github/workflows/ci.yml" + + if not workflow_path.exists(): + self.error("ci.yml workflow not found") + return False + + with open(workflow_path) as f: + workflow = yaml.safe_load(f) + + # Check for required jobs + jobs = workflow.get("jobs", {}) + required_jobs = ["lint", "validate-config"] + + for job_name in required_jobs: + if job_name in jobs: + self.success(f"CI has '{job_name}' job") + else: + self.error(f"CI missing '{job_name}' job") + + return True + + def run_all(self): + """Run all tests""" + print("๐Ÿงช Running sync functionality tests...\n") + + tests = [ + self.test_workflow_exists, + self.test_workflow_valid_yaml, + self.test_workflow_triggers, + self.test_registry_loads, + self.test_active_orgs, + self.test_org_repos_valid, + self.test_dispatch_payload_format, + self.test_auto_merge_workflow_exists, + self.test_auto_merge_triggers, + self.test_ci_workflow_valid, + ] + + for test in tests: + try: + test() + except Exception as e: + self.error(f"Test {test.__name__} failed with exception: {e}") + print() + + # Summary + print("=" * 50) + if self.errors: + print(f"โŒ Tests failed: {len(self.errors)} error(s)") + for error in self.errors: + print(f" - {error}") + return 1 + else: + print("โœ“ All tests passed!") + return 0 + + +if __name__ == "__main__": + tester = TestSyncToOrgs() + sys.exit(tester.run_all()) From 330d3527ad0f098c5fa0b201de4942df304048ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:06:00 +0000 Subject: [PATCH 3/5] Add integration tests and fix sync-receiver template Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com> --- templates/workflows/sync-receiver.yml | 10 +-- tests/test_integration.py | 100 ++++++++++++++++++++++++++ tests/test_sync.py | 0 3 files changed, 105 insertions(+), 5 deletions(-) create mode 100755 tests/test_integration.py mode change 100644 => 100755 tests/test_sync.py diff --git a/templates/workflows/sync-receiver.yml b/templates/workflows/sync-receiver.yml index 553321a..bb5c2ce 100644 --- a/templates/workflows/sync-receiver.yml +++ b/templates/workflows/sync-receiver.yml @@ -78,11 +78,11 @@ jobs: if git diff --cached --quiet; then echo "No changes to commit" else - git commit -m "๐Ÿ”„ Sync from bridge - -Synced from: ${{ github.event.client_payload.source || 'manual' }} -Ref: ${{ github.event.client_payload.ref || 'main' }} -Timestamp: ${{ github.event.client_payload.timestamp || github.event.head_commit.timestamp }}" + SOURCE="${{ github.event.client_payload.source || 'manual' }}" + REF="${{ github.event.client_payload.ref || 'main' }}" + TIMESTAMP="${{ github.event.client_payload.timestamp || github.event.head_commit.timestamp }}" + + git commit -m "๐Ÿ”„ Sync from bridge" -m "Synced from: $SOURCE" -m "Ref: $REF" -m "Timestamp: $TIMESTAMP" git push echo "โœ“ Changes committed and pushed" diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100755 index 0000000..27a83ca --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Integration test: Simulate the complete sync flow +Tests the end-to-end process without actually dispatching +""" + +import json +import yaml +from pathlib import Path + + +def test_integration(): + """Test complete sync integration flow""" + + print("๐Ÿ”„ Running integration test...\n") + + # Step 1: Load registry + print("1๏ธโƒฃ Loading registry...") + registry_path = Path(__file__).parent.parent / "routes/registry.yaml" + with open(registry_path) as f: + registry = yaml.safe_load(f) + + active_orgs = [code for code, org in registry["orgs"].items() if org.get("status") == "active"] + print(f" โœ“ Loaded {len(registry['orgs'])} orgs, {len(active_orgs)} active") + + # Step 2: Check workflows exist + print("\n2๏ธโƒฃ Checking workflows...") + workflows_dir = Path(__file__).parent.parent / ".github/workflows" + + required_workflows = ["sync-to-orgs.yml", "auto-merge.yml", "ci.yml"] + for wf in required_workflows: + wf_path = workflows_dir / wf + assert wf_path.exists(), f"Missing {wf}" + + with open(wf_path) as f: + data = yaml.safe_load(f) + assert data is not None, f"Invalid YAML in {wf}" + + print(f" โœ“ {wf}") + + # Step 3: Simulate dispatch payload + print("\n3๏ธโƒฃ Simulating dispatch payload...") + for code in active_orgs: + org = registry["orgs"][code] + + for repo in org.get("repos", []): + payload = { + "event_type": "sync_from_bridge", + "client_payload": { + "source": "BlackRoad-OS/.github", + "ref": "main", + "timestamp": "2026-01-27T20:00:00Z" + } + } + print(f" โœ“ Would dispatch to {code}/{repo['name']}") + print(f" Payload: {json.dumps(payload, indent=8)}") + + # Step 4: Check test infrastructure + print("\n4๏ธโƒฃ Checking test infrastructure...") + test_file = Path(__file__).parent / "test_sync.py" + assert test_file.exists(), "test_sync.py not found" + print(f" โœ“ test_sync.py exists") + + # Step 5: Check documentation + print("\n5๏ธโƒฃ Checking documentation...") + docs = [ + Path(__file__).parent.parent / "docs/SYNC.md", + Path(__file__).parent.parent / "README.md", + ] + for doc in docs: + assert doc.exists(), f"Missing {doc.name}" + print(f" โœ“ {doc.name}") + + # Step 6: Check templates + print("\n6๏ธโƒฃ Checking templates...") + template = Path(__file__).parent.parent / "templates/workflows/sync-receiver.yml" + assert template.exists(), "sync-receiver.yml template not found" + + with open(template) as f: + data = yaml.safe_load(f) + # Check for repository_dispatch trigger + triggers = data.get(True, data.get("on", {})) + assert "repository_dispatch" in triggers, "Missing repository_dispatch trigger" + + print(f" โœ“ sync-receiver.yml template") + + # Summary + print("\n" + "=" * 50) + print("โœ… Integration test PASSED!") + print("\nReady to:") + print(" 1. Push to main โ†’ triggers sync-to-orgs.yml") + print(" 2. Dispatches to active org repos") + print(" 3. Target repos receive sync_from_bridge event") + print(" 4. PR auto-merges after approval + CI") + print("\n๐Ÿ’ก To test manually:") + print(" gh workflow run sync-to-orgs.yml -f dry_run=true") + + +if __name__ == "__main__": + test_integration() diff --git a/tests/test_sync.py b/tests/test_sync.py old mode 100644 new mode 100755 From caa4bce9f8e08200c89cc0b8144f35f563df5868 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:08:50 +0000 Subject: [PATCH 4/5] Address code review feedback: improve error handling, security, and documentation Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com> --- .github/workflows/auto-merge.yml | 11 ++++------- .github/workflows/sync-to-orgs.yml | 28 +++++++++++++++++++++------ docs/SYNC.md | 19 +++++++++++++----- templates/workflows/sync-receiver.yml | 16 ++++++++++++--- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index d18072f..b405842 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -6,9 +6,6 @@ name: Auto Merge on: pull_request_review: types: [submitted] - check_suite: - types: [completed] - status: {} workflow_run: workflows: ["CI"] types: [completed] @@ -43,13 +40,13 @@ jobs: # Extract PR number from workflow run PR_NUMBER=$(gh pr list --json number,headRefName --jq '.[] | select(.headRefName=="${{ github.event.workflow_run.head_branch }}") | .number' | head -1) else - echo "No PR found" - exit 0 + echo "No PR found for event type: ${{ github.event_name }}" + exit 1 fi if [ -z "$PR_NUMBER" ]; then - echo "No PR number found" - exit 0 + echo "No PR number found for branch ${{ github.event.workflow_run.head_branch }}" + exit 1 fi echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT diff --git a/.github/workflows/sync-to-orgs.yml b/.github/workflows/sync-to-orgs.yml index dcd27cd..841661c 100644 --- a/.github/workflows/sync-to-orgs.yml +++ b/.github/workflows/sync-to-orgs.yml @@ -73,7 +73,7 @@ jobs: - name: Dispatch to target orgs env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.DISPATCH_TOKEN || secrets.GITHUB_TOKEN }} TARGET_ORGS: ${{ inputs.target_orgs || 'all' }} DRY_RUN: ${{ inputs.dry_run || 'false' }} ORGS_JSON: ${{ steps.registry.outputs.orgs }} @@ -101,6 +101,9 @@ jobs: print(f'Dry run: {dry_run}') print('') + # Track failures + failures = [] + # Dispatch to each target org for org in orgs: if org['code'] not in target_codes: @@ -143,18 +146,31 @@ jobs: } try: - resp = requests.post(url, json=payload, headers=headers, timeout=10) + resp = requests.post(url, json=payload, headers=headers, timeout=30) if resp.status_code == 204: print(f' โœ“ Dispatched') elif resp.status_code == 404: - print(f' โš ๏ธ Repo not found or no dispatch workflow') + msg = f'{owner}/{repo_slug}: Repo not found or no dispatch workflow' + print(f' โš ๏ธ {msg}') + failures.append(msg) else: - print(f' โŒ Failed: {resp.status_code}') + msg = f'{owner}/{repo_slug}: HTTP {resp.status_code}' + print(f' โŒ {msg}') + failures.append(msg) except Exception as e: - print(f' โŒ Error: {e}') + msg = f'{owner}/{repo_slug}: {e}' + print(f' โŒ {msg}') + failures.append(msg) print('') - print('โœ“ Sync dispatch complete') + if failures: + print(f'โš ๏ธ {len(failures)} dispatch(es) failed:') + for failure in failures: + print(f' - {failure}') + print('') + print('Note: 404 errors are expected if target repos have not set up dispatch workflows yet.') + else: + print('โœ“ All dispatches successful') " - name: Summary diff --git a/docs/SYNC.md b/docs/SYNC.md index efe54a0..a25be9f 100644 --- a/docs/SYNC.md +++ b/docs/SYNC.md @@ -213,11 +213,20 @@ When making changes that affect other orgs: ## Security -- Use `GITHUB_TOKEN` for same-org dispatches -- Use PAT with minimal scope for cross-org dispatches -- Validate all payloads in target repos -- Never sync secrets or credentials -- Use dry-run mode when testing +- Uses repository dispatch for safe cross-repo communication +- Supports dry-run mode to test without dispatching +- No secrets or credentials are synced +- Target repos must explicitly set up receiver workflows + +### Cross-Organization Dispatches + +For syncing to repositories in different GitHub organizations: + +1. Create a Personal Access Token (PAT) with `repo` scope +2. Add it as a repository secret named `DISPATCH_TOKEN` +3. The workflow will use `DISPATCH_TOKEN` if available, falling back to `GITHUB_TOKEN` for same-org repos + +**Note**: `GITHUB_TOKEN` only has permissions within the current organization, so cross-org dispatches require a PAT. ## Troubleshooting diff --git a/templates/workflows/sync-receiver.yml b/templates/workflows/sync-receiver.yml index bb5c2ce..0a107da 100644 --- a/templates/workflows/sync-receiver.yml +++ b/templates/workflows/sync-receiver.yml @@ -1,5 +1,13 @@ # Shared workflow template for receiving sync updates from the bridge # This workflow should be added to target org repositories +# +# โš ๏ธ IMPORTANT NOTES: +# 1. This template commits directly to the default branch. For production use, +# consider creating a PR instead to allow review of changes. +# 2. The GITHUB_TOKEN may not have permissions to push to protected branches. +# If needed, use a PAT with appropriate permissions or configure branch +# protection to allow github-actions bot to push. +# 3. Customize the sync logic in steps below based on your needs. name: Sync from Bridge @@ -41,7 +49,8 @@ jobs: # Example: Fetch a shared CI workflow template # Uncomment and customize for your needs: - # curl -o .github/workflows/ci.yml \ + # curl -fsSL -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + # -o .github/workflows/ci.yml \ # https://raw.githubusercontent.com/BlackRoad-OS/.github/main/templates/workflows/ci-template.yml echo "โœ“ Workflows fetched" @@ -50,8 +59,9 @@ jobs: run: | echo "โš™๏ธ Fetching shared configurations..." - # Example: Fetch shared configs - # curl -o .editorconfig \ + # Example: Fetch shared configs with authentication + # curl -fsSL -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + # -o .editorconfig \ # https://raw.githubusercontent.com/BlackRoad-OS/.github/main/templates/configs/.editorconfig echo "โœ“ Configs fetched" From 46f5a4b8effc1c6128fa61941bd9f254cc5febbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:13:15 +0000 Subject: [PATCH 5/5] Changes before error encountered Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++++++ .github/workflows/sync-to-orgs.yml | 3 +++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c2f970..7990443 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,6 +183,10 @@ jobs: test-sync: name: Test Sync runs-on: ubuntu-latest + + permissions: + contents: read + steps: - uses: actions/checkout@v4 @@ -205,6 +209,10 @@ jobs: runs-on: ubuntu-latest needs: [lint, test-operator, test-dispatcher, test-webhooks, validate-config, test-sync] if: always() + + permissions: + contents: read + steps: - name: Check results run: | diff --git a/.github/workflows/sync-to-orgs.yml b/.github/workflows/sync-to-orgs.yml index 841661c..1798891 100644 --- a/.github/workflows/sync-to-orgs.yml +++ b/.github/workflows/sync-to-orgs.yml @@ -28,6 +28,9 @@ jobs: name: Sync to Organizations runs-on: ubuntu-latest + permissions: + contents: read + steps: - uses: actions/checkout@v4 with: