You are on page 1of 114

version package.

json master

push master
action.yml

name description
runs name description
runs
docker node.js

docker

action.yml name
description runs.using runs.image

action-test
github-action-explore-debug

git clone
YOUR_USERNAME

$ git clone git@github.com:YOUR_USERNAME/github-action-explore-debug.git


Cloning into 'github-action-explore-debug'...
warning: You appear to have cloned an empty repository.

github-action-explore-debug

action.yml

name: Explore Debug


description: Show available data for any Action run
runs:
using: docker
image: Dockerfile

runs
Dockerfile docker
image Dockerfile

Dockerfile docker

alpine
jq
action.yml Dockerfile

FROM alpine

RUN apk add --no-cache jq

COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

alpine jq apk

COPY ENTRYPOINT
docker
entrypoint.sh
entrypoint.sh

action.yml
Dockerfile
entrypoint.sh

entrypoint.sh

#!/bin/sh

echo "Debug information:"

chmod +x entrypoint.sh

env
GITHUB_ INPUT_ GITHUB_

INPUT_
cut
entrypoint.sh
env

IFS=$'\n'
for v in `env`; do
PREFIX=$(echo $v| cut -d '_' -f1)

if [[ $PREFIX == 'GITHUB' ]] || [[ $PREFIX == 'INPUT' ]]; then


echo "$v";
fi
done

./entrypoint.sh

GITHUB_ INPUT_

GITHUB_DEMO=example ./entrypoint.sh

$ GITHUB_DEMO=example ./entrypoint.sh
Debug information:
GITHUB_DEMO=example

jq
Dockerfile

entrypoint.sh

echo "---"
echo "Event:"

jq . $GITHUB_EVENT_PATH
. jq

input

repository sender
.pull_request
jq

jq

INPUT_
jqSelector INPUT_JQSELECTOR
jq_selector INPUT_JQ_SELECTOR
TitleCase camelCase snake_case
snake_case

entrypoint.sh
jq jq

echo "---"
echo "Event:"

jq $INPUT_JQ_SELECTOR $GITHUB_EVENT_PATH

jq

#!/bin/sh

echo "Debug information:"

IFS=$'\n'
for v in `env`; do
PREFIX=$(echo $v| cut -d '_' -f1)

if [[ $PREFIX == 'GITHUB' ]] || [[ $PREFIX == 'INPUT' ]]; then


echo "$v";
fi
done

echo "---"
echo "Event:"

jq $INPUT_JQ_SELECTOR $GITHUB_EVENT_PATH

$INPUT_JQ_SELECTOR
jq
action.yml inputs

inputs:
jq_selector:
description: 'The JQ selector to execute'
required: false
default: "."

jq_selector
INPUT_JQ_SELECTOR .

input action.yml

name: Explore Debug


description: Show available data for any Action run
runs:
using: docker
image: Dockerfile
inputs:
jq_selector:
description: 'The JQ selector to execute'
required: false
default: "."

git add action.yml Dockerfile entrypoint.sh


git commit -m "Initial Commit"
git push origin master

docker
GITHUB_ INPUT_

jq
- name: Output debug
runs: |
echo "Debug information:"

IFS=$'\n'
for v in `env`; do
PREFIX=$(echo $v| cut -d '_' -f1)

if [[ $PREFIX == 'GITHUB' ]] || [[ $PREFIX == 'INPUT' ]]; then


echo "$v";
fi
done

echo "---"
echo "Event:"

jq .pull_request $GITHUB_EVENT_PATH

- uses: YOUR_NAME/github-action-explore-debug@master
action-test

action-test

$ git clone git@github.com:YOUR_USERNAME/action-test.git


Cloning into 'action-test'...
warning: You appear to have cloned an empty repository.

action-test

.github/workflows

bananas.yml

<trigger>-<action>
pull_request-check-redirects
.github/workflows

pull_request
.github/workflows/pull_request-debug.yml
YOUR_USERNAME
name: Debug
on:
pull_request:
types: [opened, synchronize, closed]
jobs:
output-debug:
runs-on: ubuntu-18.04
steps:
- uses: YOUR_USERNAME/github-action-explore-debug@master
with:
jq_selector: ".pull_request"

pull_request

git add .github


git commit -m "Adding Debug Workflow"
git push origin master
on

on: push
on: pull_request
opened synchronize labeled locked
closed

action-test

on:
pull_request:
types: [opened, synchronize, closed]

pull_request opened
synchronize closed pull_request
review_requested reopened

master on

on:
push:
branches:
- master

v1.0 v1.1 v1.8

on:
push:
tags:
- 'v1.*'

on:
push:
paths:
- 'test/*'
branches-ignore tags-ignore

on:
push:
branches-ignore:
- 'spike-*'
tags-ignore:
- ‘beta-*’

spike-
beta-

branches tags

branches

on:
push:
branches:
- 'release/*'
- '!release/*-beta'

release
-beta

branches-ignore
branches-ignore

release test

output-debug
ubuntu-18.04 runs-on

ubuntu-16.04 runs-on
ubuntu-20.04
ubuntu-latest ubuntu-18.04 act
ubuntu-20.04

windows-latest
windows-2019
macOS-latest macOS-10.15

<os>-latest

<os>-latest
steps

steps

continue-on-error

uses

run

name

with

env

uses run

uses: YOUR_USERNAME/github-action-explore-debug@master
action.yml

steps:
- uses: YOUR_USERNAME/github-action-explore-debug@master

run

steps:
- run: |
echo "Hello World"
[[ -z "$MY_VAR" ]] && echo "MY_VAR is empty"

uses run

name

uses run
name uses run
with env

inputs
env with

env

AWS_ACCESS_KEY_ID AZURE_SUBSCRIPTION_ID 

with INPUT_

env

env with

README.md

README.md

README.md

echo "One" > README.md


git add README.md
git commit -m "Initial README"
git push origin master

README.md

git checkout -b changes


echo "Two" > README.md
git add README.md
git commit -m "Update README"
git push origin changes
git checkout master

[changes 911bc2a] Update README


1 file changed, 1 insertion(+), 1 deletion(-)
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 284 bytes | 284.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: Create a pull request for 'changes' on GitHub by visiting:
remote: https://github.com/YOUR_USERNAME/action-test/pull/new/changes
remote:

YOUR_USERNAME

https://github.com/YOUR_USERNAME/action-test/pull/new/changes

Create Pull Request


queued
in progress successful
Show all checks Details

Run actions-book/github-action-explore-debug@master
GITHUB_ INPUT_
pull_request

issue_comment
jq_selector: "."

.pull_request.additions
nektos/act
act
act
act --version
0.2.10,

act .github/workflows
act
act push

action-test act pull_request

act act
runs-on: ubuntu-18.04

act
act -v act

act

| Debug information:
| GITHUB_TOKEN=
| GITHUB_EVENT_PATH=/github/workflow/event.json
| GITHUB_WORKFLOW=Debug
| GITHUB_RUN_ID=1
| GITHUB_RUN_NUMBER=1
| GITHUB_ACTION=0
| GITHUB_REPOSITORY=actions-book/action-test
| INPUT_JQ_SELECTOR=.pull_request
| GITHUB_ACTIONS=true
| GITHUB_WORKSPACE=/github/workspace
| GITHUB_SHA=8a55fc0886ab90ca9b3735b89bc57c63bce5817c
| GITHUB_REF=refs/heads/master
| GITHUB_ACTOR=nektos/act
| GITHUB_EVENT_NAME=pull_request
| ---
| Event:
| null

Event
act -e
event.json

{
"pull_request": {
"example": true
}
}

act act -e ./event.json pull_request

-e

| Debug information:
| GITHUB_TOKEN=
| GITHUB_EVENT_PATH=/github/workflow/event.json
| GITHUB_WORKFLOW=Debug
| GITHUB_RUN_ID=1
| GITHUB_ACTION=0
| GITHUB_RUN_NUMBER=1
| GITHUB_REPOSITORY=actions-book/action-test
| INPUT_JQ_SELECTOR=.pull_request
| GITHUB_ACTIONS=true
| GITHUB_WORKSPACE=/github/workspace
| GITHUB_SHA=8a55fc0886ab90ca9b3735b89bc57c63bce5817c
| GITHUB_REF=refs/heads/master
| GITHUB_ACTOR=nektos/act
| GITHUB_EVENT_NAME=pull_request
| ---
| Event:
| {
| "example": true
| }

