Fundamentals of GitHub Actions - Week 2

Saul HernandezSaul Hernandez
8 min read

In this post, I'll be sharing my journey through the GitHub Actions Bootcamp.

The purpose is to share all the knowledge learned during this bootcamp.

I'll be following the Github Actions Bootcamp. The main goal of this free initiative is to help developers to dominate GitHub Actions and prepare for the official GitHub Actions Certification.

In this bootcamp, I'll learn how to create and maintain automated workflows, develop custom actions, and manage GitHub Actions implementations in an enterprise environment.

YAML and Workflows

YAML means YAML Ain't Markup Language. It is a human-readable format and It's oriented to the creation of configuration files.

Characteristics

Key-Value format: It'a way to organise data. The key allows us to identify the associate value. Example: key: value

Data types: Supports various types such as booleans, strings, integers, floats, and more.

Indentation using spaces: It's a common way to organise code. We use one or more spaces at the beginning of each line to define structure and hierarchy.

Management of text multi-line: We can use separators for multi-line text blocks in order to ensure clarify and readability.

Workflows

In the deploy.yaml file, the workflow is named "Deploy to server". This will be triggered when changes are pushed to the repository (git push). This workflow contains two jobs, both running on Ubuntu OS. Each job have steps that print a message in the terminal using the echo command. The second job uses needs to ensure the execution only after the first job completes.

name: Deploy to server

on: push

jobs:
  first_job:
    runs-on: ubuntu-latest

    steps:
      - name: Hello World
        run: echo "Hello World!"

  second_job:
    needs: first_job
    runs-on: ubuntu-latest

    steps:
      - name: Hello World
        run: echo "Hello World! Second Job"

We can also use conditionals. In order to use it, we need to make use of contexts. Contexts are a way to access information about workflow runs, variables, and more. You can find the official documentation of contexts here.

We can access to contexts using the following syntax: ${{ <context> }}

For example, in the deploy.yaml file, we can use the if keyword to add a conditional to the second job. In this case, this job will be only executed when the GitHub reference is 'refs/heads/main'.

# ...
  second_job:
    if: ${{ github.ref == 'refs/heads/main' }}
    needs: first_job
    runs-on: ubuntu-latest

    steps:
      - name: Hello World
        run: echo "Hello World! Second Job"

If we need to define our own environment variables, we can use the env keyword. In this example, the first job uses a variable called NAME with the value "github". This value will be used in the echo command.

# ...
  first_job:
    runs-on: ubuntu-latest

    env:
      NAME: github

    steps:
      - name: Hello World
        run: echo "Hello World! You're name is $NAME"
# ...

We can use actions created by other users. To do this, we use the uses keyword under the steps section. In this example, I modified the first job to use the checkout action. The syntax to use an action is: uses: <username>/<action-name>@<version>

# ...
  first_job:
    runs-on: ubuntu-latest

    env:
      NAME: github

    steps:
      - name: Working dir
        run: ls -al

      - name: Action
        run: actions/checkout@v4

      - name: Working dir after action
        run: ls -al
# ...

It's possible to share information between steps by using the $GITHUB_OUTPUT variable. To do this, we need to assign an id to the step and use the following run command to save the data: run: echo "<variable-name>=<value>" >> "$GITHUB_OUTPUT"

In order to get the stored data, we need the use of the steps context. The syntax to get the value is: ${{ steps.<step_id>.outputs.<variable-name> }}.

# ...
jobs:
  first_job:
    runs-on: ubuntu-latest

    steps:
      - name: Saving data
        id: step_1
        run: echo "test=hello" >> "$GITHUB_OUTPUT"

      - name: Get data
        run: echo "${{ steps.step_1.outputs.test }}"
# ...

Workflow artifacts

Artifacts allow to store data after a job has completed and share that data with another job in the same workflow. By default, Github stores artifacts for 90 days, but this period can be customized.

In this example, in the first_job, I create a step that generate a log file named test.log to store some information. Next, I add a step to upload this artifact using uses: actions/upload-artifact@v4, specifying some parameters such as artifact's name and the file path.

In the second_job, I use the needs keyword to ensure it runs only after the first job completes. Then, I create a step to download the artifact using uses: actions/download-artifact@v4 with the parameter name. Finally, I add a step that prints the contents of the artifact using cat test.log.

name: Deploy to server

on: push

jobs:
  first_job:
    runs-on: ubuntu-latest

    steps:
      - name: Creating file
        run: |
          echo "Test file. Hello world." >> test.log

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: test-file
          path: test.log

  second_job:
    runs-on: ubuntu-latest
    needs: first_job

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: test-file

      - name: Cat file
        run: cat test.log

Continuous Integration (CI) with GitHub Actions

