Skip to content

Commit 2fe63c7

Browse files
authored
Merge pull request #298 from capralifecycle/cals-1241-simplify-cli-commands
CALS-1247: Flatten and simplify command structure
2 parents 2b0236b + 9d43ff9 commit 2fe63c7

File tree

12 files changed

+279
-174
lines changed

12 files changed

+279
-174
lines changed

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,16 @@ clean-all: clean
5454
.PHONY: upgrade-deps
5555
upgrade-deps:
5656
npm run upgrade-deps
57+
58+
# Manual test targets (requires CALS_GITHUB_TOKEN env var)
59+
.PHONY: test-repos
60+
test-repos:
61+
CALS_GITHUB_TOKEN=$(CALS_GITHUB_TOKEN) node lib/cals-cli.mjs repos --org capralifecycle --compact
62+
63+
.PHONY: test-clone
64+
test-clone:
65+
CALS_GITHUB_TOKEN=$(CALS_GITHUB_TOKEN) node lib/cals-cli.mjs clone --org capralifecycle --all | head -5
66+
67+
.PHONY: test-help
68+
test-help:
69+
node lib/cals-cli.mjs --help

README.md

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,52 +13,61 @@ It is recommended to use `npx` over global install to ensure you
1313
always run the latest version. If you install it globally remember
1414
to update it before running.
1515

16-
## Build
16+
## Commands
1717

18-
Build and verify:
18+
### Authentication
1919

