Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions REPOSITORIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,60 @@ Each repository is defined using markdown headers and properties:
- Topics: documentation, organization-management, governance
```

## Repository Migration (Coming Soon)
## Repository Migration

🚧 **Feature in development** - Repository transfer automation is not yet implemented.
🚧 **Feature partially implemented** - Permission verification complete, transfer API pending.

The `Origin` field will enable migrating repositories from "powered by worlddriven" to "worlddriven project":
The `Origin` field enables migrating repositories from "powered by worlddriven" to "worlddriven project":

- **Powered by worlddriven**: Repository stays under owner's control, uses worlddriven for PR automation
- **Worlddriven project**: Repository lives in worlddriven org with full democratic governance

**Planned workflow** (not yet functional):
1. Origin repository owner grants admin permissions to worlddriven org
2. Add repository to REPOSITORIES.md with `Origin: owner/repo-name`
3. Drift detection verifies permissions
4. On merge, repository automatically transfers to worlddriven org
5. Standard democratic configurations applied
### How to Grant Transfer Permissions

**Current status**: Parser supports Origin field, transfer logic pending implementation.
Track progress in the GitHub issue for repository migration feature.
Before adding a repository with an `Origin` field, the repository owner must grant worlddriven admin access:

1. **Navigate to repository settings**: `https://github.com/OWNER/REPO/settings/access`
2. **Invite collaborator**: Click "Add people" or "Add teams"
3. **Add worlddriven org**: Search for and select "worlddriven"
4. **Grant admin role**: Select "Admin" permission level
5. **Confirm invitation**: worlddriven org will automatically accept

**Why admin access?** GitHub's transfer API requires admin permission on the source repository to initiate a transfer.

### Migration Workflow

**Current implementation** (permission verification):
1. ✅ Repository owner grants worlddriven admin access to origin repository
2. ✅ Add repository to REPOSITORIES.md with `Origin: owner/repo-name`
3. ✅ Drift detection automatically checks if worlddriven has admin permission
4. ✅ PR comments show permission status: "Ready" or "Blocked"
5. 🚧 On merge, repository transfer (API implementation pending)

