Skip to content

Commit

Permalink
Created list-team-members GitHub action (#1)
Browse files Browse the repository at this point in the history
Created GitHub action to list all the members of a team.

I created this action with the intention of combine it with some of ours
actions.

Some action don't require to have access to the organization only for
the sake of having the team members, this action will allow us to fine
tune the steps.

I created a basic template in this repository before populating it.
  • Loading branch information
Bullrich authored Apr 3, 2023
1 parent 9cb6dda commit 9a71640
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 5 deletions.
108 changes: 108 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Publish package to GitHub Packages
on:
push:
branches:
- main
pull_request:

env:
IMAGE_NAME: action

jobs:
test-image:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: Check that the image builds
run: docker build . --file Dockerfile

test-versions:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: Extract package.json version
id: package_version
run: echo "VERSION=$(jq '.version' -r package.json)" >> $GITHUB_OUTPUT
- name: Extract action.yml version
uses: mikefarah/yq@master
id: action_image
with:
cmd: yq '.runs.image' 'action.yml'
- name: Parse action.yml version
id: action_version
run: |
echo "IMAGE_VERSION=$(echo $IMAGE_URL | cut -d: -f3)" >> $GITHUB_OUTPUT
env:
IMAGE_URL: ${{ steps.action_image.outputs.result }}
- name: Compare versions
run: |
echo "Verifying that $IMAGE_VERSION from action.yml is the same as $PACKAGE_VERSION from package.json"
[[ $IMAGE_VERSION == $PACKAGE_VERSION ]]
env:
IMAGE_VERSION: ${{ steps.action_version.outputs.IMAGE_VERSION }}
PACKAGE_VERSION: ${{ steps.package_version.outputs.VERSION }}

tag:
if: github.event_name == 'push'
needs: [test-image, test-versions]
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tagcreated: ${{ steps.autotag.outputs.tagcreated }}
tagname: ${{ steps.autotag.outputs.tagname }}
steps:
- uses: actions/[email protected]
with:
fetch-depth: 0
- uses: butlerlogic/action-autotag@stable
id: autotag
with:
head_branch: master
tag_prefix: "v"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Changelog
uses: Bullrich/[email protected]
id: Changelog
env:
REPO: ${{ github.repository }}
- name: Create Release
if: steps.autotag.outputs.tagname != ''
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.autotag.outputs.tagname }}
release_name: Release ${{ steps.autotag.outputs.tagname }}
body: |
${{ steps.Changelog.outputs.changelog }}
publish:
runs-on: ubuntu-latest
permissions:
packages: write
needs: [tag]
if: needs.tag.outputs.tagname != ''
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ ! -z $TAG ]] && VERSION=$(echo $TAG | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
env:
TAG: ${{ needs.tag.outputs.tagname }}
135 changes: 133 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,133 @@
# list-team-members
Lists all the members of a GitHub Organization's team

# List team members
GitHub action to lists all the members of an Organization's team.


[![Publish](https://github.com/paritytech/list-team-members/actions/workflows/publish.yml/badge.svg?branch=master)](https://github.com/paritytech/list-team-members/actions/workflows/publish.yml)

## Why?

This action is intended to have its output used by other action. It provides all the users belonging to a team in an organization.

By being agnostic on the result, users can use the output to generate a custom message on their favorite system.

Needed for some GitHub actions, for example [paritytech/stale-issues-finder](https://github.com/paritytech/stale-issues-finder)

## Example usage

You need to create a file in `.github/workflows` and add the following:

```yml
name: Find team members

on:
workflow_dispatch:

jobs:
get-team:
runs-on: ubuntu-latest
steps:
- name: Fetch team data
# We add the id to access to this step outputs
id: teams
uses: paritytech/list-team-members@main
with:
ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
team: developers
# optional, in case that it searches on a different organization
organization: paritytech
# example showing how to use the content
- name: Show data
run: |
echo "The users are $USERNAMES"
echo "Data: $DATA"
env:
USERNAMES: ${{ steps.teams.outputs.usernames }}"
# a json object
DATA: ${{ steps.teams.outputs.team-data }}"
```
This will produce the following message:
> The users are Username1,Username2,Username3
>
> Data : [{"username" : "Username1","url" : "https : //github.com/Username1","avatar" : "https : //avatars.githubusercontent.com/u/etcasd?v=4"},{"username" : "Username2","url" : "https : //github.com/Username2","avatar" : "https : //avatars.githubusercontent.com/u/fwedfads?v=4"},{"username" : "Username3","url" : "https : //github.com/Username3","avatar" : "https : //avatars.githubusercontent.com/u/sdffsfdsf?v=4"}]
### Inputs
You can find all the inputs in [the action file](./action.yml) but let's walk through each one of them:
- `ACCESS_TOKEN`: Personal Access Token to access the organization teams.
- **required**
- Requires the following scope
- [x] Repo (_Full control of private repositories_)
- If using a GitHub app, read the [Using a GitHub app instead of a PAT](#using-a-github-app-instead-of-a-pat) section
- `organization`: name of the organization/user where the team is. Example: `https://github.com/OWNER-NAME/list-team-members`
- **defaults** to the organization where this action is ran.
- Make sure that the `ACCESS_TOKEN` has access to that organization.
- `team`: Name of the team.
- **required**
- Be sure to get the _team slug_. You can find the teams in https://github.com/orgs/ORG-NAME/teams and copy the name in the URL.
- For example, if the team name is _CI & CD_ but the url is https://github.com/orgs/ORG-NAME/teams/ci-cd, then the _team slug_ is `ci-cd`.

### Outputs
Outputs are needed for your chained actions. If you want to use this information, remember to set an `id` field in the step so you can access it.
You can find all the outputs in [the action file](./action.yml) but let's walk through each one of them:
- `usernames`: all of the usernames combined by a comma.
- Intended to be used by [`usernames.split(",");`](https://www.w3schools.com/jsref/jsref_split.asp)
- `data`: A json array with the curated data of the team members.

#### JSON Data example
```json
[
{
"username": "Username1",
"url": "https : //github.com/Username1",
"avatar": "https : //avatars.githubusercontent.com/u/etcasd?v=4"
},
{
"username": "Username2",
"url": "https : //github.com/Username2",
"avatar": "https : //avatars.githubusercontent.com/u/fwedfads?v=4"
},
{
"username": "Username3",
"url": "https : //github.com/Username3",
"avatar": "https : //avatars.githubusercontent.com/u/sdffsfdsf?v=4"
}
]
```

### Using a GitHub app instead of a PAT
In some cases, specially in big organizations, it is more organized to use a GitHub app to authenticate, as it allows us to give it permissions per repository and we can fine-grain them even better. If you wish to do that, you need to create a GitHub app with the following permissions:
- Organization permissions:
- Members
- [x] Read

Because this project is intended to be used with a token we need to do an extra step to generate one from the GitHub app:
- After you create the app, copy the *App ID* and the *private key* and set them as secrets.
- Then you need to modify the workflow file to have an extra step:
```yml
steps:
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- name: Fetch team members
id: stale
uses: paritytech/list-team-members@main
with:
team: developers
# The previous step generates a token which is used as the input for this action
ACCESS_TOKEN: ${{ steps.generate_token.outputs.token }}
```

## Development
To work on this app, you require
- `Node 18.x`
- `yarn`

Use `yarn install` to set up the project.

`yarn build` compiles the TypeScript code to JavaScript.
25 changes: 25 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: "List Team Members"
description: "Lists all the members of an Organization's team"
author: paritytech
branding:
icon: users
color: organge
inputs:
ACCESS_TOKEN:
required: true
description: The token to access the repo
organization:
required: false
description: The repository to fetch the issues from
team:
required: false
description: The name of the org/user that owns the repository
outputs:
usernames:
description: 'All of the usernames combined by a comma'
data:
description: 'A JSON object with the users data'

runs:
using: 'docker'
image: 'docker://ghcr.io/paritytech/list-team-members/action:0.0.1'
48 changes: 45 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
function greet(name: string) {
console.log(`Hello ${name}!`);
import { getInput, info, setFailed, setOutput } from "@actions/core";
import { context, getOctokit } from "@actions/github";
import { Context } from "@actions/github/lib/context";
import { GitHub } from "@actions/github/lib/utils";

type UserData = {
username: string;
url: string;
avatar: string;
}

async function fetchTeam(octokit: InstanceType<typeof GitHub>, org: string, team: string): Promise<UserData[]> {
const teamData = await octokit.rest.teams.listMembersInOrg({
org,
team_slug: team,
});

return teamData.data.map(user => {
return {
username: user.login,
url: user.html_url,
avatar: user.avatar_url
}
});
}

async function runAction(ctx: Context) {
const token = getInput("ACCESS_TOKEN", { required: true });
let organization = getInput("organization", { required: false });
if (!organization) {
organization = ctx.repo.owner;
}

const team = getInput("team", { required: true });

const octokit = getOctokit(token);
const teamData = await fetchTeam(octokit, organization, team);
if (teamData.length > 0) {
info(`Obtained data from ${teamData.length} users`);
setOutput("usernames", teamData.map(({ username }) => username).join(","));
setOutput("team-data", JSON.stringify(teamData));
} else {
setFailed(`No users were found when searching for the team ${team}`);
}
}

greet("Parity");
runAction(context);

0 comments on commit 9a71640

Please sign in to comment.