Skip to content

Commit 2dfadd8

Browse files
authored
feat: new github-membership script (#314)
1 parent a1ba95b commit 2dfadd8

File tree

6 files changed

+201
-1
lines changed

6 files changed

+201
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ the individual command README for details.
4040
- [`garden github-branch`](src/github/branch#readme)
4141
- [`garden github-commit`](src/github/commit#readme)
4242
- [`garden github-deploy`](src/github/deploy#readme)
43+
- [`garden github-membership`](src/github/membership#readme)
4344
- [`garden github-pages`](src/github/pages#readme)
4445
- [`garden github-release`](src/github/release#readme)
4546
- [`garden github-repository`](src/github/repository#readme)
@@ -68,6 +69,6 @@ conduct](.github/CODE_OF_CONDUCT.md). Please participate accordingly.
6869

6970
## License
7071

71-
Copyright 2021 Zendesk
72+
Copyright 2025 Zendesk
7273

7374
Licensed under the [Apache License, Version 2.0](LICENSE.md)

bin/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ program
3131
.addCommand(github.branchCommand(spinner))
3232
.addCommand(github.commitCommand(spinner))
3333
.addCommand(github.deployCommand(spinner))
34+
.addCommand(github.membershipCommand(spinner))
3435
.addCommand(github.pagesCommand(spinner))
3536
.addCommand(github.releaseCommand(spinner))
3637
.addCommand(github.repositoryCommand(spinner))

src/github/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { execute as branch, default as branchCommand } from './branch/index.js';
99
import { execute as commit, default as commitCommand } from './commit/index.js';
1010
import { execute as deploy, default as deployCommand } from './deploy/index.js';
11+
import { execute as membership, default as membershipCommand } from './membership/index.js';
1112
import { execute as pages, default as pagesCommand } from './pages/index.js';
1213
import { execute as release, default as releaseCommand } from './release/index.js';
1314
import { execute as repository, default as repositoryCommand } from './repository/index.js';
@@ -18,6 +19,7 @@ const commands = {
1819
branchCommand,
1920
commitCommand,
2021
deployCommand,
22+
membershipCommand,
2123
pagesCommand,
2224
releaseCommand,
2325
repositoryCommand,
@@ -33,6 +35,8 @@ export {
3335
commitCommand,
3436
deploy,
3537
deployCommand,
38+
membership,
39+
membershipCommand,
3640
pages,
3741
pagesCommand,
3842
release,

src/github/membership/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# `garden github-membership`
2+
3+
Manage GitHub organization [membership](https://docs.github.com/en/rest/orgs/members).
4+
5+
## Usage
6+
7+
```ts
8+
import { githubMembership } from '@zendeskgarden/scripts';
9+
10+
const args: {
11+
org?: string;
12+
users?: string[];
13+
path?: string;
14+
token?: string;
15+
} = {
16+
/* optional overrides */
17+
};
18+
19+
(async () => {
20+
const { success, failure } = await githubMembership(args);
21+
22+
console.log(success, failure);
23+
})();
24+
```
25+
26+
### Arguments
27+
28+
- `org` optional GitHub organization name; defaults to repository owner.
29+
- `delete` optional list of members to remove.
30+
- `list` determine if output should be listed; defaults to a numeric summary.
31+
- `path` optional path to a git directory; defaults to the current directory.
32+
- `token` optional GitHub personal access token; defaults to the value
33+
provided by [`githubToken`](../token#readme).
34+
35+
## Command
36+
37+
```sh
38+
garden github-membership \
39+
[--org [org]] \
40+
[--delete <users...>] \
41+
[--path <path>] \
42+
[--token <token>]
43+
```

src/github/membership/index.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import { repository as getRepository, token as getToken } from '../index.js';
9+
import { handleErrorMessage, handleSuccessMessage } from '../../utils/index.js';
10+
import { Command } from 'commander';
11+
import { Octokit } from '@octokit/rest';
12+
import { Ora } from 'ora';
13+
14+
interface IGitHubMembershipArgs {
15+
org?: string;
16+
users?: string[];
17+
path?: string;
18+
token?: string;
19+
spinner?: Ora;
20+
}
21+
22+
type RETVAL = {
23+
success: string[];
24+
failure: string[];
25+
};
26+
27+
/**
28+
* Execute the `github-membership` command.
29+
*
30+
* @param {string} [args.org] GitHub organization name.
31+
* @param {string[]} [args.users] GitHub user names to remove from membership.
32+
* @param {string} [args.path] Path to a git directory.
33+
* @param {string} [args.token] GitHub personal access token.
34+
* @param {string} [args.spinner] Terminal spinner.
35+
*
36+
* @returns {Promise<object>} The success and failure results of the membership command.
37+
*/
38+
export const execute = async (args: IGitHubMembershipArgs): Promise<RETVAL> => {
39+
const retVal: RETVAL = { success: [], failure: [] };
40+
41+
try {
42+
const auth = args.token || (await getToken(args.spinner));
43+
const github = new Octokit({ auth });
44+
const org = (args.org || (await getRepository(args.path, args.spinner))?.owner)!;
45+
46+
if (args.users) {
47+
const requests = [];
48+
49+
for (const user of args.users) {
50+
/* https://octokit.github.io/rest.js/v21/#orgs-remove-member */
51+
const request = github.orgs.removeMember({ org, username: user });
52+
53+
requests.push(request);
54+
}
55+
56+
await Promise.allSettled(requests).then(results => {
57+
results.forEach(result => {
58+
if (result.status === 'fulfilled') {
59+
const user = result.value.url.split('/').pop();
60+
61+
retVal.success.push(user!);
62+
} else {
63+
const user = result.reason.response.url.split('/').pop();
64+
65+
if (typeof user === 'string') {
66+
retVal.failure.push(user);
67+
}
68+
69+
handleErrorMessage(result.reason, 'github-membership', args.spinner);
70+
}
71+
});
72+
});
73+
} else {
74+
/* https://octokit.github.io/rest.js/v21/#orgs-list-members */
75+
const members = await github.paginate(github.orgs.listMembers, { org, per_page: 100 });
76+
77+
retVal.success = members.map(member => member.login).sort();
78+
}
79+
} catch (error: unknown) {
80+
handleErrorMessage(error, 'github-membership', args.spinner);
81+
82+
throw error;
83+
}
84+
85+
return retVal;
86+
};
87+
88+
export default (spinner: Ora): Command => {
89+
const command = new Command('github-membership');
90+
91+
return command
92+
.description('manage GitHub organization membership')
93+
.option('-o, --org [org]', 'GitHub organization name; defaults to repository owner')
94+
.option('-d --delete <users...>', 'remove members')
95+
.option('-l --list', 'list members')
96+
.option('-p, --path <path>', 'git directory')
97+
.option('-t, --token <token>', 'access token')
98+
.action(async () => {
99+
try {
100+
spinner.start();
101+
102+
const options = command.opts();
103+
const results = await execute({
104+
org: options.org,
105+
users: options.delete,
106+
path: options.path,
107+
token: options.token,
108+
spinner
109+
});
110+
const toMessage = (members: string[]): string => {
111+
let retVal: string;
112+
113+
if (options.list) {
114+
retVal = members.join(', ');
115+
} else {
116+
const length = members.length;
117+
118+
retVal = `${length} ${length === 1 ? 'member' : 'members'}`;
119+
}
120+
121+
return retVal;
122+
};
123+
124+
if (results.success.length > 0) {
125+
let message = toMessage(results.success);
126+
127+
if (options.delete) {
128+
message = `Removed: ${message}`;
129+
}
130+
131+
handleSuccessMessage(message, spinner);
132+
}
133+
134+
if (results.failure.length > 0) {
135+
let message = toMessage(results.failure);
136+
137+
if (options.delete) {
138+
message = `Failed: ${message}`;
139+
}
140+
141+
handleErrorMessage(message, 'github-membership', spinner);
142+
}
143+
} catch {
144+
spinner.fail('GitHub membership not found');
145+
process.exitCode = 1;
146+
} finally {
147+
spinner.stop();
148+
}
149+
});
150+
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {
1010
branch as githubBranch,
1111
commit as githubCommit,
1212
deploy as githubDeploy,
13+
membership as githubMembership,
1314
pages as githubPages,
1415
release as githubRelease,
1516
repository as githubRepository,

0 commit comments

Comments
 (0)