**What's implemented:**
- ✅ Parser supports Origin field
- ✅ Permission verification via GitHub API
- ✅ Clear feedback in drift detection and PR comments
- 🚧 Transfer API call (pending - see issue #9)

**What happens when you add Origin field:**
- Drift detection checks if worlddriven has admin access to origin repo
- PR comment shows: ✅ "Ready to transfer" or ❌ "Missing admin permission"
- If permission missing, PR comment includes instructions for granting access
- Transfer action appears in sync plan (but won't execute until API is implemented)

### Example

```markdown
## my-project
- Description: My awesome democratic project
- Topics: worlddriven, democracy
- Origin: myusername/my-project
```

**Before adding**: Grant worlddriven admin access to `myusername/my-project`

Track implementation progress in GitHub issue #9.

---

Expand Down
156 changes: 156 additions & 0 deletions scripts/check-transfer-permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env node

/**
* Check if worlddriven organization has admin permission on a repository
* Required for repository transfer automation
*/

const GITHUB_API_BASE = 'https://api.github.com';
const ORG_NAME = 'worlddriven';

/**
* Check if worlddriven org has admin permission on the origin repository
*
* @param {string} token - GitHub token (WORLDDRIVEN_GITHUB_TOKEN)
* @param {string} originRepo - Repository in format "owner/repo-name"
* @returns {Promise<{hasPermission: boolean, permissionLevel: string, details: string}>}
*/
export async function checkTransferPermission(token, originRepo) {
if (!token) {
throw new Error('GitHub token is required');
}

if (!originRepo || !originRepo.includes('/')) {
throw new Error('Origin repository must be in format "owner/repo-name"');
}

const [owner, repo] = originRepo.split('/');

if (!owner || !repo) {
throw new Error('Invalid origin repository format');
}

try {
// Check if worlddriven org has admin permission on the origin repository
const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/collaborators/${ORG_NAME}/permission`;

const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
});

// Handle different response scenarios
if (response.status === 404) {
// Repository doesn't exist or worlddriven doesn't have any permission
return {
hasPermission: false,
permissionLevel: 'none',
details: `Repository ${originRepo} not found or worlddriven has no access`,
};
}

if (!response.ok) {
// Other errors (rate limit, auth issues, etc.)
const error = await response.text();
return {
hasPermission: false,
permissionLevel: 'unknown',
details: `Failed to check permissions: ${response.status} - ${error}`,
};
}

const data = await response.json();

// GitHub returns permission level: "admin", "write", "read", or "none"
const permissionLevel = data.permission || 'none';
const hasPermission = permissionLevel === 'admin';

return {
hasPermission,
permissionLevel,
details: hasPermission
? `✅ ${ORG_NAME} has admin access to ${originRepo}`
: `❌ ${ORG_NAME} has "${permissionLevel}" access to ${originRepo} (admin required)`,
};

} catch (error) {
// Network errors, JSON parsing errors, etc.
return {
hasPermission: false,
permissionLevel: 'error',
details: `Error checking permissions: ${error.message}`,
};
}
}

/**
* Check permissions for multiple repositories
*
* @param {string} token - GitHub token
* @param {Array<string>} originRepos - Array of repository identifiers in format "owner/repo-name"
* @returns {Promise<Map<string, Object>>} Map of origin repo to permission result
*/
export async function checkMultipleTransferPermissions(token, originRepos) {
const results = new Map();

for (const originRepo of originRepos) {
const result = await checkTransferPermission(token, originRepo);
results.set(originRepo, result);
}

return results;
}

/**
* Main function for CLI usage
*/
async function main() {
const args = process.argv.slice(2);
const token = process.env.WORLDDRIVEN_GITHUB_TOKEN;

if (!token) {
console.error('❌ Error: WORLDDRIVEN_GITHUB_TOKEN environment variable is not set');
process.exit(1);
}

if (args.length === 0) {
console.error('Usage: check-transfer-permissions.js <owner/repo> [<owner/repo2> ...]');
console.error('');
console.error('Example:');
console.error(' check-transfer-permissions.js TooAngel/worlddriven');
process.exit(1);
}

try {
console.error(`Checking transfer permissions for ${args.length} repository(ies)...\n`);

for (const originRepo of args) {
const result = await checkTransferPermission(token, originRepo);
console.log(`${originRepo}:`);
console.log(` Permission Level: ${result.permissionLevel}`);
console.log(` Can Transfer: ${result.hasPermission ? '✅ Yes' : '❌ No'}`);
console.log(` Details: ${result.details}`);
console.log('');
}

// Exit with error if any repository doesn't have admin permission
const allResults = await Promise.all(
args.map(repo => checkTransferPermission(token, repo))
);
const allHavePermission = allResults.every(r => r.hasPermission);

process.exit(allHavePermission ? 0 : 1);

} catch (error) {
console.error(`❌ Error: ${error.message}`);
process.exit(1);
}
}

// CLI usage
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
96 changes: 96 additions & 0 deletions scripts/check-transfer-permissions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env node

import { describe, test } from 'node:test';
import assert from 'node:assert';
import { checkTransferPermission } from './check-transfer-permissions.js';

describe('checkTransferPermission', () => {
test('should throw error if token is missing', async () => {
await assert.rejects(
async () => await checkTransferPermission(null, 'owner/repo'),
{ message: 'GitHub token is required' }
);
});

test('should throw error if originRepo is missing', async () => {
await assert.rejects(
async () => await checkTransferPermission('token', ''),
{ message: 'Origin repository must be in format "owner/repo-name"' }
);
});

test('should throw error if originRepo format is invalid', async () => {
await assert.rejects(
async () => await checkTransferPermission('token', 'invalid-format'),
{ message: 'Origin repository must be in format "owner/repo-name"' }
);
});

test('should throw error if originRepo has empty owner or repo', async () => {
await assert.rejects(
async () => await checkTransferPermission('token', '/repo'),
{ message: 'Invalid origin repository format' }
);

await assert.rejects(
async () => await checkTransferPermission('token', 'owner/'),
{ message: 'Invalid origin repository format' }
);
});

// Note: The following tests would require mocking the fetch API
// or using a test GitHub token with known repositories.
// For now, we document the expected behavior:

/**
* Test case for admin permission (success scenario):
* - Repository exists
* - worlddriven has admin access
* - Expected result:
* {
* hasPermission: true,
* permissionLevel: 'admin',
* details: '✅ worlddriven has admin access to owner/repo'
* }
*/

/**
* Test case for write permission (insufficient):
* - Repository exists
* - worlddriven has write (but not admin) access
* - Expected result:
* {
* hasPermission: false,
* permissionLevel: 'write',
* details: '❌ worlddriven has "write" access to owner/repo (admin required)'
* }
*/

/**
* Test case for non-existent repository:
* - Repository doesn't exist or worlddriven has no access
* - API returns 404
* - Expected result:
* {
* hasPermission: false,
* permissionLevel: 'none',
* details: 'Repository owner/repo not found or worlddriven has no access'
* }
*/

/**
* Test case for API errors:
* - Network errors, rate limits, etc.
* - Expected result:
* {
* hasPermission: false,
* permissionLevel: 'error' or 'unknown',
* details: 'Error checking permissions: ...'
* }
*/
});

// To run integration tests with actual GitHub API:
// 1. Set WORLDDRIVEN_GITHUB_TOKEN environment variable
// 2. Create test repositories with known permission levels
// 3. Run: node --test scripts/check-transfer-permissions.test.js
Loading