DEV Community

Cover image for What I miss in Github Actions pipeline?
Sergey Fedorov
Sergey Fedorov

Posted on

What I miss in Github Actions pipeline?

You might have heard that Github launched their awesome Github Actions now for everyone?

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:

  1. Trigger when the issue gets a new comment
  2. Extract the command and its arguments from the comment's body
  3. Query api.giphy.com and fetch the first matching picture
  4. 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

GitHub logo 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

How it works

Top comments (0)