diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..dc8e837 --- /dev/null +++ b/.github/workflows/publish.yml @@ -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/checkout@v3.3.0 + - name: Check that the image builds + run: docker build . --file Dockerfile + + test-versions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.3.0 + - 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/checkout@v3.3.0 + 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/generate-release-changelog@2.0.2 + 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 }} diff --git a/README.md b/README.md index 5ea0988..5efb6a3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..a10ad80 --- /dev/null +++ b/action.yml @@ -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' diff --git a/src/index.ts b/src/index.ts index 40d8b2c..61f36da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, org: string, team: string): Promise { + 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);