Skip to content

Commit

Permalink
🤘 Add support for CSV output
Browse files Browse the repository at this point in the history
  • Loading branch information
droidpl committed Jun 14, 2021
1 parent 776f46f commit bfb3298
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 50 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Enterprise members report action

Action to generate a report as markdown, html or json with the members, outside collaborators and pending invites.
Action to generate a report as markdown, html, csv or json with the members, outside collaborators and pending invites.

## Description

Expand All @@ -13,11 +13,11 @@ This action generates a report for an enterprise with the following elements:

This actions has the following inputs:

| Parameter | Description | Default | Is Required |
|------------|------------------------------------------------------------------------------------------------|---------|-------------|
| token | A personal access token with permissions on all the orgs of the enterprise | None ||
| enterprise | The enterprise where we want to generate the report | None ||
| format | Determines how the output parameter will be formatted. Supports: `json`, `markdown` and `html` | None ||
| Parameter | Description | Default | Is Required |
|------------|-------------------------------------------------------------------------------------------------------|---------|-------------|
| token | A personal access token with permissions on all the orgs of the enterprise | None ||
| enterprise | The enterprise where we want to generate the report | None ||
| format | Determines how the output parameter will be formatted. Supports: `json`, `markdown`, `csv` and `html` | None ||

This action has the following outputs:

Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ inputs:
description: 'The enterprise where we want to generate the report'
format:
required: true
description: 'Determines how the output parameter will be formatted. Supports: json, markdown and html'
description: 'Determines how the output parameter will be formatted. Supports: json, markdown, csv and html'
outputs:
data:
description: 'The data extracted from the license API calls in the format specified. The type of the output is always string'
Expand Down
9 changes: 7 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
"email": "services@github.com",
"url": "https://services.github.com"
},
"contributors": [{
"name": "Javier de Pedro López",
"email": "droidpl@github.com",
"url": "https://github.com/droidpl"
}],
"contributors": [
{
"name": "Javier de Pedro López",
"email": "droidpl@github.com",
"url": "https://github.com/droidpl"
}
],
"license": "MIT",
"repository": {
"type": "git",
Expand Down Expand Up @@ -48,6 +50,7 @@
"@actions/core": "^1.2.6",
"@actions/github": "^5.0.0",
"@octokit/rest": "^18.5.6",
"csv-string": "^4.0.1",
"marked": "^2.0.7"
},
"devDependencies": {
Expand Down
34 changes: 6 additions & 28 deletions src/api/github-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ export async function getPendingInvitesFromOrgs(orgs: string[], octokit: Octokit
org,
login: invite.login,
email: invite.email,
created_at: invite.created_at
createdAt: invite.created_at
})
}
}
// Sort them by created_at
return pendingInvites.sort((a, b) => a.created_at.localeCompare(b.created_at))
return pendingInvites.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
}

export async function getMembersFromOrgs(orgs: string[], octokit: Octokit): Promise<OrgMember[]> {
Expand All @@ -73,6 +73,7 @@ export async function getMembersFromOrgs(orgs: string[], octokit: Octokit): Prom
}
nodes {
login
createdAt
emails: organizationVerifiedDomainEmails(login: $org)
}
}
Expand All @@ -99,7 +100,7 @@ export async function getMembersFromOrgs(orgs: string[], octokit: Octokit): Prom
existingMember.orgs.push(org)
} else {
// Create a new item
members.set(member.login, {...member, orgs: [org], type: Membership.MEMBER})
members.set(member.login, {...member, orgs: [org], createdAt: member.createdAt, type: Membership.MEMBER})
}
}
}
Expand Down Expand Up @@ -131,6 +132,7 @@ export async function getOutsideCollaborators(enterprise: string, octokit: Octok
node {
login
email
createdAt
}
}
}
Expand All @@ -153,6 +155,7 @@ export async function getOutsideCollaborators(enterprise: string, octokit: Octok
orgs,
login: item.node.login,
emails: [item.node.email],
createdAt: item.node.createdAt,
type: Membership.OUTSISE_COLLABORATOR
}
})
Expand All @@ -161,28 +164,3 @@ export async function getOutsideCollaborators(enterprise: string, octokit: Octok

return collaborators
}

