GitHub Actions - Udemy

https://cloudwalk.udemy.com/course/github-actions-the-complete-guide/

Awesome tool to run GitHub Actions locally: https://github.com/nektos/act

Git Crash Course

undoing commits

GitHub Actions

Summary

Core components

Workflows go in .github/workflows/*.yml

Events

Most used:

official documentation

Runners

Environments where the jobs are executed.

Most used: ubuntu-latest.

official documentation.

Actions

In the steps you can either run Actions or run shell commands.

Action: a custom application that performs a typically complex frequently repeated task.

3 main building blocks

creating your first workflow

# define the name of the **workflow**
name: First Workflow

# define the event that triggers this workflow
on: workflow_dispatch # <- this means "manual trigger"

# define the **jobs** of this workflow
jobs:
  first-job: 
    # define the runner for this job
    # list of supported runners:
    # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
    runs-on: ubuntu-latest

    # define the **steps** of this job
    steps:
      - name: Print greeting
        run: echo "Hello World"
      - name: Print bye
        run: echo "Done! Bye!"

simple workflow structure to memorize:

name: <workflow-name>
on: <event>
jobs:
  <job-name>:
    runs-on: ubuntu-latest
    steps:
      - name: <step-name>
        uses: <action-name>
      - name: <step-name>
        run: <shell-command>

Events (Workflow Triggers)

official doc

See also #Event Filters and Activity Types

NOTE: the on: keyword accepts an array with multiple events.

What are Actions?

Action is an alternative to the run command in the workflow yaml file.

Action: a custom application that performs a typically complex frequently repeated task.

You can build your own Actions, use the official or community Actions.

In order to use an Action, we must use the word uses: instead of run:

Expressions & Context Objects

See the docs:

Example:

$\{\{ toJSON(github) }}
$\{\{ github.ref_name }}

Event Filters and Activity Types

Activity Types

same docs as the events, just check the Activity types column of the event.

Structure:

# ...
on:
  <event>:
    types: [activity-type-1, activity-type-n]
  <another-event>:
# ...

example:

# ...
on:
  pull_request:
    types: 
      - opened
      - edited
  dispatch_workflow:
  # yeah, it's an empty key
# ...

Filters

Check the Workflow syntax doc. For example this part for pull_request filters.

Example:

# ...
on:
  push:
    branches:
      - main
      - 'dev-*'
      - 'feat/**' # works like shell globbing
    paths-ignore:
      # don't trigger the workflow for the following paths
      - '.github/workflows/*'
# ...

Check the Filter pattern cheat sheet, it's quite handy.

Fork Pull Request Workflows

Note

By default, Pull Requests based on forks do NOT trigger a workflow.

Reasoning: prevent malicious code running on your runners.

Skipping workflows

doc

Commit messages with strings like this:

[skip ci]
[ci skip]
[no ci]
[skip actions]
[actions skip]

Job Outputs

Artifacts

Use these actions:

Job Outputs

Simple values. Typically used for reusing a value in different jobs.

Example: name of a file generated in a previous step.

# ...
build
  # ...
  outputs:
    # <identifier>: ${{ steps.$ID.outputs.$SOMETHING }}
    script-file: ${{ steps.publish.outputs.script-file }}
  steps:
    - name: publish JS filename
      id: publish
      run: find *.js -type f -execdir echo 'script-file={}' >> $GITHUB_OUTPUT ';'
    # ...
      
  deploy:
    - name: output filename
    - run: echo "${{ needs.build.outputs.script-file }}"
  

Useful docs:

Caching dependencies

Check the docs

jobs:
  test:
    # ...
    steps:
      - name: get code
        # ...
        # cache needs to be right before installing deps
      - name: cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: deps-node-modules-$\{\{ hashFiles('**/package-lock.json') }}
      - name install dependencies
        run: npm ci
      # ...
  build:
    needs: test
    # ...
    steps:
      # yeah, we need to repeat the same yaml code... :(
      - name: get code
        # ...
        # cache needs to be right before installing deps
      - name: cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: deps-node-modules-$\{\{ hashFiles('**/package-lock.json') }}
      - name install dependencies
        run: npm ci

Environment Variables and Secrets

Environment Variables

See also: default environment variables

name: <workflow-name>

on: <events>

env: # variables available for all jobs
  <key>: <value>

jobs:
  <job-name>:
    env: # variables available only for this job
      <key>: <value>
      
    steps:
      - name: <step-name>
        env: # variables available only for this step
        # ...

Secrets

Secrets can be stored on repository-level or via Environments (in the repo Settings tab).