act pull_request event.json


-e

event.json act

pull_request
sender pull_request

mergeable requested_reviewers
pull_request-opened.json
act

act -e ./pull_request-opened.json pull_request

| Debug information:
| GITHUB_TOKEN=
| GITHUB_EVENT_PATH=/github/workflow/event.json
| GITHUB_WORKFLOW=Debug
| GITHUB_RUN_ID=1
| GITHUB_RUN_NUMBER=1
| GITHUB_ACTION=0
| GITHUB_REPOSITORY=actions-book/action-test
| INPUT_JQ_SELECTOR=.pull_request
| GITHUB_ACTIONS=true
| GITHUB_WORKSPACE=/github/workspace
| GITHUB_SHA=8a55fc0886ab90ca9b3735b89bc57c63bce5817c
| GITHUB_REF=refs/heads/master
| GITHUB_ACTOR=nektos/act
| GITHUB_EVENT_NAME=pull_request
| ---
| Event:
| {
| "action": "opened",
| "number": 11,
| "pull_request": {
| ...
| "html_url": "https://github.com/mheap/action-test/pull/11",
| "id": 413144686,
| "issue_url": "https://api.github.com/repos/mheap/action-test/issues/11",
| "labels": [],
| "locked": false,
| "maintainer_can_modify": true,
| "merge_commit_sha": null,
| ...
| }
| }

secrets
.github/workflows/pull_request-debug.yml
${{ secrets.SECRET_NAME }}

- uses: mheap/github-action-explore-debug@master
with:
jq_selector: ".pull_request"
example: ${{ secrets.EXAMPLE }}

EXAMPLE
INPUT_EXAMPLE

act -e event.json pull_request

| Debug information:
| INPUT_EXAMPLE=

act -s

act -e event.json pull_request -s EXAMPLE


enter
INPUT_EXAMPLE

| Debug information:
| INPUT_EXAMPLE=***

act
act
act -e event.json pull_request -s EXAMPLE=Hello

GITHUB_TOKEN GITHUB_TOKEN

GITHUB_TOKEN
GITHUB_TOKEN

GITHUB_TOKEN
v1

- uses: mheap/github-action-explore-debug@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
jq_selector: ".pull_request"

github-action-explore-debug
act

[Debug/output-debug] Run actions-book/github-action-explore-debug@master


[Debug/output-debug] git clone 'https://github.com/actions-book/github-action-explore-
debug' # ref=master
[Debug/output-debug] docker build -t act-actions-book-github-action-explore-debug-
master:latest /Users/michael/.cache/act/actions-book-github-action-explore-debug@master

act
act
act

github-action-explore-debug action-test .github

.
README.md
event.json
github-action-explore-debug
   Dockerfile
   action.yml
   entrypoint.sh
pull_request-opened.json

action-test
pull_request-debug.yml

- uses: mheap/github-action-explore-debug@master

- uses: ./github-action-explore-debug

act -e event.json pull_request


git clone
github-action-explore-debug/entrypoint.sh
act

act

pull_request-debug.yml
act
github-action-explore-debug

- uses: ./

github-action-explore-debug

act -W
action-test

action-test
github-action-explore-debug

act -e ../action-test/event.json pull_request -W ../action-test/.github/workflows


act
../action-test/.github/workflows ./

-W

act

GITHUB_TOKEN

act
docker

Dockerfile
action.yml entrypoint.sh
github-action-pull-request-milestone-bash
action-test

action.yml

docker
name: Pull Request Milestone
description: Congratulate people when they hit a certain number of merged pull requests
runs:
using: docker
image: Dockerfile

docker
Dockerfile alpine
jq event.json curl
bash

FROM alpine

RUN apk add --no-cache jq curl bash

COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh

#!/bin/bash

echo "This is my action, and it's working!"

chmod +x entrypoint.sh

github-action-pull-request-milestone-bash
YOUR_USERNAME

git init
git add .
git commit -m "Initial Commit"
git remote add origin git@github.com:YOUR_USERNAME/github-action-pull-request-milestone-
bash.git
git push -u origin master

action-test
action-test/.github/workflows/pull_request-merged.yml
uses
name: Pull Request Milestone
on:
pull_request:
types: [closed]
jobs:
milestone:
runs-on: ubuntu-18.04
steps:
- uses: YOUR_USERNAME/github-action-pull-request-milestone-bash@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GITHUB_TOKEN

act
act -j
milestone

action-test
act -j milestone pull_request

[Pull Request Milestone/milestone] Start image=node:12.6-buster-slim


[Pull Request Milestone/milestone] docker run image=node:12.6-buster-slim entrypoint=
["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[Pull Request Milestone/milestone] Run actions-book/github-action-pull-request-milestone-
bash@master
[Pull Request Milestone/milestone] git clone 'https://github.com/actions-book/github-
action-pull-request-milestone-bash' # ref=master
[Pull Request Milestone/milestone] docker build -t act-actions-book-github-action-pull-
request-milestone-bash-master:latest /Users/michael/.cache/act/actions-book-github-action-
pull-request-milestone-bash@master
[Pull Request Milestone/milestone] docker run image=act-actions-book-github-action-
pull-request-milestone-bash-master:latest entrypoint=[] cmd=[]
| This is my action, and it's working!
[Pull Request Milestone/milestone] Success - actions-book/github-action-pull-request-
milestone-bash@master@master

action.yml
pull_request

GITHUB_EVENT_NAME
jq
.action closed
entrypoint.sh
#!/bin/bash

if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then


echo "This action only runs on pull_request.closed"
echo "Found: $GITHUB_EVENT_NAME"
exit 1
fi

ACTION=$(jq -r ".action" $GITHUB_EVENT_PATH)


if [[ "$ACTION" != "closed" ]]; then
echo "This action only runs on pull_request.closed"
echo "Found: $GITHUB_EVENT_NAME.$ACTION"
exit 1
fi

0
1
1

merged
closed pull_request.merged

entrypoint.sh

IS_MERGED=$(jq -r ".pull_request.merged" $GITHUB_EVENT_PATH)


if [[ "$IS_MERGED" != "true" ]]; then
echo "Pull request closed without merge"
exit 0
fi

entrypoint.sh

git add entrypoint.sh


git commit -m "Add guard rails"
git push origin master

event.json
act

pull_request-opened.json

action-test pull_request-closed.json
YOUR_USERNAME

{
"action": "closed",
"pull_request": {
"number": 1,
"merged": true,
"user": {
"login": "YOUR_USERNAME"
}
}
}

act
act -j milestone -e pull_request-closed.json pull_request action-test
act
true false closed opened
pull_request-closed.json act

| This action only runs on pull_request.closed


| Found: pull_request.opened

GITHUB_TOKEN

act
repo.public_repo
Authorization token

curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com

merged
closed

GET /repos/:owner/:repo/pulls
state

GITHUB_REPOSITORY
YOUR_USERNAME/action-test
:owner :repo GET

curl

curl -Ss -H "Authorization: token $GITHUB_TOKEN"


"https://api.github.com/repos/$GITHUB_REPOSITORY/pulls?state=closed"

-s curl
-S
-s

per_page
&per_page=100 curl

Link rel="next"
while sed
rel="next:

entrypoint.sh
$PULLS
PULLS=""
URL="https://api.github.com/repos/$GITHUB_REPOSITORY/pulls?state=closed&per_page=100"
while [ "$URL" ]; do
RESP=$(curl -i -Ss -H "Authorization: token $GITHUB_TOKEN" "$URL")
HEADERS=$(echo "$RESP" | sed '/^\r$/q')
URL=$(echo "$HEADERS" | sed -n -E 's/Link:.*<(.*?)>; rel="next".*/\1/p')
PULLS="$PULLS $(echo "$RESP" | sed '1,/^\r$/d')"
done

event.json event.json
merged true
merged_at null

merged

.pull_request.user.login event.json
entrypoint.sh PR_AUTHOR

PR_AUTHOR=$(jq -r ".pull_request.user.login" $GITHUB_EVENT_PATH)

jq merged_at
null PR_AUTHOR
wc -l
wc
tr
wc -l

MERGED_COUNT=$(echo $PULLS | jq -c ".[] | select(.merged_at != null and .user.login ==


\"$PR_AUTHOR\")" | wc -l | tr -d '[:space:]')

jq -c compressed jq

$PR_AUTHOR

with
entrypoint.sh

INPUT_MERGED_1="Great job, your first PR"


INPUT_MERGED_13="Unlucky for some? Not for us! That's 13 PRs merged, keep it up!"
INPUT_MERGED_100="100 merged PRs? You're the best!"

$MERGED_COUNT

INPUT_MERGED_
entrypoint.sh $COMMENT

COMMENT_VAR="INPUT_MERGED_${MERGED_COUNT}"
COMMENT=${!COMMENT_VAR}

if [[ -z "$COMMENT" ]]; then


echo "No action required"
exit 0
fi

POST /repos/:owner/:repo/issues/:issue_number/comments

:owner :repo $GITHUB_REPOSITORY


:issue_number event.json
entrypoint.sh

ISSUE_NUMBER=$(jq -r ".pull_request.number" $GITHUB_EVENT_PATH)

body
jq
jq

entrypoint.sh
POSTBODY=$(echo $COMMENT | jq -c -R '. | {"body": .}')

-d
$COMMENT_ADDED

COMMENT_ADDED=$(curl -i -Ss -H "Authorization: token $GITHUB_TOKEN"


"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" -d
"$POSTBODY")

201 Created

201 Created grep 1


$?
exit 1

# Check if the comment was successfully added


echo $COMMENT_ADDED | head -n1 | grep "201 Created" > /dev/null
if [[ $? -eq 1 ]]; then
echo "Error creating comment:"
echo "$COMMENT_ADDED" | sed '1,/^\r$/d'
exit 1
fi

echo "Added comment:"


echo $COMMENT

POST

POST /repos/:owner/:repo/issues/:issue_number/labels
:owner :repo :issue_number
POST

merge-milestone
merge-milestone:$MERGED_COUNT

{
"labels": ["merge-milestone", "merge-milestone:$MERGED_COUNT"]
}

200 OK
201 Created

# Add labels
LABELS='{"labels":["merge-milestone","merge-milestone:'$MERGED_COUNT'"]}'
LABELS_ADDED=$(curl -i -Ss -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/labels" -d $LABELS)

# Check if the labels were successfully added


echo $LABELS_ADDED | head -n1 | grep "200 OK" > /dev/null
if [[ $? -eq 1 ]]; then
echo "Error Adding Labels:"
echo "$LABELS_ADDED" | sed '1,/^\r$/d'
exit 1
fi

is:closed label:merge-milestone

INPUT_MERGED_
pull_request-merged.yml
action-test
with
- uses: YOUR_USERNAME/github-action-pull-request-milestone-bash@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
merged_1: "Your first PR! We're glad to have you on board"
merged_2: "Two in a row? Thanks for coming back to us"
merged_5: "5? FIVE!? Do you work here or something? Thanks for all the contributions"
merged_13: "Unlucky for some? Not for us! That's 13 PRs merged, keep it up!"
merged_50: "That's half a century for you. Do the same again and let's see what happens!"
merged_100: "100 merged PRs? You're the best. Raise an issue and we'll send you some
swag"

INPUT_MERGED_50

INPUT_MERGED_ entrypoint.sh

event.json

pull_request-merged.yml
GITHUB_TOKEN PAT
PAT

PAT
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.PAT }}