It's a process where developers integrate changes frequently in the repository. Each integration triggers an automatic build and tests. It helps to detect errors as soon as possible and reduce conflicts between branches.

Benefits

  • Software delivery faster and more frequent.

  • Best quality in the software due to automatic tests.

  • Reduction of errors and conflicts.

  • More reliability in the deployments.

  • Early feedback.

Environment variables and Secrets

This variables are used in the workflows. It provide a way to pass data into the workflow without hardcoding in the yaml file.

Both type of variables works the same way. The difference is that secrets are encrypted in order to store sensitive information.

To create a new variable or secret on Github, We need to follow this steps:

  • Navigate to the repository on Github.

  • Go to Settings > Secrets & Variables and click Actions.

  • Select between Variables or Secrets tab.

  • Click on New Repository Value.

  • Enter a name for your variable and the value.

Service containers

We can use service containers to connect databases, memory caches, and other tool to the workflow. In this example, I added a Redis service using the following parameters: the image name and the exposed ports.

The first step in this job is to install redis-cli. After that, I validate the Redis service using the ping command. Next, I set up node using an action called setup-node in version 4 with the node-version parameter to specify the use of node version 18.

name: Deploy

on: push

jobs:
  first_job:
    runs-on: ubuntu-latest

    services:
      redis:
        image: redis
        ports: 
          - 6379:6379

    steps:
      - name: Install redis cli
        run: sudo apt-get update -y && sudo apt get redis-tools -y

      - name: Test redis
        run: redis-cli -h localhost -p 6379 ping

      - name: setup
        uses: actions/setup-node@v4
        with:
          node-version: "18"

Building, Testing and Deploying with Workflow Automation

The following exercise consists of a simple web page. It's a Node.js app that uses Express as a server and Jest as a testing framework. This project includes a build script that compiles the app and generates the final artifact. In this case a single html index. It also includes a test script that validate the page contains a specific string.

Inside the .github/workflows directory, I created a deploy.yaml file. This workflow will be executed whenever changes are pushed to the repository.

For the Build job, It runs on the latest Ubuntu OS and includes the following steps:

  1. Check out the repository code

  2. Set up Node.js, specifying version 18.

  3. Install all the project dependencies.

  4. Run tests to ensure everything passes.

  5. Build the page

  6. Upload the static files as an artifact using the upload-pages-artifact action (version 3), specifying the directory where the static files are located.

For the Deploy job, I use the needs keyword to ensure it runs only after the Build job completes. It also runs on the Ubuntu OS latest version.

I set the required permissions to enable GitHub pages deployment, also I define environment variables for Github Pages, such as the deployment name and URL.

This job contains a single step that is responsible for deploying the page using the actions/deploy-pages action (version 4), with the job ID set to deployment-pages.

For the Notify job, I use the needs keyword to ensure their execution only after the Deploy job completes. It runs on the Ubuntu OS latest version and is configured to execute even if the previous job fails or is canceled.

This job only needs a single step to send a notification to Slack. It uses the action-slack action (version 3) with parameters such as status and the fields you want to include in the notification.

Additionally, I added a secret variable named SLACK, which value is the Webhook URL provided by Slack. In order to pass this value to the action, I define an environment variable called SLACK_WEBHOOK_URL that is required to send the notifications.

name: Deploy

on: push

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Check out the repository
        uses: actions/checkout@v4

      - name: Setup of Node
        uses: actions/setup-node@v4
        with:
          node-version: "18"

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm run test

      - name: Build page
        run: npm run build

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: public/

  deploy:
    runs-on: ubuntu-latest
    needs: build

    # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
    permissions:
      pages: write
      id-token: write

    # Deploy to the github-pages environment
    environment:
      name: github-pages
      url: ${{ steps.deployment-pages.outputs.page_url }}

    steps:
      - name: Deploy in Github Pages
        id: deployment-pages
        uses: actions/deploy-pages@v4

  notify:
    runs-on: ubuntu-latest
    needs: deploy
    if: always()

    steps:
      - name: Notify to Slack
        uses: 8398a7/action-slack@v3
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK }}
        with:
          status: ${{ job.status }}
          fields: repo,commit,author,job,took

Conclusion

This week, I learned about YAML, Workflows, Artifacts and CI with GitHub Actions! I started with the fundamental concepts with the purpose of understand how to craft powerful workflows. I've learned how to manage artifacts, ensuring our builds and deployments run smoothly. As a practical outcome, I built a workflow that builds the project, deploys it to GitHub Pages, and even sends notifications to Slack.

0
Subscribe to my newsletter

Read articles from Saul Hernandez directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Saul Hernandez
Saul Hernandez

Continuously learning and growing in software development, with experience in web, mobile, and SQL. I love sharing my journey and exploring innovation in tech! ๐Ÿš€