Secrets can be refereenced via the ${{ secrets }} context object.

Controlling Workflow and Job Execution (conditionals)

video

Special conditional functions (doc)

Note that the default behavior is to interrupt execution if any previous step fails, so if we want to run something after a failure, we need to use if: failure() && ....

Example 1: upload report only if a previous job:

jobs:
  test:
    # ...
  lint:
    # ...
  report:
    # this 'needs:' is mandatory to specify which
    # job we want to check if failed.
    needs: [lint, test]
    if: failure()
    steps:
      # ...

Example 2: upload report only if a previous step in the same job failed:

jobs:
  test:
    steps:
      # ...
      - name: test code
        id: run-tests
        run: npm run test
      - name: upload test report
        # only runs if there's a failure and 'run-tests' fails
        if: failure() && steps.run-tests.outcome == 'failure'
        # doc:
        # https://docs.github.com/en/actions/learn-github-actions/contexts#steps-context
        uses: actions/upload-artifact@v3
        with:
          name: test-report
          path: test.json
# ...

Example 2: only install dependencies if there's no cache (info about cache-hit here)

jobs:
  test:
    steps:
      # ...
      - name cache dependencies
        id: cache
        uses: actions/cache@v3
        with:
          path: node_modules
          key: deps-node-modules-$\{\{ hashFiles('**/package-lock.json') }}
      - name: install dependencies
        # info about 'cache-hit': https://github.com/actions/cache#outputs
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm ci
      - name: test code
        run: npm run test
        # ...

Continue on Error

To flag a step as something where a failure is acceptable, just set the continue-on-error to true.

In technical terms, we can say that "continue-on-error forces the conclusion to be always success"

Example:

# ...
jobs:
  test:
    steps:
      # ...
      - name: test code
        continue-on-error: true
        run: npm run test # the job continues even if this fails.
        # ...

outcome vs. conclusion

Note: continue-on-error affects the conclusion status of a job.

From the docs:

steps.<step_id>.outcome

The result of a completed step before continue-on-error is applied.

Possible values are:

When a continue-on-error step fails, the outcome is failure, but the final conclusion is success.

steps.<step_id>.conclusion

[similar to outcome, but after continue-on-error is applied]

matrix

Example:

name: Matrix Demo
on: push
jobs:
  build:

    # if we don't use 'continue-on-error', a failure in any
    # job from the matrix combinations would cancel all the
    # incomplete ones.
    continue-on-error: true
  
    strategy:
      matrix:
        node-version: [12, 14, 16]
        operating-system: [ubuntu-latest, windows-latest]

    runs-on: ${{ matrix.operating-system }}
    
    steps:
      - name: get code
        uses: actions/checkout@v3
      - name: install NodeJS
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
      - name: install dependencies
        run: npm ci
      - name: build project
        run: npm run build

It's also possible to include/exclude specific combinations. Example:

    strategy:
      matrix:
        node-version: [12, 14, 16]
        operating-system: [ubuntu-latest, windows-latest]
        include:
          # only runs node 18 on ubuntu-latest
          - node-version: 18
            operating-system: ubuntu-latest
        exclude:
          # ignore node 12 on windows-latest
          - node-version: 12
            operating-system: windows-latest
        

Reusable Workflows

(aka "creating a module" in my own jargon)

Create a .github/workflows/reusable-deploy.yml:

name: Reusable Deploy

# here's the "secret"
on: workflow_call

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy project
        run: echo "Deploying & uploading..."

In another workflow file you can use:

# ...
jobs:
  # ...
  deploy:
    needs: build
    # it needs to be the path relative to the repository
    uses: ./.github/workflows/reusable-deploy.yml

adding inputs to reusable workflows

video

If our reusable-deploy.yml needs input from previous jobs, we can do something like this:

name: Reusable Deploy

on:
  workflow_call:
    inputs:
      # can be any name here
      artifact-name:
        description: The name of the deployable artifact files
        required: false # when set to false, define a 'default'
        # if the caller doesn't pass a 'with:', the default value
        # for ${{ inputs.artifact-name }} is 'dist'
        default: dist
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: get code
        uses: actions/download-artifact@v3
        with:
          name: ${{ inputs.artifact-name }}
      # ...

In the caller:

# ...
jobs:
  # ...
  steps:
    # ...
    deploy:
      needs: build
      uses: ./.github/workflows/reusable-deploy.yml
      with:
        artifact-name: dist-files

Jobs and Containers

didn't take any note... 😔

JavaScript Actions

name: 'Deploy to AWS S3'
description: 'Deploy a static website via AWS S3.'
runs:
  using: 'node16'