pull_request-merged.yml action-test
entrypoint.sh
github-action-pull-request-milestone-bash master

#!/bin/bash

if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then


echo "This action only runs on pull_request.closed"
echo "Found: $GITHUB_EVENT_NAME"
exit 1
fi

ACTION=$(jq -r ".action" $GITHUB_EVENT_PATH)


if [[ "$ACTION" != "closed" ]]; then
echo "This action only runs on pull_request.closed"
echo "Found: $GITHUB_EVENT_NAME.$ACTION"
exit 1
fi

IS_MERGED=$(jq -r ".pull_request.merged" $GITHUB_EVENT_PATH)


if [[ "$IS_MERGED" != "true" ]]; then
echo "Pull request closed without merge"
exit 0
fi

PULLS=""
URL="https://api.github.com/repos/$GITHUB_REPOSITORY/pulls?state=closed&per_page=100"
while [ "$URL" ]; do
RESP=$(curl -i -Ss -H "Authorization: token $GITHUB_TOKEN" "$URL")
HEADERS=$(echo "$RESP" | sed '/^\r$/q')
URL=$(echo "$HEADERS" | sed -n -E 's/Link:.*<(.*?)>; rel="next".*/\1/p')
PULLS="$PULLS $(echo "$RESP" | sed '1,/^\r$/d')"
done

PR_AUTHOR=$(jq -r ".pull_request.user.login" $GITHUB_EVENT_PATH)

MERGED_COUNT=$(echo $PULLS | jq -c ".[] | select(.merged_at != null and .user.login ==


\"$PR_AUTHOR\")" | wc -l | tr -d '[:space:]')

COMMENT_VAR="INPUT_MERGED_${MERGED_COUNT}"
COMMENT=${!COMMENT_VAR}

if [[ -z "$COMMENT" ]]; then


echo "No action required"
exit 0
fi

ISSUE_NUMBER=$(jq -r ".pull_request.number" $GITHUB_EVENT_PATH)

POSTBODY=$(echo $COMMENT | jq -c -R '. | {"body": .}')

COMMENT_ADDED=$(curl -i -Ss -H "Authorization: token $GITHUB_TOKEN"


"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" -d
"$POSTBODY")

# Check if the comment was successfully added


echo $COMMENT_ADDED | head -n1 | grep "201 Created" > /dev/null
if [[ $? -eq 1 ]]; then
echo "Error creating comment:"
echo "$COMMENT_ADDED" | sed '1,/^\r$/d'
exit 1
fi

echo "Added comment:"


echo $COMMENT

# Add labels
LABELS='{"labels":["merge-milestone","merge-milestone:'$MERGED_COUNT'"]}'
LABELS_ADDED=$(curl -i -Ss -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/labels" -d $LABELS)

# Check if the labels were successfully added


echo $LABELS_ADDED | head -n1 | grep "200 OK" > /dev/null
if [[ $? -eq 1 ]]; then
echo "Error Adding Labels:"
echo "$LABELS_ADDED" | sed '1,/^\r$/d'
exit 1
fi
master
github-action-pull-request-milestone-bash action-test
action-test

Node
docker
node12 docker

checkout
cache setup-* setup-java

@actions

@actions/core
$PATH

// This will read INPUT_INPUTNAME from the environment, and error if it's missing
core.getInput('inputName', { required: true });

// This will set an output that can be used in future workflow steps
// Equivalent to echo "::set-output name=outputKey::outputVal" in Bash
core.setOutput('outputKey', 'outputVal');

// Sets the exit code of the action to 1 (Failure) but does *NOT* stop Action execution
core.setFailed(`Action failed with error ${err}`);

@actions/core

@actions/github

const octokit = new github.GitHub(authToken);

// Fetch the first page of closed pull requests


const pulls = await octokit.pulls.list({
owner: 'octokit',
repo: 'rest.js',
state: 'closed',
per_page: 100
});

context owner
repo

const context = github.context;

// Instead of hardcoding the owner and repo


const newIssue = await octokit.issues.createComment({
owner: 'octokit',
repo: 'rest.js',
issue_number: 123,
body: "Congrats! This is your first pull request"
});

// You can read these values from context.repo which reads from
// process.env.GITHUB_REPOSITORY
const newIssue = await octokit.issues.createComment({
...context.repo,
issue_number: 123,
body: "Congrats! This is your first pull request"
});

// In fact, it can even automatically populate the issue number by detecting if you're
// in an Issue or a Pull Request and reading the correct key in the event payload
const newIssue = await octokit.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: "Congrats! This is your first pull request"
});

@actions/github

@actions/github

@actions/exec
@actions/exec

// Example of capturing the output on stdout and stderr to use later


const options = {};
options.listeners = {
stdout: (data: Buffer) => {
myOutput += data.toString();
},
stderr: (data: Buffer) => {
myError += data.toString();
}
};
options.cwd = './lib';

await exec.exec('node', ['index.js', 'foo=bar'], options);

@actions/artifact

// Upload
const files = [
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
];
const rootDirectory = '/home/user/files/plz-upload';

