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
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,16 @@ clean-all: clean
.PHONY: upgrade-deps
upgrade-deps:
npm run upgrade-deps

# Manual test targets (requires CALS_GITHUB_TOKEN env var)
.PHONY: test-repos
test-repos:
CALS_GITHUB_TOKEN=$(CALS_GITHUB_TOKEN) node lib/cals-cli.mjs repos --org capralifecycle --compact

.PHONY: test-clone
test-clone:
CALS_GITHUB_TOKEN=$(CALS_GITHUB_TOKEN) node lib/cals-cli.mjs clone --org capralifecycle --all | head -5

.PHONY: test-help
test-help:
node lib/cals-cli.mjs --help
75 changes: 42 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,61 @@ It is recommended to use `npx` over global install to ensure you
always run the latest version. If you install it globally remember
to update it before running.

## Build
## Commands

Build and verify:
### Authentication

```sh
$ make # or "make build"
Set your GitHub token (will be stored in the OS keychain):

```bash
cals auth
```

## Contributing
### List repositories

This project uses [semantic release](https://semantic-release.gitbook.io/semantic-release/)
to automate releases and follows
[Git commit guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
from the Angular project.
```bash
cals repos --org capralifecycle
cals repos --org capralifecycle --compact
cals repos --org capralifecycle --csv
```

### List repository groups

```bash
cals groups --org capralifecycle
```

### Generate clone commands

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
Generate clone commands (pipe to bash to execute):

## Goals of CLI
```bash
cals clone --org capralifecycle --all | bash
cals clone --org capralifecycle mygroup | bash
```

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

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

- Automate onboarding of people
- Granting access to various resources: AWS, GitHub, Confluence, JIRA, Slack, ...
- Automate offboarding of people
- Automate generation of new projects/resources
- Creating GitHub repos, giving permissions etc
- Slack channels
- AWS account and structure
- Checklist for manual processes
- AWS infrastructure management, e.g. scripts such as https://github.com/capralifecycle/rvr-aws-infrastructure/blob/master/rvr/create-stack.sh
- `cals aws ...`
```bash
cals sync
cals sync --clone # Prompt to clone missing repos
```

### Snyk management
## Build

https://snyk.docs.apiary.io/reference/projects
Build and verify:

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

## Contributing

This project doesn't currently accept contributions. For inquiries, please contact the maintainers at [Slack](https://liflig.slack.com/archives/C02T4KTPYS2).
This project uses [semantic release](https://semantic-release.gitbook.io/semantic-release/)
to automate releases and follows
[Git commit guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
from the Angular project.

For inquiries, please contact the maintainers at [Slack](https://liflig.slack.com/archives/C02T4KTPYS2).
22 changes: 11 additions & 11 deletions src/cli/commands/github/set-token.ts → src/cli/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { CommandModule } from "yargs"
import { GitHubTokenCliProvider } from "../../../github/token"
import { type Reporter, readInput } from "../../reporter"
import { createReporter } from "../../util"
import { GitHubTokenCliProvider } from "../../github/token"
import { type Reporter, readInput } from "../reporter"
import { createReporter } from "../util"

async function setToken({
async function authenticate({
reporter,
token,
tokenProvider,
Expand All @@ -18,26 +18,26 @@ async function setToken({
"https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook",
)
const inputToken = await readInput({
prompt: "Enter new GitHub API token: ",
prompt: "Enter GitHub token: ",
silent: true,
})
token = inputToken
}

await tokenProvider.setToken(token)
reporter.info("Token saved")
reporter.info("Token saved to keychain")
}

const command: CommandModule = {
command: "set-token",
describe: "Set GitHub token for API calls",
command: "auth [token]",
describe: "Authenticate with GitHub",
builder: (yargs) =>
yargs.positional("token", {
describe:
"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",
describe: "GitHub token (prompted if not provided)",
type: "string",
}),
handler: async (argv) => {
await setToken({
await authenticate({
reporter: createReporter(),
token: argv.token as string | undefined,
tokenProvider: new GitHubTokenCliProvider(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,38 @@ import path from "node:path"
import process from "node:process"
import yargs, { type CommandModule } from "yargs"
import { hideBin } from "yargs/helpers"
import type { Config } from "../../../config"
import { createGitHubService, type GitHubService } from "../../../github"
import { getGroupedRepos, includesTopic } from "../../../github/util"
import type { Reporter } from "../../reporter"
import { createCacheProvider, createConfig, createReporter } from "../../util"
import type { Config } from "../../config"
import { createGitHubService, type GitHubService } from "../../github"
import { getGroupedRepos, includesTopic } from "../../github/util"
import { createCacheProvider, createConfig } from "../util"

async function generateCloneCommands({
reporter,
config,
github,
org,
...opt
}: {
reporter: Reporter
config: Config
github: GitHubService
all: boolean
excludeExisting: boolean
skipCloned: boolean
group: string | undefined
includeArchived: boolean
listGroups: boolean
name: string | undefined
topic: string | undefined
org: string
}) {
if (!opt.listGroups && !opt.all && opt.group === undefined) {
if (!opt.all && opt.group === undefined) {
yargs(hideBin(process.argv)).showHelp()
return
}

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

if (opt.listGroups) {
groups.forEach((it) => {
reporter.log(it.name)
})
return
}

groups.forEach((group) => {
for (const group of groups) {
if (opt.group !== undefined && opt.group !== group.name) {
return
continue
}

group.items
Expand All @@ -54,39 +43,35 @@ async function generateCloneCommands({
.filter((it) => opt.topic === undefined || includesTopic(it, opt.topic))
.filter(
(it) =>
!opt.excludeExisting ||
!fs.existsSync(path.resolve(config.cwd, it.name)),
!opt.skipCloned || !fs.existsSync(path.resolve(config.cwd, it.name)),
)
.forEach((repo) => {
// The output of this is used to pipe into e.g. bash.
// We cannot use reporter.log as it adds additional characters.
process.stdout.write(
`[ ! -e "${repo.name}" ] && git clone ${repo.sshUrl}\n`,
)
})
})
}
}

const command: CommandModule = {
command: "generate-clone-commands",
describe: "Generate shell commands to clone GitHub repos for an organization",
command: "clone [group]",
describe: "Generate git clone commands (pipe to bash to execute)",
builder: (yargs) =>
yargs
.positional("group", {
describe: "Group to generate commands for",
describe: "Clone only repos in this group",
type: "string",
})
.options("org", {
demandOption: true,
describe: "Specify GitHub organization",
alias: "o",
default: "capralifecycle",
requiresArg: true,
describe: "GitHub organization",
type: "string",
})
.option("all", {
describe: "Use all groups",
type: "boolean",
})
.option("list-groups", {
alias: "l",
describe: "List available groups",
describe: "Clone all repos",
type: "boolean",
})
.option("include-archived", {
Expand All @@ -97,32 +82,32 @@ const command: CommandModule = {
.option("name", {
describe: "Filter to include the specified name",
type: "string",
requiresArg: true,
})
.option("topic", {
alias: "t",
describe: "Filter by specific topic",
type: "string",
requiresArg: true,
})
.option("exclude-existing", {
alias: "x",
describe: "Exclude if existing in working directory",
.option("skip-cloned", {
alias: "s",
describe: "Skip repos already cloned in working directory",
type: "boolean",
}),
handler: async (argv) => {
const config = createConfig()

return generateCloneCommands({
reporter: createReporter(),
config,
github: await createGitHubService({
cache: createCacheProvider(config, argv),
}),
all: !!argv.all,
listGroups: !!argv["list-groups"],
includeArchived: !!argv["include-archived"],
name: argv.name as string | undefined,
topic: argv.topic as string | undefined,
excludeExisting: !!argv["exclude-existing"],
skipCloned: !!argv["skip-cloned"],
group: argv.group as string | undefined,
org: argv.org as string,
})
Expand Down
40 changes: 0 additions & 40 deletions src/cli/commands/github.ts

This file was deleted.

33 changes: 33 additions & 0 deletions src/cli/commands/groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { CommandModule } from "yargs"
import { createGitHubService } from "../../github"
import { getGroupedRepos } from "../../github/util"
import { createCacheProvider, createConfig, createReporter } from "../util"

const command: CommandModule = {
command: "groups",
describe: "List available repository groups in a GitHub organization",
builder: (yargs) =>
yargs.options("org", {
alias: "o",
default: "capralifecycle",
requiresArg: true,
describe: "GitHub organization",
type: "string",
}),
handler: async (argv) => {
const config = createConfig()
const reporter = createReporter()
const github = await createGitHubService({
cache: createCacheProvider(config, argv),
})

const repos = await github.getOrgRepoList({ org: argv.org as string })
const groups = getGroupedRepos(repos)

for (const group of groups) {
reporter.log(group.name)
}
},
}

export default command
Loading