export async function getOutsideCollaborator(orgs: string[], octokit: Octokit): Promise<OrgMember[]> {
const collaborators: Map<string, OrgMember> = new Map()
for (const org of orgs) {
const data = await octokit.paginate(octokit.rest.orgs.listOutsideCollaborators, {
org
})
for (const collaborator of data) {
if (collaborator !== null) {
const existingCollaborator = collaborators.get(collaborator.login)
if (existingCollaborator) {
existingCollaborator.orgs.push(org)
} else {
collaborators.set(collaborator.login, {
login: collaborator.login,
emails: [],
orgs: [org],
type: Membership.OUTSISE_COLLABORATOR
})
}
}
}
}
return Array.from(collaborators.values())
}
51 changes: 47 additions & 4 deletions src/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {Octokit} from '@octokit/rest'
import marked from 'marked'
import * as CSV from 'csv-string'
import {
getMembersFromOrgs,
getOrgsForEnterprise,
getOutsideCollaborators,
getPendingInvitesFromOrgs
} from './api/github-api'
import {getMarkdownTable} from './markdown/markdown-table'
import {ActionParams, OutputFormat, OrgMember, PendingInvite} from './types'
import {ActionParams, OutputFormat, OrgMember, PendingInvite, Membership} from './types'

export async function generateReport(params: ActionParams): Promise<string> {
const octokit = new Octokit({
Expand All @@ -26,6 +27,8 @@ export async function generateReport(params: ActionParams): Promise<string> {
return getHtmlFormat(members, outsideCollaborators, pendingInvites)
case OutputFormat.JSON:
return getJSONFormat(members, outsideCollaborators, pendingInvites)
case OutputFormat.CSV:
return getCSVFormat(members, outsideCollaborators, pendingInvites)
}
}

Expand All @@ -38,16 +41,26 @@ function getMarkdownFormat(
const allMembers = members.concat(outsideCollaborators)
const membersContent = getMarkdownTable({
table: {
head: ['Login', 'Emails', 'Orgs', 'Membership'],
body: [...allMembers.map(item => [item.login, item.emails.join(','), item.orgs.join(','), item.type.toString()])]
head: ['Login', 'Emails', 'Orgs', 'Membership', 'Created At'],
body: [
...allMembers.map(item => [
item.login,
item.emails.join(','),
item.orgs.join(','),
item.type.toString(),
item.createdAt
])
]
}
})

// Generate the pending invites table
const pendingInvitesContent = getMarkdownTable({
table: {
head: ['Login', 'Email', 'Org', 'Created At'],
body: [...pendingInvites.map(item => [item.login || 'Not registered', item.email, item.org, item.created_at])]
body: [
...pendingInvites.map(item => [item.login || 'No account', item.email || 'No email', item.org, item.createdAt])
]
}
})
return `
Expand Down Expand Up @@ -81,3 +94,33 @@ function getJSONFormat(
pendingInvites
})
}

function getCSVFormat(
members: OrgMember[],
outsideCollaborators: OrgMember[],
pendingInvites: PendingInvite[]
): string {
// Map pending invites into org members as CSVs can only have one file
const pendingInviteMembers: OrgMember[] = pendingInvites.map(item => {
return {
login: item.login || 'No account',
orgs: [item.org],
emails: item.email ? [item.email] : [],
createdAt: item.createdAt,
type: Membership.PENDING_INVITE
}
})
const allMembers = members.concat(outsideCollaborators).concat(pendingInviteMembers)
const membersContent = [
['Login', 'Emails', 'Orgs', 'Membership', 'Created At'],
...allMembers.map(item => [
item.login,
item.emails.join(','),
item.orgs.join(','),
item.type.toString(),
item.createdAt
])
]

return CSV.stringify(membersContent)
}
13 changes: 9 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
export enum OutputFormat {
HTML,
MARKDOWN,
JSON
JSON,
CSV
}

export type FormatParams = {
Expand All @@ -23,21 +24,23 @@ export type OrgLogin = {
export type PendingInvite = {
org: string
login?: string
email: string
created_at: string
email?: string
createdAt: string
}

// eslint-disable-next-line no-shadow
export enum Membership {
MEMBER = 'member',
OUTSISE_COLLABORATOR = 'outside collaborator'
OUTSISE_COLLABORATOR = 'outside collaborator',
PENDING_INVITE = 'pending invite'
}

export type OrgMember = {
orgs: string[]
login: string
emails: string[]
type: Membership
createdAt: string
}

type PageInfo = {
Expand All @@ -62,6 +65,7 @@ export type GetMembersResponse = {
{
login: string
emails: string[]
createdAt: string
}
]
}
Expand All @@ -80,6 +84,7 @@ export type OutsideCollaborator = {
node: {
login: string
email: string
createdAt: string
}
}
export type GetOutsideCollaborators = {
Expand Down

0 comments on commit bfb3298

Please sign in to comment.