20-
```sh
21-
$ make # or "make build"
20+
Set your GitHub token (will be stored in the OS keychain):
21+
22+
```bash
23+
cals auth
2224
```
2325

24-
## Contributing
26+
### List repositories
2527

26-
This project uses [semantic release](https://semantic-release.gitbook.io/semantic-release/)
27-
to automate releases and follows
28-
[Git commit guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
29-
from the Angular project.
28+
```bash
29+
cals repos --org capralifecycle
30+
cals repos --org capralifecycle --compact
31+
cals repos --org capralifecycle --csv
32+
```
33+
34+
### List repository groups
35+
36+
```bash
37+
cals groups --org capralifecycle
38+
```
39+
40+
### Generate clone commands
3041

31-
Version numbers depend on the commit type and footers: https://github.com/semantic-release/commit-analyzer/blob/75c9c87c88772d7ded4ca9614852b42519e41931/lib/default-release-rules.js#L7-L12
42+
Generate clone commands (pipe to bash to execute):
3243

33-
## Goals of CLI
44+
```bash
45+
cals clone --org capralifecycle --all | bash
46+
cals clone --org capralifecycle mygroup | bash
47+
```
3448

35-
- Provide an uniform way of consistently doing repeatable CALS tasks
36-
- Provide simple guidelines to improve the experience for developers
37-
- A tool that everybody uses and gets ownership of
38-
- Automate repeatable CALS tasks as we go
49+
### Sync repositories
3950

40-
## Ideas and future work
51+
Pull latest changes for all repositories in a directory managed by a `.cals.yaml` manifest:
4152

42-
- Automate onboarding of people
43-
- Granting access to various resources: AWS, GitHub, Confluence, JIRA, Slack, ...
44-
- Automate offboarding of people
45-
- Automate generation of new projects/resources
46-
- Creating GitHub repos, giving permissions etc
47-
- Slack channels
48-
- AWS account and structure
49-
- Checklist for manual processes
50-
- AWS infrastructure management, e.g. scripts such as https://github.com/capralifecycle/rvr-aws-infrastructure/blob/master/rvr/create-stack.sh
51-
- `cals aws ...`
53+
```bash
54+
cals sync
55+
cals sync --clone # Prompt to clone missing repos
56+
```
5257

53-
### Snyk management
58+
## Build
5459

55-
https://snyk.docs.apiary.io/reference/projects
60+
Build and verify:
5661

57-
- [ ] Automatically set up project in Snyk
58-
- [x] Report of which repos are in Snyk and which is not
59-
- [ ] Detect active vs disabled projects in Snyk (no way through API now?)
60-
- [x] Report issues in Snyk grouped by our projects
62+
```sh
63+
$ make # or "make build"
64+
```
6165

6266
## Contributing
6367

64-
This project doesn't currently accept contributions. For inquiries, please contact the maintainers at [Slack](https://liflig.slack.com/archives/C02T4KTPYS2).
68+
This project uses [semantic release](https://semantic-release.gitbook.io/semantic-release/)
69+
to automate releases and follows
70+
[Git commit guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
71+
from the Angular project.
72+
73+
For inquiries, please contact the maintainers at [Slack](https://liflig.slack.com/archives/C02T4KTPYS2).
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { CommandModule } from "yargs"
2-
import { GitHubTokenCliProvider } from "../../../github/token"
3-
import { type Reporter, readInput } from "../../reporter"
4-
import { createReporter } from "../../util"
2+
import { GitHubTokenCliProvider } from "../../github/token"
3+
import { type Reporter, readInput } from "../reporter"
4+
import { createReporter } from "../util"
55

6-
async function setToken({
6+
async function authenticate({
77
reporter,
88
token,
99
tokenProvider,
@@ -18,26 +18,26 @@ async function setToken({
1818
"https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook",
1919
)
2020
const inputToken = await readInput({
21-
prompt: "Enter new GitHub API token: ",
21+
prompt: "Enter GitHub token: ",
2222
silent: true,
2323
})
2424
token = inputToken
2525
}
2626

2727
await tokenProvider.setToken(token)
28-
reporter.info("Token saved")
28+
reporter.info("Token saved to keychain")
2929
}
3030

3131
const command: CommandModule = {
32-
command: "set-token",
33-
describe: "Set GitHub token for API calls",
32+
command: "auth [token]",
33+
describe: "Authenticate with GitHub",
3434
builder: (yargs) =>
3535
yargs.positional("token", {
36-
describe:
37-
"Token. If not provided it will be requested as input. Can be generated at https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook",
36+
describe: "GitHub token (prompted if not provided)",
37+
type: "string",
3838
}),
3939
handler: async (argv) => {
40-
await setToken({
40+
await authenticate({
4141
reporter: createReporter(),
4242
token: argv.token as string | undefined,
4343
tokenProvider: new GitHubTokenCliProvider(),

src/cli/commands/github/generate-clone-commands.ts renamed to src/cli/commands/clone.ts

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,38 @@ import path from "node:path"
33
import process from "node:process"
44
import yargs, { type CommandModule } from "yargs"
55
import { hideBin } from "yargs/helpers"
6-
import type { Config } from "../../../config"
7-
import { createGitHubService, type GitHubService } from "../../../github"
8-
import { getGroupedRepos, includesTopic } from "../../../github/util"
9-
import type { Reporter } from "../../reporter"
10-
import { createCacheProvider, createConfig, createReporter } from "../../util"
6+
import type { Config } from "../../config"
7+
import { createGitHubService, type GitHubService } from "../../github"
8+
import { getGroupedRepos, includesTopic } from "../../github/util"
9+
import { createCacheProvider, createConfig } from "../util"
1110

1211
async function generateCloneCommands({
13-
reporter,
1412
config,
1513
github,
1614
org,
1715
...opt
1816
}: {
19-
reporter: Reporter
2017
config: Config
2118
github: GitHubService
2219
all: boolean
23-
excludeExisting: boolean
20+
skipCloned: boolean
2421
group: string | undefined
2522
includeArchived: boolean
26-
listGroups: boolean
2723
name: string | undefined
2824
topic: string | undefined
2925
org: string
3026
}) {
31-
if (!opt.listGroups && !opt.all && opt.group === undefined) {
27+
if (!opt.all && opt.group === undefined) {
3228
yargs(hideBin(process.argv)).showHelp()
3329
return
3430
}
3531

3632
const repos = await github.getOrgRepoList({ org })
3733
const groups = getGroupedRepos(repos)
3834

39-
if (opt.listGroups) {
40-
groups.forEach((it) => {
41-
reporter.log(it.name)
42-
})
43-
return
44-
}
45-
46-
groups.forEach((group) => {
35+
for (const group of groups) {
4736
if (opt.group !== undefined && opt.group !== group.name) {
48-
return
37+
continue
4938
}
5039

5140
group.items
@@ -54,39 +43,35 @@ async function generateCloneCommands({
5443
.filter((it) => opt.topic === undefined || includesTopic(it, opt.topic))
5544
.filter(
5645
(it) =>
57-
!opt.excludeExisting ||
58-
!fs.existsSync(path.resolve(config.cwd, it.name)),
46+
!opt.skipCloned || !fs.existsSync(path.resolve(config.cwd, it.name)),
5947
)
6048
.forEach((repo) => {
6149
// The output of this is used to pipe into e.g. bash.
62-
// We cannot use reporter.log as it adds additional characters.
6350
process.stdout.write(
6451
`[ ! -e "${repo.name}" ] && git clone ${repo.sshUrl}\n`,
6552
)
6653
})
67-
})
54+
}
6855
}
6956

7057
const command: CommandModule = {
71-
command: "generate-clone-commands",
72-
describe: "Generate shell commands to clone GitHub repos for an organization",
58+
command: "clone [group]",
59+
describe: "Generate git clone commands (pipe to bash to execute)",
7360
builder: (yargs) =>
7461
yargs
7562
.positional("group", {
76-
describe: "Group to generate commands for",
63+
describe: "Clone only repos in this group",
64+
type: "string",
7765
})
7866
.options("org", {
79-
demandOption: true,
80-
describe: "Specify GitHub organization",
67+
alias: "o",
68+
default: "capralifecycle",
69+
requiresArg: true,
70+
describe: "GitHub organization",
8171
type: "string",
8272
})
8373
.option("all", {
84-
describe: "Use all groups",
85-
type: "boolean",
86-
})
87-
.option("list-groups", {
88-
alias: "l",
89-
describe: "List available groups",
74+
describe: "Clone all repos",
9075
type: "boolean",
9176
})
9277
.option("include-archived", {
@@ -97,32 +82,32 @@ const command: CommandModule = {
9782
.option("name", {
9883
describe: "Filter to include the specified name",
9984
type: "string",
85+
requiresArg: true,
10086
})
10187
.option("topic", {
10288
alias: "t",
10389
describe: "Filter by specific topic",
10490
type: "string",
91+
requiresArg: true,
10592
})
106-
.option("exclude-existing", {
107-
alias: "x",
108-
describe: "Exclude if existing in working directory",
93+
.option("skip-cloned", {
94+
alias: "s",
95+
describe: "Skip repos already cloned in working directory",
10996
type: "boolean",
11097
}),
11198
handler: async (argv) => {
11299
const config = createConfig()
113100

114101
return generateCloneCommands({
115-
reporter: createReporter(),
116102
config,
117103
github: await createGitHubService({
118104
cache: createCacheProvider(config, argv),
119105
}),
120106
all: !!argv.all,
121-
listGroups: !!argv["list-groups"],
122107
includeArchived: !!argv["include-archived"],
123108
name: argv.name as string | undefined,
124109
topic: argv.topic as string | undefined,
125-
excludeExisting: !!argv["exclude-existing"],
110+
skipCloned: !!argv["skip-cloned"],
126111
group: argv.group as string | undefined,
127112
org: argv.org as string,
128113
})

src/cli/commands/github.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/cli/commands/groups.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { CommandModule } from "yargs"
2+
import { createGitHubService } from "../../github"
3+
import { getGroupedRepos } from "../../github/util"
4+
import { createCacheProvider, createConfig, createReporter } from "../util"
5+
6+
const command: CommandModule = {
7+
command: "groups",
8+
describe: "List available repository groups in a GitHub organization",
9+
builder: (yargs) =>
10+
yargs.options("org", {
11+
alias: "o",
12+
default: "capralifecycle",
13+
requiresArg: true,
14+
describe: "GitHub organization",
15+
type: "string",
16+
}),
17+
handler: async (argv) => {
18+
const config = createConfig()
19+
const reporter = createReporter()
20+
const github = await createGitHubService({
21+
cache: createCacheProvider(config, argv),
22+
})
23+
24+
const repos = await github.getOrgRepoList({ org: argv.org as string })
25+
const groups = getGroupedRepos(repos)
26+
27+
for (const group of groups) {
28+
reporter.log(group.name)
29+
}
30+
},
31+
}
32+
33+
export default command

0 commit comments

Comments
 (0)