[GitHub Actions] Rest in Peace, Manual Deployment — GitHub Actions Has Arrived - 5. Get to Work, GitHub Actions! Steps & Actions
[GitHub Actions] Rest in Peace, Manual Deployment — GitHub Actions Has Arrived - 5. Get to Work, GitHub Actions! Steps & Actions
This post covers Steps and Actions, which are what actually instruct GitHub Actions to perform work.
This is the area where GitHub Actions' behavior is directly described, and how you configure the actions essentially determines the purpose of that workflow.
Steps are declared under Jobs, and form a collection made up of multiple actions.
While a workflow is running, a step sequentially runs the actions declared under it.
YAML
name: jobs test on: workflow_dispatch jobs: jobs_test1: runs-on: ubuntu-latest steps: # actions
It can be defined under the desired job using the steps directive.
The example above shows it applied to the jobs_test1 job.
Actions are the smallest units that describe a workflow's behavior, and by default you can use Linux shell commands.
YAML
name: jobs test on: workflow_dispatch jobs: jobs_test1: runs-on: ubuntu-latest steps: - run: echo "Hello, World!" - run: | echo "This is for" echo "multi line"
When run, the workflow above prints out the text Hello, World.
In addition to writing a workflow yourself, you can also easily use scripts you want by leveraging actions published on the marketplace.
For scripts, you can enter | on the first line to conveniently use multiple lines.
You can assign a name to an action. This name is displayed when GitHub Actions runs, and can describe the action's behavior or purpose.
The specified name is shown in GitHub Actions.
YAML
name: name test on: workflow_dispatch jobs: job: runs-on: ubuntu-latest steps: - name: name test run: echo include name - run: echo exclude name
The script above includes two actions — one has a name declared, and the other doesn't.
It's displayed in GitHub Actions like this.
🖼️ How actions are displayed in GitHub Actions
When name is provided, that text is shown; if not, default text is shown.
This lets you intuitively indicate the nature of each action.
The name attribute is optional, and has no effect on the actual behavior.
id is a unique value that lets you target an action.
YAML
name: name test on: workflow_dispatch jobs: job: runs-on: ubuntu-latest steps: - name: name test id: name run: echo include name
You can declare id as shown above, with any text you want. This lets you reference a specific action by this unique value.
Just as you can specify desired output values for a job, you can also specify output values for an action and use them.
YAML
name: actions output test on: workflow_dispatch jobs: job: runs-on: ubuntu-latest steps: - name: output test id: key run: echo "VAR=action output" >> $GITHUB_OUTPUT - name: output check run: echo ${{ steps.key.outputs.VAR }}
Within an action, you can set an output value with the code below.
BASH
echo "key=value" >> $GITHUB_OUTPUT
If you search for related info, you may come across resources that say to assign it using the command echo "::set-output name=SELECTED_COLOR::green" — that command is deprecated. It still works, but you'll see a warning in the GitHub Actions console. For more details, see the official documentation.
Specify the desired key in key and the value to assign in value, and the output value is defined.
Output values can be used in the form ${{ steps.[ID].outputs.[KEY] }}.
The script above assigned the value action output to a key called VAR as output, and the result is as follows.
BASH
action output
You can also use multiple commands to specify multiple output values.
Occasionally, because work is split across multiple jobs, you may need to reference an output value declared in the current job from another job.
In that case, it's not impossible, but you do need to change the declaration method a bit.
YAML
name: actions output test on: workflow_dispatch jobs: var: runs-on: ubuntu-latest outputs: VAR: ${{ steps.key.outputs.VAR }} steps: - name: output test id: key run: echo "VAR=action output" >> $GITHUB_OUTPUT check: runs-on: ubuntu-latest needs: var steps: - name: output check in other job run: echo ${{ needs.var.outputs.VAR }}
Unlike before, you can see that the output value is now specified on the job. What's notable here is that the job's output value is mapped to the action's output value.
Using this form, you can map an output value to a job so that other jobs can reference it. However, this assumes the referenced job's mapping has already completed. Since the check job references the var job, the needs keyword is used to configure the flow so it only runs after the var job finishes.
Just as you can specify environment variables for a workflow, you can also specify desired environment variables for an action.
YAML
name: env test on: workflow_dispatch jobs: var: runs-on: ubuntu-latest steps: - id: key run: echo "VAR=action env" >> $GITHUB_ENV - run: echo ${{ env.VAR }}
Within an action, you can set an environment variable with the code below.
BASH
echo "key=value" >> $GITHUB_ENV
This is very similar to the output-value-setting code mentioned above. Likewise, there's a deprecated version of this code starting with echo "::set-env.
Environment variables can be called with ${{ env.[KEY] }}, and can only be used within the scope in which they were declared. Unlike a workflow-level environment variable, which can be used anywhere in the workflow, a job-level environment variable can only be used within that job.
If you need to reference an environment variable declared within a job from another job, you should manage it as an output value instead of an environment variable.
BASH
action env
The output result is as shown above.
As we saw in Chapter 2, you can specify environment variables at the workflow level.
What happens if you declare an environment variable with the same key again inside a job?
YAML
name: env test on: workflow_dispatch env: VAR: workflow env jobs: var: runs-on: ubuntu-latest steps: - id: key run: echo "VAR=action env" >> $GITHUB_ENV - run: echo ${{ env.VAR }} check: runs-on: ubuntu-latest steps: - run: echo ${{ env.VAR }}
The code above declares two jobs, var and check, and each of their sub-actions prints out the VAR environment variable.
However, in var, the VAR environment variable is reassigned within the action.
When an environment variable with the same key is assigned in a narrower scope like this, it gets overridden by the value assigned in that narrower scope.
BASH
# var action env # check workflow env
So the results come out as shown above, respectively. Within the var job, the overridden value is printed.
Using conditional actions, you can run or skip a desired action only under certain conditions.
YAML
name: conditional test on: workflow_dispatch jobs: job: runs-on: ubuntu-latest steps: - name: always run run: echo "always run" - name: run on condition if: ${{ github.event_name == 'push' }} run: echo "run at only push event"
You can specify a condition on an action using the if directive, which returns true or false. The code above calls the GitHub Actions event name and only runs the additional action on a push event.
BASH
# On push always run run at only push event # Not on push always run
Using the output values of jobs or actions to drive conditional branching is a commonly used approach.
Occasionally, depending on the nature of the workflow, there may be workflows that require a long load time of several minutes or more.
In that case, you can specify a timeout to limit the run time.
YAML
name: timeout test on: workflow_dispatch jobs: job: runs-on: ubuntu-latest timeout-minutes: 1 steps: - name: half min test run: sleep 120
You can specify the desired timeout duration in minutes using the timeout-minutes directive. The code above limits that job's run time to 1 minute, while the sleep command specifies a wait time of 2 minutes.
BASH
Error: The operation was canceled.
If the timeout is exceeded, the above error occurs.
You can also directly use various actions built by other people. This lets you conveniently compress long, complex scripts down to something simple.
For an already-defined action, you just enter that action's name using the uses directive.
YAML
name: checkout test on: workflow_dispatch jobs: job: runs-on: ubuntu-latest timeout-minutes: 1 steps: - uses: actions/checkout@v4 with: repository: RWB0104/itcode.dev path: repo - run: ls -al
The actions/checkout@v4 action is a script that checks out and downloads the code from the user's current repository, and it's an action you'll end up using often.
A custom action like this may require various parameters depending on its role or purpose, in which case you can specify key: value parameters using the with directive. Naturally, the required parameters and their roles are documented by that action's developer, so refer to that when specifying them.
For instance, the code above supplies extra options to check out the repository RWB0104/itcode.dev into the repo/ path.
Usually the biggest reason to use GitHub Actions is CI/CD. Nine times out of ten, you'll end up doing build-related work, and every container is independent as its own unit of work for each script run. So it's assumed that every job always starts in a fresh, initialized state.
Installing dependencies and libraries during the build process takes a lot of time. Applying caching can skip this process and dramatically improve build time.
Let's cache a desired folder using the actions/cache@v4 action.
On the frontend, there's a dependency folder called node_modules. You can cache that folder to skip the dependency installation process.
YAML
name: Caching Primes on: push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Cache Primes id: cache-primes uses: actions/cache@v4 with: path: node_modules key: cache-${{ hashFiles('./package-lock.json') }} - name: Generate Prime Numbers if: steps.cache-primes.outputs.cache-hit != 'true' run: npm i - run: npm build - run: npm publish
Here's what the actions in the code above do.
- Check out the repository to download the code into the container.
- Cache the node_modules/ folder. The cache key is the value entered in key.
- GitHub Actions caches the data under that key.
- ${{ hashFiles('./package-lock.json') }} is an expression that returns the hash value of package-lock.json. Since that JSON file changes whenever a dependency changes, the cache key changes along with it. This lets it signal when a new cache is needed.
- The actions/cache@v4 action returns cache-hit as an output value indicating whether the cache was hit. This is used as a condition so dependency installation only runs when there was no cache hit.
- Build the project.
- Deploy the project.
That's the general idea. Actions, output values, and branching are combined to optimize the CI/CD pipeline. Once data has been cached, if the dependencies haven't changed, the next build can skip dependency installation entirely and just use the cached data right away.
You can check cached data from the repository under [Actions tab - Caches menu under the Management item in the sidebar]. Each repository is given 10GB, and if that capacity is exceeded, the oldest caches are removed in order to free up space.
I remember that back when I had no concept at all of GitHub Actions or CI/CD services, I used to frequently break my flow by manually building and deploying locally.
After coming to understand GitHub Actions, I was able to automate a lot of small, tedious tasks and reduce the amount of development work.
This isn't a perfect write-up, but I hope this post helps you get a better understanding of GitHub Actions.
With this post, I'm wrapping up the GitHub Actions series. I started it boldly, but along the way the flow of posts broke down, and it ended up as a post I couldn't finish for a long time.
I've got a few more posts I want to write, and I felt I needed to wrap up this series before starting them, so I'm ending the series with this chapter.