const uploadResult = await artifactClient.uploadArtifact("demo-artifact", files,


rootDirectory);
// And in another action
const downloadResponse = await artifactClient.downloadArtifact("demo-artifact", path);

@actions/artifact

@actions/cache

const cache = require('@actions/cache');


const paths = ['prime-numbers'];
const key = `primes-${currentDate}`;
const restoreKeys = ['primes-'];

// Check the cache


const cacheKey = await cache.restoreCache(paths, key, restoreKeys);

if (!cacheKey) {
// Really expensive calculation
await writePrimesToFile('prime-numbers');
}

// Cache the data for next time


const cacheId = await cache.saveCache(paths, key);

// Then we use @actions/core to set an output


core.setOutput('cache-primes', cacheId);

// Read the prime-numbers file and do any work we need

- name: Cache Primes


id: cache-primes
uses: actions/cache@v1
with:
path: prime-numbers
key: primes-$currentDate
restore-keys:
- "primes-"

@actions/glob

const globber = await glob.create('**/*.zip')


const files = await globber.glob()
@actions/io

await io.mkdirP('my/backup');
await io.cp('app/important/folder', 'my/backup', { recursive: true });
await io.rmRF('app');

@actions/tool-cache

@actions/tool-cache
RUNNER_TOOL_CACHE

setup-* @actions/tool-cache

@actions

github-action-pull-request-milestone-node
action-test

action.yml
docker

name: Pull Request Milestone


description: Congratulate people when they hit a certain number of merged pull requests
runs:
using: docker
image: Dockerfile

Dockerfile alpine
node:alpine
npm ci
FROM node:alpine
COPY package*.json ./
RUN npm ci
COPY . .
ENTRYPOINT ["node", "/index.js"]

index.js

console.log("This is my action, and it's working!")

npm init -y github-action-pull-request-milestone-node

npm init npm ci Dockerfile


package-lock.json
npm install

github-action-pull-request-milestone-node

git init
npx gitignore node
git add .
git commit -m "Initial Commit"
git remote add origin git@github.com:YOUR_USERNAME/github-action-pull-request-milestone-
node.git
git push -u origin master

action-test
.github/workflows/pull_request-merged.yml
uses
name: Pull Request Milestone
on:
pull_request:
types: [closed]
jobs:
milestone:
runs-on: ubuntu-18.04
steps:
- uses: YOUR_USERNAME/github-action-pull-request-milestone-node@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
merged_1: "Your first PR! We're glad to have you onboard"
merged_2: "Two in a row? Thanks for coming back to us"
merged_5: "5? FIVE!? Do you work here or something? Thanks for all the
contributions"
merged_13: "Unlucky for some? Not for us! That's 13 PRs merged, keep it up!"
merged_50: "That's half a century for you. Do the same again and let's see what
happens!"
merged_100: "100 merged PRs? You're the best. Raise an issue and we'll send you
some swag"

act
action-test
act -j milestone pull_request
node:alpine

[Pull Request Milestone/milestone] Start image=node:12.6-buster-slim


