Skip to content

Commit ca4719a

Browse files
authored
feat: implement permission verification for repository transfers (#11)
Add automated permission checking to verify worlddriven org has admin access to origin repositories before attempting transfer. This completes step 1 of issue #9 (permission verification) and sets the foundation for transfer API implementation. Changes: - Created check-transfer-permissions.js module with GitHub API integration - Added unit tests for permission verification logic - Integrated permission checking into drift detection workflow - Enhanced drift reports to show permission status (ready/blocked) - Updated sync plan to include transfer actions when permissions verified - Updated REPOSITORIES.md with detailed permission granting instructions Permission Verification: - Checks if worlddriven has admin access to origin repository - Uses GitHub API: GET /repos/{owner}/{repo}/collaborators/worlddriven/permission - Reports clear status: admin (ready), write/read (insufficient), or none - Gracefully handles errors and provides actionable feedback User Experience: - PR comments show permission status for each pending transfer - Clear instructions for granting admin access - Transfer actions appear in sync plan (execution pending API implementation) - No breaking changes - existing functionality unchanged Closes #9 (partial - permission verification complete)
1 parent 712f234 commit ca4719a

7 files changed

Lines changed: 461 additions & 36 deletions

REPOSITORIES.md

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,60 @@ Each repository is defined using markdown headers and properties:
4848
- Topics: documentation, organization-management, governance
4949
```
5050

51-
## Repository Migration (Coming Soon)
51+
## Repository Migration
5252

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

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

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

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

67-
**Current status**: Parser supports Origin field, transfer logic pending implementation.
68-
Track progress in the GitHub issue for repository migration feature.
62+
Before adding a repository with an `Origin` field, the repository owner must grant worlddriven admin access:
63+
64+
1. **Navigate to repository settings**: `https://github.com/OWNER/REPO/settings/access`
65+
2. **Invite collaborator**: Click "Add people" or "Add teams"
66+
3. **Add worlddriven org**: Search for and select "worlddriven"
67+
4. **Grant admin role**: Select "Admin" permission level
68+
5. **Confirm invitation**: worlddriven org will automatically accept
69+
70+
**Why admin access?** GitHub's transfer API requires admin permission on the source repository to initiate a transfer.
71+
72+
### Migration Workflow
73+
74+
**Current implementation** (permission verification):
75+
1. ✅ Repository owner grants worlddriven admin access to origin repository
76+
2. ✅ Add repository to REPOSITORIES.md with `Origin: owner/repo-name`
77+
3. ✅ Drift detection automatically checks if worlddriven has admin permission
78+
4. ✅ PR comments show permission status: "Ready" or "Blocked"
79+
5. 🚧 On merge, repository transfer (API implementation pending)
80+
81+
**What's implemented:**
82+
- ✅ Parser supports Origin field
83+
- ✅ Permission verification via GitHub API
84+
- ✅ Clear feedback in drift detection and PR comments
85+
- 🚧 Transfer API call (pending - see issue #9)
86+
87+
**What happens when you add Origin field:**
88+
- Drift detection checks if worlddriven has admin access to origin repo
89+
- PR comment shows: ✅ "Ready to transfer" or ❌ "Missing admin permission"
90+
- If permission missing, PR comment includes instructions for granting access
91+
- Transfer action appears in sync plan (but won't execute until API is implemented)
92+
93+
### Example
94+
95+
```markdown
96+
## my-project
97+
- Description: My awesome democratic project
98+
- Topics: worlddriven, democracy
99+
- Origin: myusername/my-project
100+
```
101+
102+
**Before adding**: Grant worlddriven admin access to `myusername/my-project`
103+
104+
Track implementation progress in GitHub issue #9.
69105

70106
---
71107

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Check if worlddriven organization has admin permission on a repository
5+
* Required for repository transfer automation
6+
*/
7+
8+
const GITHUB_API_BASE = 'https://api.github.com';
9+
const ORG_NAME = 'worlddriven';
10+
11+
/**
12+
* Check if worlddriven org has admin permission on the origin repository
13+
*
14+
* @param {string} token - GitHub token (WORLDDRIVEN_GITHUB_TOKEN)
15+
* @param {string} originRepo - Repository in format "owner/repo-name"
16+
* @returns {Promise<{hasPermission: boolean, permissionLevel: string, details: string}>}
17+
*/
18+
export async function checkTransferPermission(token, originRepo) {
19+
if (!token) {
20+
throw new Error('GitHub token is required');
21+
}
22+
23+
if (!originRepo || !originRepo.includes('/')) {
24+
throw new Error('Origin repository must be in format "owner/repo-name"');
25+
}
26+
27+
const [owner, repo] = originRepo.split('/');
28+
29+
if (!owner || !repo) {
30+
throw new Error('Invalid origin repository format');
31+
}
32+
33+
try {
34+
// Check if worlddriven org has admin permission on the origin repository
35+
const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/collaborators/${ORG_NAME}/permission`;
36+
37+
const response = await fetch(url, {
38+
headers: {
39+
'Authorization': `Bearer ${token}`,
40+
'Accept': 'application/vnd.github+json',
41+
'X-GitHub-Api-Version': '2022-11-28',
42+
},
43+
});
44+
45+
// Handle different response scenarios
46+
if (response.status === 404) {
47+
// Repository doesn't exist or worlddriven doesn't have any permission
48+
return {
49+
hasPermission: false,
50+
permissionLevel: 'none',
51+
details: `Repository ${originRepo} not found or worlddriven has no access`,
52+
};
53+
}
54+
55+
if (!response.ok) {
56+
// Other errors (rate limit, auth issues, etc.)
57+
const error = await response.text();
58+
return {
59+
hasPermission: false,
60+
permissionLevel: 'unknown',
61+
details: `Failed to check permissions: ${response.status} - ${error}`,
62+
};
63+
}
64+
65+
const data = await response.json();
66+
67+
// GitHub returns permission level: "admin", "write", "read", or "none"
68+
const permissionLevel = data.permission || 'none';
69+
const hasPermission = permissionLevel === 'admin';
70+
71+
return {
72+
hasPermission,
73+
permissionLevel,
74+
details: hasPermission
75+
? `✅ ${ORG_NAME} has admin access to ${originRepo}`
76+
: `❌ ${ORG_NAME} has "${permissionLevel}" access to ${originRepo} (admin required)`,
77+
};
78+
79+
} catch (error) {
80+
// Network errors, JSON parsing errors, etc.
81+
return {
82+
hasPermission: false,
83+
permissionLevel: 'error',
84+
details: `Error checking permissions: ${error.message}`,
85+
};
86+
}
87+
}
88+
89+
/**
90+
* Check permissions for multiple repositories
91+
*
92+
* @param {string} token - GitHub token
93+
* @param {Array<string>} originRepos - Array of repository identifiers in format "owner/repo-name"
94+
* @returns {Promise<Map<string, Object>>} Map of origin repo to permission result
95+
*/
96+
export async function checkMultipleTransferPermissions(token, originRepos) {
97+
const results = new Map();
98+
99+
for (const originRepo of originRepos) {
100+
const result = await checkTransferPermission(token, originRepo);
101+
results.set(originRepo, result);
102+
}
103+
104+
return results;
105+
}
106+
107+
/**
108+
* Main function for CLI usage
109+
*/
110+
async function main() {
111+
const args = process.argv.slice(2);
112+
const token = process.env.WORLDDRIVEN_GITHUB_TOKEN;
113+
114+
if (!token) {
115+
console.error('❌ Error: WORLDDRIVEN_GITHUB_TOKEN environment variable is not set');
116+
process.exit(1);
117+
}
118+
119+
if (args.length === 0) {
120+
console.error('Usage: check-transfer-permissions.js <owner/repo> [<owner/repo2> ...]');
121+
console.error('');
122+
console.error('Example:');
123+
console.error(' check-transfer-permissions.js TooAngel/worlddriven');
124+
process.exit(1);
125+
}
126+
127+
try {
128+
console.error(`Checking transfer permissions for ${args.length} repository(ies)...\n`);
129+
130+
for (const originRepo of args) {
131+
const result = await checkTransferPermission(token, originRepo);
132+
console.log(`${originRepo}:`);
133+
console.log(` Permission Level: ${result.permissionLevel}`);
134+
console.log(` Can Transfer: ${result.hasPermission ? '✅ Yes' : '❌ No'}`);
135+
console.log(` Details: ${result.details}`);
136+
console.log('');
137+
}
138+
139+
// Exit with error if any repository doesn't have admin permission
140+
const allResults = await Promise.all(
141+
args.map(repo => checkTransferPermission(token, repo))
142+
);
143+
const allHavePermission = allResults.every(r => r.hasPermission);
144+
145+
process.exit(allHavePermission ? 0 : 1);
146+
147+
} catch (error) {
148+
console.error(`❌ Error: ${error.message}`);
149+
process.exit(1);
150+
}
151+
}
152+
153+
// CLI usage
154+
if (import.meta.url === `file://${process.argv[1]}`) {
155+
main();
156+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env node
2+
3+
import { describe, test } from 'node:test';
4+
import assert from 'node:assert';
5+
import { checkTransferPermission } from './check-transfer-permissions.js';
6+
7+
describe('checkTransferPermission', () => {
8+
test('should throw error if token is missing', async () => {
9+
await assert.rejects(
10+
async () => await checkTransferPermission(null, 'owner/repo'),
11+
{ message: 'GitHub token is required' }
12+
);
13+
});
14+
15+
test('should throw error if originRepo is missing', async () => {
16+
await assert.rejects(
17+
async () => await checkTransferPermission('token', ''),
18+
{ message: 'Origin repository must be in format "owner/repo-name"' }
19+
);
20+
});
21+
22+
test('should throw error if originRepo format is invalid', async () => {
23+
await assert.rejects(
24+
async () => await checkTransferPermission('token', 'invalid-format'),
25+
{ message: 'Origin repository must be in format "owner/repo-name"' }
26+
);
27+
});
28+
29+
test('should throw error if originRepo has empty owner or repo', async () => {
30+
await assert.rejects(
31+
async () => await checkTransferPermission('token', '/repo'),
32+
{ message: 'Invalid origin repository format' }
33+
);
34+
35+
await assert.rejects(
36+
async () => await checkTransferPermission('token', 'owner/'),
37+
{ message: 'Invalid origin repository format' }
38+
);
39+
});
40+
41+
// Note: The following tests would require mocking the fetch API
42+
// or using a test GitHub token with known repositories.
43+
// For now, we document the expected behavior:
44+
45+
/**
46+
* Test case for admin permission (success scenario):
47+
* - Repository exists
48+
* - worlddriven has admin access
49+
* - Expected result:
50+
* {
51+
* hasPermission: true,
52+
* permissionLevel: 'admin',
53+
* details: '✅ worlddriven has admin access to owner/repo'
54+
* }
55+
*/
56+
57+
/**
58+
* Test case for write permission (insufficient):
59+
* - Repository exists
60+
* - worlddriven has write (but not admin) access
61+
* - Expected result:
62+
* {
63+
* hasPermission: false,
64+
* permissionLevel: 'write',
65+
* details: '❌ worlddriven has "write" access to owner/repo (admin required)'
66+
* }
67+
*/
68+
69+
/**
70+
* Test case for non-existent repository:
71+
* - Repository doesn't exist or worlddriven has no access
72+
* - API returns 404
73+
* - Expected result:
74+
* {
75+
* hasPermission: false,
76+
* permissionLevel: 'none',
77+
* details: 'Repository owner/repo not found or worlddriven has no access'
78+
* }
79+
*/
80+
81+
/**
82+
* Test case for API errors:
83+
* - Network errors, rate limits, etc.
84+
* - Expected result:
85+
* {
86+
* hasPermission: false,
87+
* permissionLevel: 'error' or 'unknown',
88+
* details: 'Error checking permissions: ...'
89+
* }
90+
*/
91+
});
92+
93+
// To run integration tests with actual GitHub API:
94+
// 1. Set WORLDDRIVEN_GITHUB_TOKEN environment variable
95+
// 2. Create test repositories with known permission levels
96+
// 3. Run: node --test scripts/check-transfer-permissions.test.js

0 commit comments

Comments
 (0)