You might have heard that Github launched their awesome Github Actions now for everyone?
Nat Friedman@natfriedmanGood news: anyone who signs up for the GitHub Actions beta will get admitted instantly. Give it a try! github.com/features/actio…03:10 AM - 18 Sep 2019
Github Actions brings a huge potential for automation of almost any kind of routines.
And the first thing I tried was a bot. I like the idea of interactive automation via commands, be it in Slack or Github. But if you can't run your server with a bot, Github Actions comes to the rescue!
The idea
Let's say you want to have a great GIPHY bot which can handle commands from the issue or pull request comments and its syntax could look like this:
~giphy cat
The workflow of the Github Action can be described as:
- Trigger when the issue gets a new comment
- Extract the command and its arguments from the comment's body
- Query api.giphy.com and fetch the first matching picture
- Post a comment back with the gif
The action
As an entry point, the official documentation is a good place to start.
Let's say we know how to define a skeleton of the action which should trigger on a new comment
---
name: A GIPHY bot
on: issue_comment
jobs:
post_image:
name: Post an image from the search
runs-on: ubuntu-latest
steps:
# ... our steps ...
After our skeleton is defined we need to set the steps that will be executed. According to the workflow we describe, after a new issue comment appears, we need to parse the message body and then pass the result to the next step. Sounds familiar, right?
It sounds like a Linux pipe operator and if I needed to do this in a console, I would do something like this
$ echo "~giphy cat" | \
grep --color=never -oP "(?<=~giphy\s)[\w\d\s]+" | \
xargs -I{} curl "https://api.giphy.com/v1/gifs/search?api_key=<API Key>&limit=1&rating=g&q={}"
Ok, let's try it ...
The fail
Execute a command, take its result and pass to the next step, sounds easy, right? According to the documentation of the spec context, you should identify your step and then you can use its output like this
---
# ... omitted ...
steps:
- name: Step 1
id: step_1
run: echo "Hello World"
shell: bash
- name: Step 2
id: step_2
run: echo "${{ steps.step_1.outputs.<output name> }}"
shell: bash
What to put in as the "output name" you ask? I asked myself the same question. At first glance, I would expect to have like the Linux standards streams here
Name | File descriptor | Abbreviation |
---|---|---|
Standard input | 0 | stdin |
Standard output | 1 | stdout |
Standard error | 2 | stderr |
But if you try to use it, you will get an error:
---
# ... omitted ...
steps:
- name: Step 1
id: step_1
run: echo "Hello World"
shell: bash
- name: Step 2
id: step_2
run: echo "Simon says - ${{ steps.step_1.outputs.0 }}"
shell: bash
### ERRORED 09:27:44Z
- Your workflow file was invalid: The pipeline is not valid.
.github/workflows/giphy.yml (Line: 16, Col: 14): Unexpected symbol: '0'.
Located at position 29 within expression: steps.parse.outputs.0
Seems it expects letters for the output name, which I can understand because your command can set more than one value for its execution result.
Surely we can write a custom action and use the action API to set the output, but I just want to have a dead-simple pipelining and less code to maintain.
The help comes from Google. Looks like I was not the only one who fails to get step output. Same question had been asked in the Github Community and some other blog posts
https://jasonet.co/posts/new-features-of-github-actions/
These methods use functionality of the runtime that isn’t documented (I had to read through some code to figure out how it was working), so it may feel a little magical. Now, if you’re like me, you’re thinking “Can this be done without the toolkit or JavaScript?” Turns out, yes!
Turns out, that to set the result you should use the yet undocumented functionality of the commands which looks like this
/**
* Commands
*
* Command Format:
* ##[name key=value;key=value]message
*
* Examples:
* ##[warning]This is the user warning message
* ##[set-secret name=mypassword]definitelyNotAPassword!
*/
Let's give it a try:
---
# ... omitted ...
steps:
- name: Step 1
id: step_1
run: echo "##[set-output name=hello;]Hello World"
shell: bash
- name: Step 2
id: step_2
run: echo "Simon says - ${{ steps.step_1.outputs.hello }}"
shell: bash
2019-10-16T17:27:03.2937669Z Current runner version: '2.159.0'
2019-10-16T17:27:03.2956102Z Prepare workflow directory
2019-10-16T17:27:03.4191005Z Prepare all required actions
2019-10-16T17:27:03.5977738Z ##[group]Run echo "##[set-output name=hello;]Hello World"
2019-10-16T17:27:03.5978178Z echo "##[set-output name=hello;]Hello World"
2019-10-16T17:27:04.3506195Z shell: /bin/bash --noprofile --norc -e -o pipefail {0}
2019-10-16T17:27:04.3506360Z ##[endgroup]
2019-10-16T17:27:04.4259015Z ##[set-output name=hello;]Hello World
2019-10-16T17:27:04.4740495Z ##[group]Run echo "Simon says - Hello World"
2019-10-16T17:27:04.4740667Z echo "Simon says - Hello World"
2019-10-16T17:27:04.4778909Z shell: /bin/bash --noprofile --norc -e -o pipefail {0}
2019-10-16T17:27:04.4779052Z ##[endgroup]
2019-10-16T17:27:04.4858165Z Simon says - Hello World
2019-10-16T17:27:04.4936789Z Cleaning up orphan processes
Ok, now we know how to set output! I will show the result of the workflow with the steps pipelining and a use case of extra actions, like actions/github-script.
---
name: A GIPHY reply bot
on: issue_comment
jobs:
post_image:
name: Post an image from the GIPHY search
runs-on: ubuntu-latest
steps:
- name: Parse command
id: parse
shell: bash
run: |
echo "##[set-output name=query;]$(echo "${{ github.event.comment.body }}" | grep --color=never -oP "(?<=~giphy\s)[\w\d\s]+")"
- name: Search picture
id: search
shell: python
run: |
import json
from urllib import urlopen
url = 'https://api.giphy.com/v1/gifs/search?limit=1&rating=g&api_key=${{ secrets.API_KEY }}&q=${{ steps.parse.outputs.query }}'
body = json.loads(urlopen(url).read())
print('##[set-output name=url;]' + body['data'][0]['images']['fixed_width']['url'])
- name: Reply with a picture
uses: actions/github-script@0.2.0
with:
github-token: ${{ github.token }}
script: |
return github.issues.createComment({
...context.issue, body: "![giphy](${{ steps.search.outputs.url }})"
});
NOTE: Don't forget to set the repository secret API_KEY
with your giphy application key if you want to try this workflow
Thoughts
I like the new YAML syntax. It feels more logical and simple. I see the constant improvements over the GUI of the Actions page.
I wish that Github makes steps
more like a Linux pipeline, which will allow you to easily transfer your knowledge of working in the console to the Github Actions.
At the same time, I understand the need to create custom Actions to satisfy different needs, but listen, somehow GNU manages to have an awesome set of composable commands and so can we, too!
Here is the repository with the code samples
Strech / giphy-bot-example
An example of pipelining in Github actions
A Github Actions GIPHY bot
If you would like to see how bot is reacting on your messages, go to this issue and leave a comment like
~giphy grumpy cat
Top comments (0)