[Pull Request Milestone/milestone] docker run image=node:12.6-buster-slim entrypoint=
["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[Pull Request Milestone/milestone] Run actions-book/github-action-pull-request-milestone-
node@master
[Pull Request Milestone/milestone] git clone 'https://github.com/actions-book/github-
action-pull-request-milestone-node' # ref=master
[Pull Request Milestone/milestone] docker build -t act-actions-book-github-action-pull-
request-milestone-node-master:latest /Users/michael/.cache/act/actions-book-github-action-
pull-request-milestone-node@master
[Pull Request Milestone/milestone] docker run image=act-actions-book-github-action-
pull-request-milestone-node-master:latest entrypoint=[] cmd=[]
| This is my action, and it's working!
[Pull Request Milestone/milestone] Success - actions-book/github-action-pull-request-
milestone-node@master/github-action-pull-request-milestone-node@master

JavaScript npm

npm @actions

github-action-pull-request-milestone-node
npm install @actions/core @actions/github common-tags --save

console.log
index.js

async function action() {


const { stripIndent } = require("common-tags");

const core = require("@actions/core");


const github = require("@actions/github");
}

if (require.main === module) {


action();
}

module.exports = action;

require.main === module

module.exports

async
async/await

action

action()

const event = process.env.GITHUB_EVENT_NAME;


const payload = require(process.env.GITHUB_EVENT_PATH);

if (event != "pull_request" || payload.action != "closed") {


core.setFailed(stripIndent`
This action only runs on pull_request.closed
Found: ${event}.${payload.action}
`);
return;
}
core.setFailed

return return process.exit() return

require(process.env.GITHUB_EVENT_PATH)

merged
merged
pull_request.merged

pull_request.closed

setFailed 0
action()

if (!payload.pull_request.merged) {
core.warning("Pull request closed without merge");
return;
}

index.js
package.json package-lock.json
act

pull_request-closed.json action-test

YOUR_USERNAME

{
"action": "closed",
"pull_request": {
"number": 1,
"merged": true,
"user": {
"login": "YOUR_USERNAME"
}
}
}
action-test
act -j milestone -e pull_request-closed.json pull_request
true false
closed opened pull_request-closed.json act

GITHUB_TOKEN

act
repo.public_repo

@actions/github
getOctokit action()
process.env.GITHUB_TOKEN

const octokit = github.getOctokit(process.env.GITHUB_TOKEN);

octokit.pulls.list

state

const pulls = await octokit.pulls.list(


{
...github.context.repo,
state: "closed",
per_page: 100,
}
);

Link

Link

action()
let pulls = await octokit.paginate(
"GET /repos/:owner/:repo/pulls",
{
...github.context.repo,
state: "closed",
per_page: 100,
},
(response) => response.data
);

octokit.paginate Link

response.data

octokit.pulls.list
@actions/github

...github.context.repo
github.context
owner repo
github.context.repo

{"repo": "action-test", "owner": "YOUR_USERNAME"}

...
github.context.repo
github.context.issue

merged
merged_at null
true

pulls

const expectedAuthor = payload.pull_request.user.login;


pulls = pulls.filter((p) => {
if (!p.merged_at) {
return false;
}

return p.user.login == expectedAuthor;


});

const pullCount = pulls.length;


console.log(`There are ${pullCount} Pull Requests`);
pulls.length

pull_request-closed.json
pull_request.user.login act

act -j milestone -e pull_request-closed.json pull_request -s GITHUB_TOKEN

act
-s GITHUB_TOKEN act
GITHUB_TOKEN
GITHUB_TOKEN,

| There are 2 Pull Requests

merged_X X
@actions/core

const message = core.getInput(`merged_${pullCount}`);


if (!message) {
console.log("No action required");
return;
}

github.context

issues

octokit.issues.createComment
owner repo issue_number body
action()

await octokit.issues.createComment({
...github.context.repo,
issue_number: github.context.issue.number,
body: message,
});

console.log(stripIndent`
Added comment:
${message}
`);

github.context.repo owner repo


issue_number github.context.issue.nubmer
event.json

octokit.issues.addLabels

owner repo issue_number


labels
body

await octokit.issues.addLabels({
...github.context.repo,
issue_number: github.context.issue.number,
labels: [`merge-milestone`, `merge-milestone:${pullCount}`],
});

merge-milestone

async function action() {


const { stripIndent } = require("common-tags");

const core = require("@actions/core");


const github = require("@actions/github");

const event = process.env.GITHUB_EVENT_NAME;


const payload = require(process.env.GITHUB_EVENT_PATH);

if (event != "pull_request" || payload.action != "closed") {


core.setFailed(stripIndent`
This action only runs on pull_request.closed
Found: ${event}.${payload.action}
`);
return;
}

if (!payload.pull_request.merged) {
core.warning("Pull request closed without merge");
return;
}

const octokit = github.getOctokit(process.env.GITHUB_TOKEN);

let pulls = await octokit.paginate(


"GET /repos/:owner/:repo/pulls",
{
...github.context.repo,
state: "closed",
per_page: 100,
},
(response) => response.data
);

const expectedAuthor = payload.pull_request.user.login;


pulls = pulls.filter((p) => {
if (!p.merged_at) {
return false;
}

return p.user.login == expectedAuthor;


});

const pullCount = pulls.length;


console.log(`There are ${pullCount} Pull Requests`);

const message = core.getInput(`merged_${pullCount}`);


if (!message) {
console.log("No action required");
return;
}

await octokit.issues.createComment({
...github.context.repo,
issue_number: github.context.issue.number,
body: message,
});

console.log(stripIndent`
Added comment:
${message}
`);

await octokit.issues.addLabels({
...github.context.repo,
issue_number: github.context.issue.number,
labels: [`merge-milestone`, `merge-milestone:${pullCount}`],
});
}

if (require.main === module) {


action();
}

module.exports = action;

entrypoint.sh index.js
index.js

npm

node12
docker
context

action-guard

action-test
npx actions-toolkit@5 github-action-pull-request-milestone-toolkit

actions-toolkit

Welcome to actions-toolkit! Let's get started creating an action.

What is the name of your action? · Pull Request Milestone


What is a short description of your action? · Congratulate people when they hit a certain
number of merged pull requests
Choose an icon for your action. Visit https://feathericons.com for a visual reference. ·
star
Choose a background color used in the visual workflow editor for your action. · yellow

branding action.yml

cd github-action-pull-request-milestone-toolkit && npm install

.
Dockerfile
action.yml
index.js
index.test.js
node_modules
package-lock.json
package.json

Dockerfile action.yml index.js

github-action-pull-request-milestone-toolkit

git init
npx gitignore node
git add .
git commit -m "Initial Commit"
git remote add origin git@github.com:YOUR_USERNAME/github-action-pull-request-milestone-
toolkit.git
git push -u origin master

action-test

.github/workflows/pull_request-merged.yml

YOUR_USERNAME/github-action-pull-request-milestone-node@master

YOUR_USERNAME/github-action-pull-request-milestone-toolkit@master

act action-test
act -j milestone pull_request
act node:slim
Dockerfile

node:slim node:alpine Dockerfile


node:slim

success We did it!

[Pull Request Milestone/milestone] Start image=node:12.6-buster-slim


[Pull Request Milestone/milestone] docker run image=node:12.6-buster-slim entrypoint=
["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[Pull Request Milestone/milestone] Run actions-book/github-action-pull-request-milestone-
toolkit@master
[Pull Request Milestone/milestone] git clone 'https://github.com/actions-book/github-
action-pull-request-milestone-toolkit' # ref=master
[Pull Request Milestone/milestone] docker build -t act-actions-book-github-action-pull-
request-milestone-toolkit-master:latest /Users/michael/.cache/act/actions-book-github-action-
pull-request-milestone-toolkit@master
[Pull Request Milestone/milestone] docker run image=act-actions-book-github-action-
pull-request-milestone-toolkit-master:latest entrypoint=[] cmd=[]
| success We did it!
[Pull Request Milestone/milestone] Success - actions-book/github-action-pull-request-
milestone-toolkit@master
Dockerfile action.yml

npx actions-toolkit github-action-pull-request-milestone-toolkit

index.js
async Toolkit.run
async/await

action-guard

action-guard event
Toolkit.run action-guard

github-action-pull-request-milestone-toolkit action-guard

npm install action-guard --save

Toolkit.run(async (tools) => {


require("action-guard")("pull_request.closed");
tools.exit.success("We did it!");
});

action-guard
pull_request
closed merged merged
tools.context
tools.log.warn
return tools.exit.success("We did it!");

const payload = tools.context.payload;

if (!payload.pull_request.merged) {
tools.log.warn("Pull request closed without merge");
return;
}

tools.log Signale
debug
info warn error start complete
await success

github.getOctokit()
GITHUB_TOKEN
Octokit
tools.github

Octokit
Context tools.context
github.context

require

let pulls = await tools.github.paginate(


tools.github.pulls.list,
{
...tools.context.repo,
state: "closed",
per_page: 100,
},
(response) => response.data
);

octokit.pulls.list
github.Github Octokit
pulls.list
merged_at

const expectedAuthor = payload.pull_request.user.login;


pulls = pulls.filter((p) => {
if (!p.merged_at) {
return false;
}

return p.user.login == expectedAuthor;


});

const pullCount = pulls.length;


tools.log.debug(`There are ${pullCount} Pull Requests`);

Toolkit.run
act

action-test
pull_request-closed.json

act -j milestone -e pull_request-closed.json pull_request -s GITHUB_TOKEN

| debug There are 2 Pull Requests

Octokit

tools.inputs

// Using @actions/core
const message = core.getInput(`merged_${pullCount}`);

// Using Actions Toolkit


const message = tools.inputs[`merged_${pullCount}`];

// This bit stays the same


if (!message) {
tools.log.info("No action required");
return;
}

Toolkit.run

const message = tools.inputs[`merged_${pullCount}`];


if (!message) {
tools.log.info("No action required");
return;
}

github.Github
tools.github octokit tools.context
github.context tools.context.issue
tools.context.repo tools.context.repo
issue_number Issue

tools.log.pending tools.log.complete

Toolkit.run

tools.log.pending(`Adding comment`);
await tools.github.issues.createComment({
...tools.context.issue,
body: message,
});
tools.log.complete(`Added comment: ${message}`);

pending complete

| pending Adding comment


| complete Added comment: Two in a row? Thanks for coming back to us

tools.github tools.context @actions/github


pending
complete

tools.log.pending(`Adding labels`);
const labels = [`merge-milestone`, `merge-milestone:${pullCount}`];
await tools.github.issues.addLabels({
...tools.context.issue,
labels,
});
tools.log.complete(`Added labels:`, labels);

pending complete

const { Toolkit } = require("actions-toolkit");

Toolkit.run(async (tools) => {


require("action-guard")("pull_request.closed");

const payload = tools.context.payload;

if (!payload.pull_request.merged) {
tools.log.warn("Pull request closed without merge");
return;
}

tools.log.pending(`Fetching pulls`);
let pulls = await tools.github.paginate(
tools.github.pulls.list,
{
...tools.context.repo,
state: "closed",
per_page: 100,
},
(response) => response.data
);
tools.log.complete(`Fetched pulls`);

const expectedAuthor = payload.pull_request.user.login;


pulls = pulls.filter((p) => {
if (!p.merged_at) {
return false;
}

return p.user.login == expectedAuthor;


});

const pullCount = pulls.length;


tools.log.debug(`There are ${pullCount} Pull Requests`);

const message = tools.inputs[`merged_${pullCount}`];


if (!message) {
tools.log.info("No action required");
return;
}

tools.log.pending(`Adding comment`);
await tools.github.issues.createComment({
...tools.context.issue,
body: message,
});
tools.log.complete(`Added comment: ${message}`);

tools.log.pending(`Adding labels`);
const labels = [`merge-milestone`, `merge-milestone:${pullCount}`];
await tools.github.issues.addLabels({
...tools.context.issue,
labels,
});
tools.log.complete(`Added labels:`, labels);
});

act

index.test.js
index.test.js
index.js
jest

pull_request

pull_request closed

pull_request

github-action-pull-request-milestone-toolkit
npm test

jest npm install

it('exits successfully', () => {


action(tools)
expect(tools.exit.success).toHaveBeenCalled()
expect(tools.exit.success).toHaveBeenCalledWith('We did it!')
})
tools.exit.success() We did it!

warning There are environment variables missing from this runtime, but would be present
on GitHub.
- GITHUB_WORKFLOW
- GITHUB_ACTION
- GITHUB_ACTOR
- GITHUB_REPOSITORY
- GITHUB_EVENT_NAME
- GITHUB_EVENT_PATH
- GITHUB_WORKSPACE
- GITHUB_SHA

index.test.js
describe

process.env.GITHUB_WORKFLOW = "demo-workflow";
process.env.GITHUB_ACTION = "pull-request-milestone";
process.env.GITHUB_ACTOR = "YOUR_USERNAME";
process.env.GITHUB_REPOSITORY = "YOUR_USERNAME/action-test";
process.env.GITHUB_WORKSPACE = "/tmp/github/workspace";
process.env.GITHUB_SHA = "fake-sha-a1c85481edd2ea7d19052874ea3743caa8f1bdf6";
process.env.INPUT_MERGED_3 = "This message is added after 3 PRs are merged";

GITHUB_EVENT_NAME GITHUB_EVENT_PATH

GITHUB_EVENT_NAME GITHUB_EVENT_PATH
beforeEach
beforeEach
index.test.js
describe

function mockEvent(name, eventPath) {


process.env.GITHUB_EVENT_NAME = name;
process.env.GITHUB_EVENT_PATH = __dirname + "/" + eventPath + ".json";

return new Toolkit();


}
pull_request
issues

it('exits successfully')

it("fails when triggered by the wrong event", () => {


});

jest it test
describe/it

GITHUB_EVENT_NAME
GITHUB_EVENT_PATH mockEvent

it("fails when triggered by the wrong event", () => {


tools = mockEvent("issues", "issues-created");
});

GITHUB_EVENT_PATH

issues-created
issue-created.json

issues-created.json
{} index.test.js

expect(action(tools))

index.js async
jest
expect().resolves expect().rejects

expect(action(tools)).rejects

Error toThrow
.toThrow(
new Error("Invalid event. Expected 'pull_request', got 'issues'")
);

it("fails when triggered by the wrong event", () => {


tools = mockEvent("issues", "issues-created");
return expect(action(tools)).rejects.toThrow(
new Error("Invalid event. Expected 'pull_request', got 'issues'")
);
});

index.test.js it npm test

PASS ./index.test.js
Pull Request Milestone
exits when triggered by the wrong event (14ms)

Test Suites: 1 passed, 1 total


Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.024s
Ran all test suites.

issues-created.json

pull_request closed

it

it("fails when triggered by the correct event but the wrong action", () => {
tools = mockEvent("pull_request", "pull_request-opened");
});

pull_request-opened
pull_request-opened.json issues-created.json

{"action":"opened"}
index.test.js

it("fails when triggered by the correct event but the wrong action", () => {
tools = mockEvent("pull_request", "pull_request-opened");
return expect(action(tools)).rejects.toThrow(
new Error(
"Invalid event. Expected 'pull_request.closed', got 'pull_request.opened'"
)
);
});

npm test

pull_request-opened.json

GITHUB_EVENT_PATH

require

jest require jest.mock


mockEvent

function mockEvent(name, mockPayload) {


jest.mock(
"/github/workspace/event.json",
() => {
return mockPayload;
},
{
virtual: true,
}
);

process.env.GITHUB_EVENT_NAME = name;
process.env.GITHUB_EVENT_PATH = "/github/workspace/event.json";

return new Toolkit();


}
jest.mock
require(process.env.GITHUB_EVENT_PATH)

jest.mock() beforeEach
require(".") it()

beforeEach(() => {
jest.resetModules();
});

issues-created.json
pull_request-opened.json

mockEvent

// Update "it fails when triggered by the wrong event"


// Replace:
tools = mockEvent("issues", "issues-created");
// With:
tools = mockEvent("issues", {});

// And in "it fails when triggered by the correct event but the wrong action"
// Replace:
tools = mockEvent("pull_request", "pull_request-opened");
// With:
tools = mockEvent("pull_request", { action: "opened" });

npm test

expect(action(tools)).resolves async/await

it async
pull_request

it("exits when a pull request is not merged", async () => {


tools = mockEvent("pull_request", {
action: "closed",
pull_request: { merged: false },
});
});
tools.log.warn
jest
spy
tools.log.warn jest.fn()

tools.log.warn = jest.fn();
await action(tools);
expect(tools.log.warn).toBeCalledWith("Pull request closed without merge");

jest.fn()

tools.log.warn await action(tools)


tools.log.warn

it("exits when a pull request is not merged", async () => {


tools = mockEvent("pull_request", {
action: "closed",
pull_request: { merged: false },
});

tools.log.warn = jest.fn();
await action(tools);
expect(tools.log.warn).toBeCalledWith("Pull request closed without merge");
});

pull_request.merged false true user.login

index.test.js npm test

it("exits when no action is required", async () => {


tools = mockEvent("pull_request", {
action: "closed",
pull_request: {
merged: true,
user: { login: "example-user" },
},
});
await action(tools);
});

HttpError: Bad Credentials


GITHUB_TOKEN

nock

npm install nock --save-dev index.test.js

const nock = require("nock");


nock.disableNetConnect();

nock
nock
disableNetConnect

HttpError: request to https://api.github.com/repos/YOUR_USERNAME/action-test/pulls?


state=closed&per_page=100 failed, reason: Nock: Disallowed net connect for
"api.github.com:443/repos/YOUR_USERNAME/action-test/pulls?state=closed&per_page=100"

nock

await action(tools);

const getPrMock = nock("https://api.github.com")


.get("/repos/YOUR_USERNAME/action-test/pulls?state=closed&per_page=100")
.reply(200, []);

npm test

tools.log.debug tools.log.info

tools.log.debug = jest.fn();
tools.log.info = jest.fn();

await action(tools);
expect(tools.log.debug).toBeCalledWith("There are 0 Pull Requests");
expect(tools.log.info).toBeCalledWith("No action required");

it("exits when no action is required", async () => {


tools = mockEvent("pull_request", {
action: "closed",
pull_request: {
merged: true,
user: { login: "example-user" },
},
});

const getPrMock = nock("https://api.github.com")


.get("/repos/YOUR_USERNAME/action-test/pulls?state=closed&per_page=100")
.reply(200, []);

tools.log.debug = jest.fn();
tools.log.info = jest.fn();

await action(tools);
expect(tools.log.debug).toBeCalledWith("There are 0 Pull Requests");
expect(tools.log.info).toBeCalledWith("No action required");
});

pull_request.user.login

filters down to the current actor

it("filters down to the current actor", async () => {

reply

tools.log.debug

tools.log.debug
There are 2 Pull Requests

Expected: "There are 2 Pull Requests"


Received: "There are 0 Pull Requests"
merged_at user.login reply(200, [])
filters down to the current actor

.reply(200, [
{ merged_at: "2020-04-27T21:21:49Z", user: { login: "example-user" } },
{ merged_at: "2020-04-28T22:53:53Z", user: { login: "non-matching" } },
{ merged_at: "2020-04-29T23:48:22Z", user: { login: "example-user" } },
{ merged_at: null, user: { login: "example-user" } },
{ merged_at: "2020-04-30T00:11:24Z", user: { login: "non-matching" } },
])

/pulls
example-user

There are 2 Pull Requests

nock

filters down to the current actor


completes successfully reply(200, [...

.reply(200, [
{ merged_at: "2020-04-27T21:21:49Z", user: { login: "example-user" } },
{ merged_at: "2020-04-28T22:53:53Z", user: { login: "example-user" } },
{ merged_at: "2020-04-29T23:48:22Z", user: { login: "example-user" } },
])

mockEvent number pull_request

tools = mockEvent("pull_request", {
action: "closed",
pull_request: {
number: 18,
merged: true,
user: { login: "example-user" },
},
});

number
process.env.INPUT_MERGED_3

npm test
nock

HttpError: request to https://api.github.com/repos/YOUR_USERNAME/action-


test/issues/18/comments failed, reason: Nock: No match for request {
"method": "POST",
...
"body": "{\"body\":\"This message is added after 3 PRs are merged\"}"
}

getPrMock

const addCommentMock = nock("https://api.github.com")


.post("/repos/YOUR_USERNAME/action-test/issues/18/comments", {
body: "This message is added after 3 PRs are merged",
})
.reply(200);

POST GET
.post()
nock

200

nock

HttpError: request to https://api.github.com/repos/YOUR_USERNAME/action-test/issues/18/labels


failed, reason: Nock: No match for request {
"method": "POST",
...
"body": "{\"labels\":[\"merge-milestone\",\"merge-milestone:3\"]}"
}

POST
addCommentMock

const addLabelMock = nock("https://api.github.com")


.post("/repos/YOUR_USERNAME/action-test/issues/18/labels", {
labels: ["merge-milestone", "merge-milestone:3"],
})
.reply(200);
tools.log.debug tools.log.info
pending
complete

expect()

it("completes successfully", async () => {


tools = mockEvent("pull_request", {
action: "closed",
pull_request: {
number: 18,
merged: true,
user: { login: "example-user" },
},
});

const getPrMock = nock("https://api.github.com")


.get("/repos/YOUR_USERNAME/action-test/pulls?state=closed&per_page=100")
.reply(200, [
{ merged_at: "2020-04-27T21:21:49Z", user: { login: "example-user" } },
{ merged_at: "2020-04-28T22:53:53Z", user: { login: "example-user" } },
{ merged_at: "2020-04-29T23:48:22Z", user: { login: "example-user" } },
]);

const addCommentMock = nock("https://api.github.com")


.post("/repos/YOUR_USERNAME/action-test/issues/18/comments", {
body: "This message is added after 3 PRs are merged",
})
.reply(200);

const addLabelMock = nock("https://api.github.com")


.post("/repos/YOUR_USERNAME/action-test/issues/18/labels", {
labels: ["merge-milestone", "merge-milestone:3"],
})
.reply(200);

await action(tools);
});

pending complete
debug
tools.log.pending = jest.fn();
npm test -- --collect-coverage
jest

.reply(200) .reply(500)

tools.github.* try/catch

INPUT_MERGED_10

docker node12
docker

docker
docker npm ci

alpine bash
node:slim

node12

uses: YOUR_USERNAME/repo@master
uses: docker://<YOUR_DOCKERHUB_USERNAME>/my-action:latest.
docker

Docker

index.js

peter-evans/create-pull-request

await exec.exec(python, [`${__dirname}/cpr/create_pull_request.py`])

actions/toolkit

using.main
node_modules
npm install npm ci

node_modules
node_modules
dist/index.js vercel/ncc

npm install -g @zeit/ncc


ncc build index.js
# Update `using.main` in action.yml to be `dist/index.js`, commit and push

ncc
index.js
docker node12 docker
node12

docker
node12

github-action-auto-compile-node using: docker


master github-action-auto-compile-node 
ncc
ncc

mheap/github-action-auto-compile-node@master

name: Auto-Compile
on:
release:
types: [published]
jobs:
compile:
runs-on: ubuntu-16.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Automatically build action
uses: mheap/github-action-auto-compile-node@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

github-action-auto-compile-node

npm ci

ncc build

action.yml runs.using node12 runs.main


index.dist.js

docker
node12
github-action-auto-compile-node
ncc node12
build-and-tag-action
build-and-tag-action

docker

node12

index.js
node12
docker github-action-auto-compile-node
node12
branding

action.yml

branding:
icon: stop-circle
color: red
white yellow blue green orange red purple
gray-dark
airplay zap
filter key package

haya14busa

LICENSE

MIT MIT

LICENSE

npx license npx license <name>

LICENSE

LICENSE README.md

README.md

README.md
on

inputs

README.md

# github-action-pull-request-milestone

This action runs whenever a pull request is merged. If merging that pull request helps the
author hit one of our predefined milestones (e.g. 1, 5, 10, 100 PRs merged), then a comment
will be added congratulating them, and a label will be added so that we can find it again
later.

## Usage

This action is intended to run whenever a `pull_request` is `closed`. If any other event
triggers it an error will be returned.

Example workflow:

````yaml
name: Pull Request Milestone
on:
pull_request:
types: [closed]
jobs:
milestone:
runs-on: ubuntu-18.04
steps:
- uses: YOUR_NAME/github-action-pull-request-milestone@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
merged_1: "Your first PR! We're glad to have you onboard"
merged_2: "Two in a row? Thanks for coming back to us"
merged_5: "5? FIVE!? Do you work here or something? Thanks for all the
contributions"
````

## Configuration

There are no required inputs for this action.

This action uses the `merged_` inputs to control if a milestone has been hit or not. If you
would like to add a new comment after 33 pull requests, define the `merged_33` input, e.g.:

````
- uses: YOUR_NAME/github-action-pull-request-milestone@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
merged_33: "Lucky number...33?"
````

If you'd like a weekly summary of all of the milestones hit each week, consider installing
YOUR_NAME/github-action-milestone-summary.
README.md

inputs

LICENSE README.md
action.yml

Draft a release
action.yml README

pull-request-milestone

v1

awesome-actions

awesome-actions

LICENSE
README.md
schedule

merge-milestone

schedule

schedule cron
cron

* * * * *
0 1 * * * cron

cron 0 3 * * 1

action-test
.github/workflows/pull_request-summary.yml

name: Milestone Summary


on:
schedule:
- cron: '0 3 * * 1'
jobs:
summary:
runs-on: ubuntu-18.04
steps:
- uses: YOUR_USERNAME/github-action-milestone-summary@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
since: P7D

schedule
YOUR_USERNAME/github-action-milestone-summary
GITHUB_TOKEN
since

P1D PT3H
P1Y2M4DT20H44M12.67S

action-test

action-guard

schedule
github-action-milestone-summary
npx actions-toolkit
github-action-pull-request-milestone-toolkit

$ npx actions-toolkit@5 github-action-milestone-summary

Welcome to actions-toolkit! Let's get started creating an action.

What is the name of your action? · Milestone Summary


What is a short description of your action? · Get a regular summary of any PR milestones
that have been hit in your repo
Choose an icon for your action. Visit https://feathericons.com for a visual reference. ·
paperclip
Choose a background color used in the visual workflow editor for your action. · green

cd github-action-milestone-summary && npm install

iso8601-duration

npm

npm install iso8601-duration --save

iso8601-duration
index.js

const { parse, toSeconds } = require("iso8601-duration");

parse toSeconds iso8601-duration


since
Toolkit.run tools.exit.success

// What time frame are we looking at?


let duration;
try {
duration = toSeconds(parse(tools.inputs.since));
} catch (e) {
tools.exit.failure(`Invalid duration provided: ${tools.inputs.since}`);
return;
}
const earliestDate = Math.floor(Date.now() / 1000) - duration;

tools.github.paginate

since
updated created

let pulls = await tools.github.paginate(


tools.github.pulls.list,
{
...tools.context.repo,
state: "closed",
per_page: 100,
sort: "updated",
},
(response) => response.data
);

tools.github.paginate

(response) => response.data

done
earliestDate done

(response, done) => {


const pulls = response.data.filter((pr) => {
const updated = Math.floor(Date.parse(pr.updated_at).valueOf() / 1000);
return updated > earliestDate;
});

if (pulls.length !== response.data.length) {


done();
}
return pulls;
}
let pulls = await tools.github.paginate(
tools.github.pulls.list,
{
...tools.context.repo,
state: "closed",
per_page: 100,
sort: "updated",
},
(response, done) => {
const pulls = response.data.filter((pr) => {
const updated = Math.floor(Date.parse(pr.updated_at).valueOf() / 1000);
return updated > earliestDate;
});

if (pulls.length !== response.data.length) {


done();
}
return pulls;
}
);

index.js earliestDate

merge-milestone
.filter()
Toolkit.run

// Filter to those that have the right labels


pulls = pulls.filter((pr) => {
return pr.labels.find((label) => {
return label.name == "merge-milestone";
});
});

.find()

pulls
merge-milestone

merge-milestone:

// Group PRs by label


const milestones = {};
for (const pr of pulls) {
const label = pr.labels.find((label) =>
label.name.startsWith("merge-milestone:")
);

if (label) {
milestones[label.name] = milestones[label.name] || [];
milestones[label.name].push(pr);
}
}
milestones

{
"merge-milestone:5": [<pr>, <pr>],
"merge-milestone:10": [<pr>],
}

milestones

// Are there any milestones?


if (!Object.keys(milestones).length) {
tools.exit.success("No milestones hit");
return;
}

// Build an issue body


let body = "";
for (const milestone in milestones) {
body += `## ${milestone}\n\n`;
for (const pr of milestones[milestone]) {
body += `* [${pr.title}](${pr.html_url}) (@${pr.user.login})`;
}
}

body title
tools.context.repo

// Create an issue
await tools.github.issues.create({
...tools.context.repo,
title: "Milestone Update",
body,
});

tools.exit.success("Report created");

const { Toolkit } = require("actions-toolkit");


const { parse, toSeconds } = require("iso8601-duration");

Toolkit.run(async (tools) => {


// What time frame are we looking at?
let duration;
try {
duration = toSeconds(parse(tools.inputs.since));
} catch (e) {
tools.exit.failure(`Invalid duration provided: ${tools.inputs.since}`);
return;
}

const earliestDate = Math.floor(Date.now() / 1000) - duration;


let pulls = await tools.github.paginate(
tools.github.pulls.list,
{
...tools.context.repo,
state: "closed",
per_page: 100,
sort: "updated",
},
(response, done) => {
const pulls = response.data.filter((pr) => {
const updated = Math.floor(Date.parse(pr.updated_at).valueOf() / 1000);
return updated > earliestDate;
});

if (pulls.length !== response.data.length) {


done();
}
return pulls;
}
);

// Filter to those that have the right labels


pulls = pulls.filter((pr) => {
return pr.labels.find((label) => {
return label.name == "merge-milestone";
});
});

// Group PRs by label


const milestones = {};
for (const pr of pulls) {
const label = pr.labels.find((label) =>
label.name.startsWith("merge-milestone:")
);

if (label) {
milestones[label.name] = milestones[label.name] || [];
milestones[label.name].push(pr);
}
}

// Are there any milestones?


if (!Object.keys(milestones).length) {
tools.exit.success("No milestones hit");
return;
}

// Build an issue body


let body = "";
for (const milestone in milestones) {
body += `## ${milestone}\n\n`;
for (const pr of milestones[milestone]) {
body += `* [${pr.title}](${pr.html_url}) (@${pr.user.login})`;
}
}

// Create an issue
await tools.github.issues.create({
...tools.context.repo,
title: "Milestone Update",
body,
});

tools.exit.success("Report created");
});
since action-test
action.yml

name: Milestone Summary


description: Get a regular summary of any PR milestones that have been hit in your repo
runs:
using: docker
image: Dockerfile
branding:
icon: paperclip
color: green
inputs:
since:
description: "How far back to search for closed PRs in ISO8601 Duration format"
required: true

act

github-action-milestone-summary
git

git init
npx gitignore node
git add .
git commit -m "Initial Commit"
git remote add origin git@github.com:YOUR_USERNAME/github-action-milestone-summary.git
git push -u origin master

action-test
schedule
act -j schedule
event.json
action.yml
GITHUB_TOKEN

master
master GITHUB_TOKEN
push

PAT
GITHUB_TOKEN

GITHUB_TOKEN

- uses: YOUR_USERNAME/action-name@master
env:
GITHUB_TOKEN: ${{ secrets.PAT }}

repository_dispatch
POST
/repos/{owner}/{repo}/dispatches
client_payload context.payload
curl repository_dispatch

curl -X POST -H "Accept: application/vnd.github.v3+json"


https://api.github.com/repos/YOUR_USERNAME/action-test/dispatches -d
'{"event_type":"event_type", "client_payload": {"unit": true, "integration": false} }'

curl
POST

repository_dispatch

workflow_dispatch
inputs
action.yml

master
master

1.2.3 1
2 3

YOUR_USERNAME/action-name@v1 v1
master

@v1 v1.1.0
action-tagger

v1.1.0
@v1

# https://github.com/marketplace/actions/actions-tagger
name: Keep the versions up-to-date
on:
release:
types: [published, edited]
jobs:
actions-tagger:
runs-on: ubuntu-latest
steps:
- uses: Actions-R-Us/actions-tagger@latest
env:
GITHUB_TOKEN: "${{secrets.GITHUB_TOKEN}}"

github-action-auto-compile-node
docker node12

build-and-tag

build-and-tag github-action-auto-compile-node
npm run build package.json
setup

# https://github.com/marketplace/actions/build-and-tag
- uses: JasonEtco/build-and-tag-action@v1
with:
setup: 'npm ci && npm run custom-build'

@v1

@master

@master @v1

@master
pin-github-action

steps:
- uses: actions/checkout@v2
- uses: YOUR_USERNAME/custom-action@master

pin-github-action /path/to/.github/workflows/your-name.yml

steps:
- uses: actions/checkout@db41740e12847bb616a339b75eb9414e711417df # pin@v2
- uses: YOUR_USERNAME/custom-action@73549280c1c566830040d9a01fe9050dae6a3036 # pin@master

pin-github-action
@v2 @master

master

branches push

on:
push:
branches:
- master

push
master
on.push.branches
jobs.<job_id>.steps.if
actions/checkout actions/setup-node
npm test push npm publish master

name: "Test and Release"


on: push
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- name: Test
run: npm test
- name: Release
run: npm publish
if: github.ref == 'refs/heads/master'

if Release

if

- uses: action-that/requires-secrets@master
if: github.repository == 'YOUR_USERNAME/repo'

- run: npm test


if: github.actor != 'mheap'

if

- uses: windows/specific-action@master
if: startsWith(matrix.os, 'windows')
- uses: unix/specific-action@master
if: !startsWith(matrix.os, 'windows')

- uses: my/action@master
if: github.event_name == 'pull_request' && github.event.action == 'labeled'
if
steps if
runs-on if

merged
closed

pullRequests
MERGED

query MergedPullRequests {
repository(owner: "YOUR_USERNAME", name: "action-test") {
pullRequests(first: 100, states: MERGED) {
totalCount
pageInfo {
hasNextPage
}
edges {
cursor
}
nodes {
number
author {
login
}
}
}
}
}

Link
pageInfo.hasNextPage
pullRequests(first:100, states: MERGED, after: <cursor>)
edges.cursor

search

query searchRepos {
search(query: "repo:YOUR_USERNAME/action-test type:pr is:merged author:YOUR_USERNAME",
type: ISSUE, first: 100) {
pageInfo {
hasNextPage
}
edges {
cursor
}
nodes {
... on PullRequest {
number
}
}
}
}

ISSUE

Link

octokit.graphql

const query = `
query($searchQuery: String!, $after: String) {
search(query: $searchQuery, type: ISSUE, first: 100, after: $after) {
pageInfo {
hasNextPage
endCursor
}
nodes {
... on PullRequest {
number
}
}
}
}
`;

let prCount = 0;
let after = null;
let results;

do {
results = await octokit.graphql(query, {
searchQuery: "repo:YOUR_USER/action-test type:pr is:merged author:ACTOR_NAME",
after,
});
after = results.search.pageInfo.endCursor;
prCount += results.search.nodes.length;
} while (results.search.pageInfo.hasNextPage);

octokit.paginate do..while

octokit.pulls.list
octokit.pulls.list

mheap/github-action-auto-compile-node@master

docker
node12
action.yml

docker dockerhub
dockerhub
docker build

master
v1.0.4 v1 v1.0
v1.0.4 docker

docker://

Dockerfile

FROM username/action:latest

Dockerfile username/action
dockerhub docker://
docker build
Dockerfile YOUR_USER/action@v1

docker://

docker

docker docker

peter-evans/create-pull-request create-pull-request
docker
inputs

create_pull_request.py @actions/exec

setupPython pip install

actions/github-script
on:
issues:
types: [opened]

jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: ' Thanks for reporting!'
})

- name: Check if version bumps should be skipped


uses: actions/github-script@v1
id: skip_check
with:
github-token: ${{secrets.GITHUB_TOKEN}}
result-encoding: string
script: |
const pr = await github.issues.get({
issue_number: context.issue.number,
owner: context.issue.owner,
repo: context.issue.repo
})
const skipVersion = pr.data.labels.filter((l) => l.name == 'skip-version-check');
return skipVersion.length ? 'skip' : 'execute'

actions/github-script

actions/github-script
octokit/request-action
request-action
github-script
- uses: octokit/request-action@v2.x
with:
route: POST /repos/:owner/:repo/issues/:issue_number/comments
owner: ${{ github.event.issue.owner }}
repository: ${{ github.event.issue.repository }}
issue_number: ${{ github.event.issue.number }}
body: ' Thanks for reporting!'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

route
github.issues.createComment()
actions/github-script

octokit/request-action

${{ secrets.GITHUB_TOKEN }}

secrets

- uses: YOUR_USERNAME/action
with:
url: https://api.example.com/create-foobit

- uses: YOUR_USERNAME/action
with:
url: https://api.example.com/create-foobit
username: admin
password: password123

username password
secrets

- uses: YOUR_USERNAME/action
with:
url: https://api.example.com/create-foobit
username: ${{ secrets.API_USERNAME }}
password: ${{ secrets.API_PASSWORD }}
Settings->Secrets API_USERNAME
Add secret

repository_dispatch workflow_dispatch

if

- uses: YOUR_USERNAME/action
with:
url: https://api.example.com/create-foobit
username: ${{ secrets.API_USERNAME }}
password: ${{ secrets.API_PASSWORD }}
if: github.repository == 'YOUR_USERNAME/repo'
action/github-script octokit/request

docker
act

You might also like