Table of Contents
- GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows us to automate your build, test, and deployment pipeline.
- We can create workflows that build and test every pull request to our repository or deploy merged pull requests to production. There are many use-cases and events which we can combine to get the outputs we desire.
In a nutshell, we define a set of commands in a file and push it inside our code. when this file is identified by Github, it will run those commands in a cloud device hosting our code.
workflow: A workflow is a
.yml(learn about its syntax here.) file that we add in the
.github/workflowsfolder, and check out in our project repo. It consists of a set of
jobsthat will run on our code when triggered by an
jobscan also be triggered manually. We can have multiple workflows in our project
Event: An event is a specific activity in a repository that triggers a workflow run. For eg, a trigger can originate from GitHub when someone creates a pull request, opens an issue, or pushes a commit to a repository. You can also trigger a workflow run on a schedule, by posting to a REST API, or manually. check this list for all events:
Jobs: Jobs are a set of
steps that run sequentially, in the same runner. a workflow defines 1 or more
Jobs and in a
job, we define 1 or more
steps. Each Job runs in its own container and therefore the steps in each job are executed in parallel unless configured otherwise
Step: A step is the smallest unit of command in a Github workflow. Each step is either a shell script that will be executed or an
action that will be run. Steps are executed in order and are dependent on each other. Since each step is executed on the same runner, we can share data from one step to another
- As previously mentioned, the
Jobsrun in parallel containers while the
stepsrun sequentially inside the same container. therefore we can mix and match whether to run a particular task in a separate job (to allow it to run in parallel) or in the same job( to allow it to run in sequential order, alongside other
- The failure of a
jobwill result in failure of the whole
jobby default(unless configured otherwise) but it won’t affect the other jobs running in parallel
Considering the following example :
name: Denotes the name of the GitHub action that will show up in the action run history
on: This section denotes the events that will trigger the jobs defined in the
jobsection. there can be many sub configurations in this block. For eg, if we want the workflow to get triggered by a pull request, we can mention the names of
branchesagainst which the PR should be made to trigger. another sub-config available for PR is
type: which can take the values of
edited, .. etc. The complete list of events can be found here
jobs: This section consists of various jobs that will be executed in parallel once the event(s) mentioned in
onsection occur. their name can be anything:
3.1 Setting up a job.
A job could consist of many attributes for a specific configuration. An example of some of the common attributes would look like these:
runs-on: [self-hosted, ubuntu-latest]
if: $ <expression>
permissions: These are the container-level permissions that we are providing to our job. There can be multiple types of permissions.
runs-on:here we are defining the machine that will be used to execute the job
if: $}: These are conditional expressions that are executed in a declarative manner. the conditions can be applied to various levels in a workflow. it can be used to skip or perform the whole job or just a step. But what do these conditions work upon? the
$can be used to evaluate any condition like
$1+2==3. There are also certain properties that get generated for every
$job.status == 'success'. These are called context.
needs: This is an attribute that makes different parallel running jobs run sequentially. ideally,
job2in the first example should be running in parallel to
my_first_job. by using
needs, we are making sure that
job2will run after
steps: A job contains a sequence of tasks called steps. Steps can run commands, run set up tasks, or run actions in your repository, a public repository, or action published in a Docker registry. Steps can take inputs (in the form of constants) and generate output. The steps in a job are executed sequentially. By default, the steps are configured to fail execution if the previous step has failed or given a failure output, but this behaviour could be modified via conditional expressions.
3.2 Structure of a step
- name: Setup Path Filter task and Execute
md: ['CHANGELOG.md'] - name: FAIL if mandatory files are not changed
if: $ steps.filter.outputs.md == 'false'
core.setFailed('Mandatory markdown files were not changed') - name: Generate AAR and APK files.
run: ./gradlew assembleDebug
name: This is an optional key that gives a small description of the step. if the name key is present, then it will show up in the workflow terminal during execution
id: This is another optional key that uniquely identifies the step. we can get details about the step(its inputs, outputs, execution time, etc) using this key. here the 2nd step “FAIL if mandatory files are not changed” uses the input of its previous step to determine whether to run or fail the action via
run: These are 2 very important features of a step. a step must define either one of these.
- when using
uses, we are letting the runner know that we want to execute a bunch of commands that are written in some action from the marketplace. It’s like using a library. for eg dorny/paths-filter is a popular activity for checking whether some file has changed or not
- When using
run, we are letting the runner know that we want to execute a particular command on the host machine. this is like using a terminal in our own machine: whether the command can execute successfully or not depends upon the already present software on the system and whether it would support its execution. For eg :
./gradlew assembleDebugis a terminal command that we want to run, and it will only execute successfully if the host machine has
gradleinstalled by default, or as a part of the previous sequential steps
shell: This command is used in association with
runcommand to let the host machine know about the terminal we want to use
with: This attribute is used with steps that use 3rd party actions (via
uses)only and provides inputs to them. you can pass numbers, strings, or even js scripts via arguments in the
withblock. The keys are defined by those actions themselves
env: This attribute is similar to
with, but is used to define inputs for the step itself, and can be used with both
Since we can have multiple workflows in our repo, there are chances that we will be using the same steps in each of them and this will cause duplication. And since duplication is bad and difficult to maintain, we might want a solution that allows us to isolate
steps from the workflows in a centralised place and reuse them whenever required.
Here’s when Workflow Composition becomes helpful. Using workflow composition we can create separate files for each step and use them via
uses syntax in any workflow. the steps will be independent of the job or system environment and therefore will be able to run in any workflow with a very small amount of integration.
Here’s an example of a workflow that DOES NOT use composition:
if we have to create 10 more workflows that use
Check lint and
Upload Lint results, it would require us to copy all those long Gradle commands and path names manually which are error-prone. It would also be difficult to maintain 10 such copies for future modifications.
With composition, this file becomes as small as this:
Only 2 lines are required to run both lint checks and upload results! now we can create 10 or even 100 copies, we don’t need to worry about making a mistake in path files or Gradle commands! this is because these details are present in a separate YML file
.github/mini_flows/s3_lint/action.yml with somewhat similar syntax: