├── ci
├── deploy-app.sh
├── unit-test-app.sh
├── build-app.sh
├── performance-test.sh
├── component-test.sh
├── push-docker.sh
├── build-docker.sh
├── print_summary.py
└── format_summary.py
├── labs
├── setup
│ ├── start
│ │ └── main.yml
│ └── done
│ │ └── main.yml
├── img
│ ├── secret.png
│ ├── dockerhub.png
│ ├── pipeline.png
│ ├── workflow.png
│ ├── run-workflow.png
│ ├── workflow-result.png
│ ├── workflow-secret.png
│ ├── github-container.png
│ ├── storing-artifact.png
│ ├── workflow-yourself.png
│ ├── create-organization.png
│ ├── secrets-and-variables.png
│ └── secrets-and-variables copy.png
├── build-app
│ ├── done
│ │ └── main.yml
│ └── start
│ │ └── main.yml
├── extend-pipeline
│ ├── start
│ │ └── main.yml
│ └── done
│ │ └── main.yml
├── storing-artifacts
│ ├── start
│ │ └── main.yml
│ └── done
│ │ └── main.yml
├── docker-image
│ ├── start
│ │ └── main.yml
│ └── done
│ │ └── main.yml
├── exercise-template.md
├── selfhosted-runner
│ ├── hello-world-unix.yml
│ └── hello-world-windows.yml
├── systems-test
│ ├── start
│ │ └── main.yml
│ └── done
│ │ └── main.yml
├── extend-pipeline.md
├── systems-test.md
├── build-cache.md
├── matrix-builds.md
├── composite-action.md
├── setup.md
├── reusable-workflow.md
├── build-app.md
├── starter-workflows-and-organization-sharing.md
├── pr-workflow.md
├── selfhosted-runner.md
├── old-labs
│ └── reusable.md
├── concurrency.md
├── docker-image.md
├── storing-artifacts.md
└── docker-action.md
├── settings.gradle
├── TRAINER.md
├── app
├── gradle.properties
├── settings.gradle
├── src
│ ├── main
│ │ ├── resources
│ │ │ ├── application.yml
│ │ │ └── logback.xml
│ │ └── java
│ │ │ └── example
│ │ │ └── micronaut
│ │ │ ├── Application.java
│ │ │ ├── ReadyController.java
│ │ │ ├── RootController.java
│ │ │ └── HelloController.java
│ └── test
│ │ └── java
│ │ └── example
│ │ └── micronaut
│ │ └── HelloControllerTest.java
├── micronaut-cli.yml
├── .gitignore
├── Dockerfile
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── Dockerfile-multistage
├── build.gradle
├── gradlew.bat
└── gradlew
├── component-test
├── requirements.txt
├── docker-compose.yml
├── Dockerfile
└── test_app.py
├── img
├── secrets.png
├── dockerhub.png
├── pr-chooser.png
├── template.png
├── hello-world.png
├── actions-checks.png
└── branch-protection.png
├── .markdownlint.json
├── trainer
└── .github
│ └── workflows
│ ├── hello-world.yaml
│ ├── build-app.yaml
│ ├── extend-app.yaml
│ ├── build-and-test.yaml
│ ├── reuse.yml
│ ├── matrix.yaml
│ ├── matrix2.yaml
│ ├── mixed-actions.yaml
│ ├── reusable.yml
│ ├── storing-artifacts.yaml
│ ├── docker-image.yaml
│ ├── component-test.yaml
│ ├── systems-tests.yaml
│ ├── pr-workflow.yaml
│ ├── caching.yaml
│ └── trivy-scan.yaml
├── helper-scripts
└── generate-tree.sh
├── performance-test
├── docker-compose.yml
└── single-request.js
├── .gitignore
├── LICENSE
└── README.md
/ci/deploy-app.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/labs/setup/start/main.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include 'app'
--------------------------------------------------------------------------------
/TRAINER.md:
--------------------------------------------------------------------------------
1 | # Instructions
2 |
--------------------------------------------------------------------------------
/app/gradle.properties:
--------------------------------------------------------------------------------
1 | micronautVersion=1.2.7
--------------------------------------------------------------------------------
/app/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name="app"
--------------------------------------------------------------------------------
/component-test/requirements.txt:
--------------------------------------------------------------------------------
1 | requests==2.31.0
2 |
--------------------------------------------------------------------------------
/ci/unit-test-app.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -e
3 | gradle test -p app
4 |
--------------------------------------------------------------------------------
/ci/build-app.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | gradle clean shadowjar -p app
4 |
--------------------------------------------------------------------------------
/img/secrets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/img/secrets.png
--------------------------------------------------------------------------------
/img/dockerhub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/img/dockerhub.png
--------------------------------------------------------------------------------
/img/pr-chooser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/img/pr-chooser.png
--------------------------------------------------------------------------------
/img/template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/img/template.png
--------------------------------------------------------------------------------
/img/hello-world.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/img/hello-world.png
--------------------------------------------------------------------------------
/labs/img/secret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/secret.png
--------------------------------------------------------------------------------
/img/actions-checks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/img/actions-checks.png
--------------------------------------------------------------------------------
/labs/img/dockerhub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/dockerhub.png
--------------------------------------------------------------------------------
/labs/img/pipeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/pipeline.png
--------------------------------------------------------------------------------
/labs/img/workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/workflow.png
--------------------------------------------------------------------------------
/app/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | micronaut:
2 | server:
3 | port: 8000
4 | application:
5 | name: app
6 |
--------------------------------------------------------------------------------
/img/branch-protection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/img/branch-protection.png
--------------------------------------------------------------------------------
/labs/img/run-workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/run-workflow.png
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "MD013": false,
3 | "MD033": {
4 | "allowed_elements": ["details", "summary"]
5 | }
6 | }
--------------------------------------------------------------------------------
/labs/img/workflow-result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/workflow-result.png
--------------------------------------------------------------------------------
/labs/img/workflow-secret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/workflow-secret.png
--------------------------------------------------------------------------------
/app/micronaut-cli.yml:
--------------------------------------------------------------------------------
1 | profile: service
2 | defaultPackage: example.micronaut
3 | ---
4 | testFramework: junit
5 | sourceLanguage: java
--------------------------------------------------------------------------------
/labs/img/github-container.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/github-container.png
--------------------------------------------------------------------------------
/labs/img/storing-artifact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/storing-artifact.png
--------------------------------------------------------------------------------
/labs/img/workflow-yourself.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/workflow-yourself.png
--------------------------------------------------------------------------------
/labs/img/create-organization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/create-organization.png
--------------------------------------------------------------------------------
/labs/img/secrets-and-variables.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/secrets-and-variables.png
--------------------------------------------------------------------------------
/labs/img/secrets-and-variables copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eficode-academy/github-actions-katas/HEAD/labs/img/secrets-and-variables copy.png
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | Thumbs.db
2 | .DS_Store
3 | .gradle
4 | build/
5 | target/
6 | out/
7 | .idea
8 | *.iml
9 | *.ipr
10 | *.iws
11 | .project
12 | .settings
13 | .classpath
--------------------------------------------------------------------------------
/trainer/.github/workflows/hello-world.yaml:
--------------------------------------------------------------------------------
1 | name: hello-world
2 | on: push
3 | jobs:
4 | my-job:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: my-step
8 | run: echo "Hello World!"
--------------------------------------------------------------------------------
/app/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM adoptopenjdk/openjdk11-openj9:alpine-slim
2 | LABEL author="Sofus Albertsen"
3 | COPY build/libs/app-*-all.jar app.jar
4 | EXPOSE 8000
5 | CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar app.jar
6 |
--------------------------------------------------------------------------------
/helper-scripts/generate-tree.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Generate project directory tree and save to file
4 | echo "::group::The repository $GITHUB_REPOSITORY contains the following files"
5 | tree > tree.txt
6 | cat tree.txt
7 | echo "::endgroup::"
--------------------------------------------------------------------------------
/component-test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | test:
3 | build: component-test
4 | environment:
5 | SERVICE_URL: http://web:8000
6 | depends_on:
7 | - web
8 | web:
9 | image: ghcr.io/${github_username}micronaut-app:latest
--------------------------------------------------------------------------------
/component-test/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY requirements.txt ./
6 | RUN pip install --no-cache-dir -r requirements.txt
7 |
8 | COPY test_app.py test_app.py
9 |
10 | CMD [ "python", "./test_app.py" ]
11 |
--------------------------------------------------------------------------------
/app/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/app/src/main/java/example/micronaut/Application.java:
--------------------------------------------------------------------------------
1 | package example.micronaut;
2 |
3 | import io.micronaut.runtime.Micronaut;
4 |
5 | public class Application {
6 |
7 | public static void main(String[] args) {
8 | Micronaut.run(Application.class);
9 | }
10 | }
--------------------------------------------------------------------------------
/trainer/.github/workflows/build-app.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone-down
9 | uses: actions/checkout@v4
10 | - run: ci/build-app.sh
--------------------------------------------------------------------------------
/labs/build-app/done/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
--------------------------------------------------------------------------------
/labs/extend-pipeline/start/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
--------------------------------------------------------------------------------
/performance-test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | test:
3 | image: grafana/k6:0.37.0
4 | depends_on:
5 | - web
6 | command: run -u 10 -d 30s /home/k6/script.js -q
7 | volumes:
8 | - "./performance-test/single-request.js:/home/k6/script.js"
9 | web:
10 | image: ghcr.io/${github_username}micronaut-app:latest
11 |
--------------------------------------------------------------------------------
/ci/performance-test.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | if [[ -z "${github_username}" ]]; then
3 | echo "ERROR: github_username must be set in the environment"
4 | exit 1
5 | fi
6 | [[ -z "${github_username}" ]] || DockerRepo="${github_username}/"
7 | github_username=$DockerRepo docker compose -f performance-test/docker-compose.yml --project-directory . -p ci up --build --exit-code-from test
--------------------------------------------------------------------------------
/labs/extend-pipeline/done/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
12 | - name: Test
13 | run: ci/unit-test-app.sh
--------------------------------------------------------------------------------
/labs/storing-artifacts/start/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
12 | - name: Test
13 | run: ci/unit-test-app.sh
--------------------------------------------------------------------------------
/trainer/.github/workflows/extend-app.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
12 | - name: Test
13 | run: ci/unit-test-app.sh
--------------------------------------------------------------------------------
/trainer/.github/workflows/build-and-test.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
12 | - name: Test
13 | run: ci/unit-test-app.sh
--------------------------------------------------------------------------------
/trainer/.github/workflows/reuse.yml:
--------------------------------------------------------------------------------
1 | name: Reuse other workflow
2 |
3 | on: [workflow_dispatch]
4 | jobs:
5 | call-workflow:
6 | uses: ./.github/workflows/reusable.yml
7 | with:
8 | who-to-greet: '@sofusalbertsen'
9 | use-output:
10 | runs-on: ubuntu-latest
11 | needs: [call-workflow]
12 | steps:
13 | - run: echo "Time was ${{ needs.call-workflow.outputs.current-time }}"
--------------------------------------------------------------------------------
/trainer/.github/workflows/matrix.yaml:
--------------------------------------------------------------------------------
1 | name: matrix-example
2 | on: [workflow_dispatch]
3 |
4 | jobs:
5 | job:
6 | runs-on: ubuntu-latest
7 | strategy:
8 | matrix:
9 | container: ["ubuntu:bionic", "fedora:31", "opensuse/leap:42.3", "centos:8"]
10 | container:
11 | image: ${{ matrix.container }}
12 | steps:
13 | - name: check os
14 | run: cat /etc/os-release
--------------------------------------------------------------------------------
/app/Dockerfile-multistage:
--------------------------------------------------------------------------------
1 | FROM gradle:6-jdk11 as builder
2 | COPY --chown=gradle:gradle . /home/gradle/src
3 | WORKDIR /home/gradle/src
4 | RUN gradle shadowjar
5 |
6 | FROM adoptopenjdk/openjdk11-openj9:alpine-slim
7 | LABEL author="Sofus Albertsen"
8 | COPY --from=builder /home/gradle/src/build/libs/app-*-all.jar app.jar
9 | EXPOSE 8000
10 | CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar app.jar
--------------------------------------------------------------------------------
/app/src/main/java/example/micronaut/ReadyController.java:
--------------------------------------------------------------------------------
1 | package example.micronaut;
2 |
3 | import io.micronaut.http.MediaType;
4 | import io.micronaut.http.annotation.Controller;
5 | import io.micronaut.http.annotation.Get;
6 | import io.micronaut.http.annotation.Produces;
7 |
8 | @Controller("/status")
9 | public class ReadyController{
10 | @Get("/")
11 | @Produces(MediaType.TEXT_PLAIN)
12 | public String index() {
13 | return "Up and running";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/example/micronaut/RootController.java:
--------------------------------------------------------------------------------
1 | package example.micronaut;
2 |
3 | import io.micronaut.http.MediaType;
4 | import io.micronaut.http.annotation.Controller;
5 | import io.micronaut.http.annotation.Get;
6 | import io.micronaut.http.annotation.Produces;
7 |
8 | @Controller("/")
9 | public class RootController{
10 | @Get("/")
11 | @Produces(MediaType.TEXT_HTML)
12 | public String index() {
13 | return "
Hello World
";
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 | complete/bin
25 | .idea*
26 | .gradle/*
27 | app/bin/
28 | .vscode/settings.json
29 | .DS_Store
30 | labs/.DS_Store
31 |
--------------------------------------------------------------------------------
/ci/component-test.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | if [[ -z "${github_username}" ]]; then
3 | echo "ERROR: github_username must be set in the environment"
4 | exit 1
5 | fi
6 | # Pass the github_username through to docker-compose as the variable the compose file expects
7 | # Compose references the username directly; include trailing slash in the value
8 | [[ -z "${github_username}" ]] || DockerRepo="${github_username}/"
9 | github_username=$DockerRepo docker compose -f component-test/docker-compose.yml --project-directory . -p ci up --build --exit-code-from test
--------------------------------------------------------------------------------
/trainer/.github/workflows/matrix2.yaml:
--------------------------------------------------------------------------------
1 | name: Matrix workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | matrix:
8 | container: ["gradle:6-jdk8", "gradle:6-jdk11", "gradle:6-jdk17"]
9 | container:
10 | image: ${{ matrix.container }}
11 |
12 | steps:
13 | - name: Clone down repository
14 | uses: actions/checkout@v4
15 | - name: Build application
16 | run: ci/build-app.sh
17 | - name: Test
18 | run: ci/unit-test-app.sh
--------------------------------------------------------------------------------
/ci/push-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | if [[ -z "${github_username}" ]]; then
4 | echo "ERROR: github_username must be set in the environment"
5 | exit 1
6 | fi
7 | if [[ -z "${github_password}" ]]; then
8 | echo "ERROR: github_password must be set in the environment"
9 | exit 1
10 | fi
11 | echo "${github_password}" | docker login ghcr.io --username "${github_username}" --password-stdin
12 | docker push "ghcr.io/${github_username}/micronaut-app:1.0-${GIT_COMMIT::8}"
13 | docker push "ghcr.io/${github_username}/micronaut-app:latest" &
14 | wait
15 |
--------------------------------------------------------------------------------
/app/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 |
7 |
8 | %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/component-test/test_app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import unittest
4 | import os
5 | import requests
6 | import re
7 | import time
8 |
9 |
10 | class TestName(unittest.TestCase):
11 | url = os.getenv('SERVICE_URL', 'http://127.0.0.1:8000')
12 | time.sleep(10)
13 | def test_status(self):
14 | url = self.url + "/status"
15 | response = requests.get(url, timeout=1)
16 | self.assertEqual(response.status_code, 200)
17 | self.assertEqual(response.encoding, 'ISO-8859-1')
18 | self.assertTrue(re.match('Up and running', response.text))
19 |
20 |
21 | if __name__ == '__main__':
22 | unittest.main()
23 |
--------------------------------------------------------------------------------
/app/src/main/java/example/micronaut/HelloController.java:
--------------------------------------------------------------------------------
1 | package example.micronaut;
2 |
3 | import io.micronaut.http.MediaType;
4 | import io.micronaut.http.annotation.Controller;
5 | import io.micronaut.http.annotation.Get;
6 | import io.micronaut.http.annotation.Produces;
7 |
8 | @Controller("/hello")
9 | public class HelloController{
10 | @Get("/{name}")
11 | @Produces(MediaType.TEXT_PLAIN)
12 | public String index(String name) {
13 | return combineName(name);
14 | }
15 |
16 | //Example of a function that can be tested through normal unit frameworks
17 | public String combineName( String name) {
18 | return "Hello "+name;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ci/build-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | # Ensure github_username is provided
4 | if [[ -z "${github_username:-}" ]]; then
5 | echo "ERROR: github_username must be set in the environment"
6 | exit 1
7 | fi
8 | # Strip any trailing slashes from the username to avoid ghcr.io// when empty or misformatted
9 | USER="${github_username%/}"
10 | if [[ -z "${USER}" ]]; then
11 | echo "ERROR: github_username is empty after trimming; please set a valid username (lowercase)"
12 | exit 1
13 | fi
14 | [[ -z "${GIT_COMMIT:-}" ]] && Tag='local' || Tag="${GIT_COMMIT::8}"
15 | REPO="ghcr.io/${USER}/"
16 | echo "Using repository prefix: ${REPO}"
17 | docker build -t "${REPO}micronaut-app:latest" -t "${REPO}micronaut-app:1.0-$Tag" app/
18 |
--------------------------------------------------------------------------------
/trainer/.github/workflows/mixed-actions.yaml:
--------------------------------------------------------------------------------
1 | name: Mixed actions
2 | on: [workflow_dispatch]
3 | jobs:
4 | github-action:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: checkout
8 | uses: actions/checkout@v4 # from https://github.com/marketplace/actions/checkout
9 | verified-action:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: install dapr
13 | uses: dapr/setup-dapr@v1 # from https://github.com/marketplace/actions/dapr-tool-installer
14 | with:
15 | version: 1.2.0 # default is 1.2.0
16 | id: install
17 | non-verified-action:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Close issues
21 | uses: mxie/close-outdated-issues-action@main # from https://github.com/mxie/close-outdated-issues-action/blob/main/README.md
22 | with:
23 | token: ${{ secrets.GITHUB_TOKEN }}
24 | days-before: 30
--------------------------------------------------------------------------------
/trainer/.github/workflows/reusable.yml:
--------------------------------------------------------------------------------
1 | name: Reusable workflow
2 | on:
3 | workflow_call:
4 | inputs:
5 | who-to-greet:
6 | description: 'The person to greet'
7 | type: string
8 | required: true
9 | default: World
10 | outputs:
11 | current-time:
12 | description: 'The time when greeting.'
13 | value: ${{ jobs.reusable-job.outputs.current-time }}
14 | jobs:
15 | reusable-job:
16 | runs-on: ubuntu-latest
17 | outputs:
18 | current-time: ${{ steps.time.outputs.time }}
19 | steps:
20 | - name: Greet someone
21 | run: echo "Hello ${{ inputs.who-to-greet }}"
22 | - name: Set time
23 | id: time
24 | run: echo "time=$(date)" >> $GITHUB_OUTPUT
25 | another-job:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: another echo
29 | run: echo "another line"
30 | depending-job:
31 | needs: [reusable-job,another-job]
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: another echo
35 | run: echo "another line"
36 |
--------------------------------------------------------------------------------
/labs/docker-image/start/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
12 | - name: Test
13 | run: ci/unit-test-app.sh
14 | - name: Upload repo
15 | uses: actions/upload-artifact@v4
16 | with:
17 | name: code
18 | path: .
19 | include-hidden-files: true
20 |
21 | Linting:
22 | runs-on: ubuntu-latest
23 | needs: [Build]
24 | steps:
25 | - name: Download code
26 | uses: actions/download-artifact@v4
27 | with:
28 | name: code
29 | path: .
30 | - name: run linting
31 | uses: super-linter/super-linter/slim@v7
32 | env:
33 | DEFAULT_BRANCH: main
34 | # To report GitHub Actions status checks
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | DISABLE_ERRORS: true
--------------------------------------------------------------------------------
/labs/storing-artifacts/done/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
12 | - name: Test
13 | run: ci/unit-test-app.sh
14 | - name: Upload repo
15 | uses: actions/upload-artifact@v4
16 | with:
17 | name: code
18 | path: .
19 | include-hidden-files: true
20 |
21 | Linting:
22 | runs-on: ubuntu-latest
23 | needs: [Build]
24 | steps:
25 | - name: Download code
26 | uses: actions/download-artifact@v4
27 | with:
28 | name: code
29 | path: .
30 | - name: run linting
31 | uses: super-linter/super-linter/slim@v7
32 | env:
33 | DEFAULT_BRANCH: main
34 | # To report GitHub Actions status checks
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | DISABLE_ERRORS: true
--------------------------------------------------------------------------------
/trainer/.github/workflows/storing-artifacts.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 | container: gradle:6-jdk11
7 | steps:
8 | - name: Clone down repository
9 | uses: actions/checkout@v4
10 | - name: Build application
11 | run: ci/build-app.sh
12 | - name: Test
13 | run: ci/unit-test-app.sh
14 | - name: Upload repo
15 | uses: actions/upload-artifact@v4
16 | with:
17 | name: code
18 | path: .
19 | include-hidden-files: true
20 | Linting:
21 | runs-on: ubuntu-latest
22 | needs: [Build]
23 | steps:
24 | - name: Download code
25 | uses: actions/download-artifact@v4
26 | with:
27 | name: code
28 | path: .
29 | - name: run linting
30 | uses: super-linter/super-linter/slim@v7
31 | env:
32 | DEFAULT_BRANCH: main
33 | # To report GitHub Actions status checks
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 | DISABLE_ERRORS: true
--------------------------------------------------------------------------------
/labs/exercise-template.md:
--------------------------------------------------------------------------------
1 | # Template headline
2 |
3 | ## Learning Goals
4 |
5 | - provide a list of goals to learn here
6 |
7 | ## Introduction
8 |
9 | Here you will provide the bare minimum of information people need to solve the exercise.
10 |
11 | ## Subsections
12 |
13 | You can have several subsections if needed.
14 |
15 |
16 | :bulb: If an explanation becomes too long, the more detailed parts can be encapsulated in a drop down section
17 |
18 |
19 | ## Exercise
20 |
21 | ### Overview
22 |
23 | - In bullets, what are you going to solve as a student
24 |
25 | ### Step by step instructions
26 |
27 |
28 | More Details
29 |
30 | ### Repeat the same bullet names as above and put them in to illustrate how far the student have gone
31 |
32 | - all actions that you believe the student should do, should be in a bullet
33 |
34 | > :bulb: Help can be illustrated with bulbs in order to make it easy to distinguish.
35 |
36 |
37 |
38 | ### Clean up
39 |
40 | If anything needs cleaning up, here is the section to do just that.
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Sofus Albertsen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Github Actions katas
2 |
3 | ## Introduction
4 |
5 | This repository contains a set of exercises to learn Github Actions.
6 |
7 | ### Exercises
8 |
9 | * [Setup](./labs/setup.md)
10 | * [Build app](./labs/build-app.md)
11 | * [Extending the Pipeline](./labs/extend-pipeline.md)
12 | * [Storing Artifacts](./labs/storing-artifacts.md)
13 | * [Building Docker images](./labs/docker-image.md)
14 | * [Systems test](./labs/systems-test.md)
15 | * [Reusable workflows](./labs/reusable-workflow.md)
16 | * [Pull Request based workflow](./labs/pr-workflow.md)
17 | * [Build app on multiple environments](./labs/matrix-builds.md)
18 |
19 | ### Rough exercises (not yet ready)
20 |
21 | * [Reusing build cache](./labs/build-cache.md)
22 | * [Selfhosted runners](./labs/selfhosted-runner.md)
23 |
24 | ## Resources
25 |
26 | * [Understand a workflow file](https://docs.github.com/en/actions/learn-github-actions/introduction-to-github-actions#understanding-the-workflow-file)
27 | * [List of starter workflow files for many different languages](https://github.com/actions/starter-workflows/tree/main/ci)
28 | * [A curated list of awesome things related to GitHub Actions](https://github.com/sdras/awesome-actions)
29 | * [Githubs own Hands On Labs](https://github.com/ps-actions-sandbox/ActionsFundamentals)
30 |
--------------------------------------------------------------------------------
/labs/setup/done/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the "main" branch
8 | push:
9 | branches: [ "main" ]
10 | pull_request:
11 | branches: [ "main" ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | # Steps represent a sequence of tasks that will be executed as part of the job
24 | steps:
25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26 | - uses: actions/checkout@v4
27 |
28 | # Runs a single command using the runners shell
29 | - name: Run a one-line script
30 | run: echo Hello, world!
31 |
32 | # Runs a set of commands using the runners shell
33 | - name: Run a multi-line script
34 | run: |
35 | echo Add other actions to build,
36 | echo test, and deploy your project.
37 |
--------------------------------------------------------------------------------
/labs/build-app/start/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the "main" branch
8 | push:
9 | branches: [ "main" ]
10 | pull_request:
11 | branches: [ "main" ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | # Steps represent a sequence of tasks that will be executed as part of the job
24 | steps:
25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26 | - uses: actions/checkout@v4
27 |
28 | # Runs a single command using the runners shell
29 | - name: Run a one-line script
30 | run: echo Hello, world!
31 |
32 | # Runs a set of commands using the runners shell
33 | - name: Run a multi-line script
34 | run: |
35 | echo Add other actions to build,
36 | echo test, and deploy your project.
37 |
--------------------------------------------------------------------------------
/app/src/test/java/example/micronaut/HelloControllerTest.java:
--------------------------------------------------------------------------------
1 | package example.micronaut;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertNotNull;
5 |
6 | import io.micronaut.http.HttpRequest;
7 | import io.micronaut.http.client.RxHttpClient;
8 | import io.micronaut.http.client.annotation.Client;
9 | import io.micronaut.test.annotation.MicronautTest;
10 | import org.junit.jupiter.api.Test;
11 |
12 | import javax.inject.Inject;
13 |
14 | @MicronautTest
15 | public class HelloControllerTest {
16 |
17 | @Inject
18 | @Client("/")
19 | RxHttpClient client;
20 | //Test spins up an entire server and client to perform the test
21 |
22 | @Test
23 | public void testHello() {
24 | HttpRequest request = HttpRequest.GET("/hello/sofus");
25 | String body = client.toBlocking().retrieve(request);
26 |
27 | assertNotNull(body);
28 | assertEquals("Hello sofus", body);
29 | }
30 | @Test
31 | public void testCombineName() {
32 | String name = "Sonny";
33 | HelloController sut = new HelloController();
34 | System.out.println("testing");
35 | assertEquals("Hello "+name, sut.combineName(name),"Name and greeting not properly combined");
36 |
37 |
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/labs/selfhosted-runner/hello-world-unix.yml:
--------------------------------------------------------------------------------
1 | name: Hello Selfhosted Runner
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | hello-world:
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout code
12 | uses: actions/checkout@v4
13 |
14 | - name: Display environment info
15 | run: |
16 | echo "Hello from a self-hosted runner!"
17 | echo "Current user: $(whoami)"
18 | echo "Operating system: $(uname -s)"
19 | echo "Hostname: $(hostname)"
20 | echo "Current directory: $(pwd)"
21 | echo "Shell: $SHELL"
22 |
23 | - name: Simple shell script test
24 | run: |
25 | echo "Running a simple shell script..."
26 | echo "Creating a test file..."
27 | echo "Hello from GitHub Actions!" > hello.txt
28 | cat hello.txt
29 | rm hello.txt
30 | echo "Test file created and cleaned up successfully!"
31 |
32 | - name: Check available commands
33 | run: |
34 | echo "Checking if common commands are available:"
35 | which git && echo "✓ Git is available" || echo "✗ Git not found"
36 | which curl && echo "✓ Curl is available" || echo "✗ Curl not found"
37 | which node && echo "✓ Node.js is available" || echo "✗ Node.js not found"
38 | which python3 && echo "✓ Python3 is available" || echo "✗ Python3 not found"
--------------------------------------------------------------------------------
/performance-test/single-request.js:
--------------------------------------------------------------------------------
1 | import http from 'k6/http';
2 |
3 | import { sleep, check } from 'k6';
4 |
5 | import { Counter } from 'k6/metrics';
6 |
7 |
8 | // A simple counter for http requests
9 |
10 |
11 | export const requests = new Counter('http_reqs');
12 |
13 |
14 |
15 | export function setup() {
16 | // 2. setup code, you can pass data to VU and teardown
17 | sleep(3)
18 | return true
19 | }
20 |
21 |
22 | // you can specify stages of your test (ramp up/down patterns) through the options object
23 |
24 | // target is the number of VUs you are aiming for
25 |
26 |
27 | export const options = {
28 |
29 | stages: [
30 |
31 | { target: 20, duration: '1m' },
32 |
33 | { target: 15, duration: '1m' },
34 |
35 | { target: 0, duration: '1m' },
36 |
37 | ],
38 |
39 | thresholds: {
40 |
41 | requests: ['count < 100'],
42 |
43 | http_req_failed: ['rate<0.01'], // http errors should be less than 1%
44 |
45 | http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms
46 |
47 |
48 |
49 | },
50 |
51 | };
52 |
53 |
54 | export default function () {
55 |
56 | // our HTTP request, note that we are saving the response to res, which can be accessed later
57 |
58 |
59 | const res = http.get('http://web:8000/status');
60 |
61 |
62 | //sleep(1);
63 |
64 |
65 | const checkRes = check(res, {
66 |
67 | 'status is 200': (r) => r.status === 200,
68 |
69 | 'response body': (r) => r.body.indexOf('Up and running') !== -1,
70 |
71 | });
72 |
73 | }
--------------------------------------------------------------------------------
/ci/print_summary.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Simple helper used by the composite action exercise.
4 | Reads JSON from stdin (or a file path passed as arg) containing a list of numbers
5 | and prints a small summary: count, sum, average, min, max.
6 | """
7 | import sys
8 | import json
9 |
10 | def summarize(numbers: list[float]) -> dict:
11 | if not numbers:
12 | return {"count": 0, "sum": 0, "avg": None, "min": None, "max": None}
13 | total = sum(numbers)
14 | return {
15 | "count": len(numbers),
16 | "sum": total,
17 | "avg": total / len(numbers),
18 | "min": min(numbers),
19 | "max": max(numbers),
20 | }
21 |
22 |
23 | def main():
24 | data = None
25 | if len(sys.argv) > 1:
26 | try:
27 | with open(sys.argv[1], "r", encoding="utf-8") as f:
28 | data = json.load(f)
29 | except Exception as e:
30 | print(f"Failed to read {sys.argv[1]}: {e}", file=sys.stderr)
31 | sys.exit(2)
32 | else:
33 | try:
34 | data = json.load(sys.stdin)
35 | except Exception:
36 | print("No JSON input provided on stdin and no file given", file=sys.stderr)
37 | sys.exit(1)
38 |
39 | if not isinstance(data, list):
40 | print("Expected a JSON array of numbers", file=sys.stderr)
41 | sys.exit(3)
42 |
43 | try:
44 | numbers = [float(x) for x in data]
45 | except Exception:
46 | print("All items in the array must be numeric", file=sys.stderr)
47 | sys.exit(4)
48 |
49 | out = summarize(numbers)
50 | print(json.dumps(out))
51 |
52 |
53 | if __name__ == "__main__":
54 | main()
55 |
--------------------------------------------------------------------------------
/labs/selfhosted-runner/hello-world-windows.yml:
--------------------------------------------------------------------------------
1 | name: Hello Selfhosted Runner
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | hello-world:
8 | runs-on: windows-latest
9 |
10 | steps:
11 | - name: Checkout code
12 | uses: actions/checkout@v4
13 |
14 | - name: Display environment info
15 | shell: powershell
16 | run: |
17 | Write-Host "Hello from a self-hosted Windows runner!"
18 | Write-Host "Current user: $env:USERNAME"
19 | Write-Host "Computer name: $env:COMPUTERNAME"
20 | Write-Host "Operating system: $((Get-CimInstance Win32_OperatingSystem).Caption)"
21 | Write-Host "Current directory: $(Get-Location)"
22 | Write-Host "PowerShell version: $($PSVersionTable.PSVersion)"
23 |
24 | - name: Simple PowerShell script test
25 | shell: powershell
26 | run: |
27 | Write-Host "Running a simple PowerShell script..."
28 | Write-Host "Creating a test file..."
29 | "Hello from GitHub Actions!" | Out-File -FilePath "hello.txt"
30 | Get-Content "hello.txt"
31 | Remove-Item "hello.txt"
32 | Write-Host "Test file created and cleaned up successfully!"
33 |
34 | - name: Check available commands
35 | shell: powershell
36 | run: |
37 | Write-Host "Checking if common commands are available:"
38 | try { git --version; Write-Host "✓ Git is available" } catch { Write-Host "✗ Git not found" }
39 | try { curl --version; Write-Host "✓ Curl is available" } catch { Write-Host "✗ Curl not found" }
40 | try { node --version; Write-Host "✓ Node.js is available" } catch { Write-Host "✗ Node.js not found" }
41 | try { python --version; Write-Host "✓ Python is available" } catch { Write-Host "✗ Python not found" }
--------------------------------------------------------------------------------
/ci/format_summary.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Format and display a summary from a JSON file.
4 | Takes a JSON file path as a command-line argument.
5 | """
6 | import json
7 | import sys
8 | from typing import Any
9 |
10 |
11 | def load_json_file(file_path: str) -> dict[str, Any]:
12 | """Load and parse a JSON file with proper error handling.
13 |
14 | Args:
15 | file_path: Path to the JSON file to load
16 |
17 | Returns:
18 | The parsed JSON data as a dictionary
19 |
20 | Raises:
21 | SystemExit: On file or JSON parsing errors
22 | """
23 | try:
24 | with open(file_path, 'r', encoding='utf-8') as f:
25 | return json.load(f)
26 | except FileNotFoundError:
27 | print(f"Error: File '{file_path}' not found", file=sys.stderr)
28 | sys.exit(2)
29 | except json.JSONDecodeError as e:
30 | print(f"Error: Invalid JSON in '{file_path}': {e}", file=sys.stderr)
31 | sys.exit(2)
32 | except OSError as e:
33 | print(f"Error: Failed to read '{file_path}': {e}", file=sys.stderr)
34 | sys.exit(2)
35 |
36 |
37 | def main() -> None:
38 | """Load a number summary in json format and output a human readable string instead
39 | """
40 | if len(sys.argv) != 2:
41 | print("Usage: python format_summary.py ", file=sys.stderr)
42 | sys.exit(1)
43 |
44 | json_file = sys.argv[1]
45 | summary = load_json_file(json_file)
46 |
47 | if not isinstance(summary, dict) or summary.get('count', 0) == 0:
48 | print('No numbers provided')
49 | else:
50 | print(f"count={summary['count']}, sum={summary['sum']}, avg={summary['avg']:.2f}, min={summary['min']}, max={summary['max']}")
51 |
52 |
53 | if __name__ == "__main__":
54 | main()
--------------------------------------------------------------------------------
/labs/docker-image/done/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ github.actor }}
5 | github_password: ${{ secrets.GITHUB_TOKEN }} # Must be made available to the workflow
6 | GIT_COMMIT: ${{ github.sha }}
7 |
8 | jobs:
9 | Build:
10 | runs-on: ubuntu-latest
11 | container: gradle:6-jdk11
12 | steps:
13 | - name: Clone down repository
14 | uses: actions/checkout@v4
15 | - name: Build application
16 | run: ci/build-app.sh
17 | - name: Test
18 | run: ci/unit-test-app.sh
19 | - name: Upload repo
20 | uses: actions/upload-artifact@v4
21 | with:
22 | name: code
23 | path: .
24 | include-hidden-files: true
25 |
26 | Linting:
27 | runs-on: ubuntu-latest
28 | needs: [Build]
29 | steps:
30 | - name: Download code
31 | uses: actions/download-artifact@v4
32 | with:
33 | name: code
34 | path: .
35 | - name: run linting
36 | uses: super-linter/super-linter/slim@v7
37 | env:
38 | DEFAULT_BRANCH: main
39 | # To report GitHub Actions status checks
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | DISABLE_ERRORS: true
42 |
43 | Docker-image:
44 | runs-on: ubuntu-latest
45 | needs: [Build]
46 | permissions:
47 | packages: write
48 | steps:
49 | - name: Download code
50 | uses: actions/download-artifact@v4
51 | with:
52 | name: code
53 | path: .
54 | - name: Validate Docker username is all lowercase
55 | id: validate_lower
56 | run: |
57 | if [[ "${{ env.github_username }}" =~ [A-Z] ]]; then
58 | echo "::error::Validation Failed: GitHub username '${{ env.github_username }}' cannot contain uppercase characters."
59 | exit 1
60 | else
61 | echo "Docker username format is valid."
62 | fi
63 | shell: bash
64 | - name: build docker
65 | run: bash ci/build-docker.sh
66 | - name: push docker
67 | run: bash ci/push-docker.sh
--------------------------------------------------------------------------------
/labs/systems-test/start/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ github.actor }}
5 | github_password: ${{ secrets.GITHUB_TOKEN }} # Must be made available to the workflow
6 | GIT_COMMIT: ${{ github.sha }}
7 |
8 | jobs:
9 | Build:
10 | runs-on: ubuntu-latest
11 | container: gradle:6-jdk11
12 | steps:
13 | - name: Clone down repository
14 | uses: actions/checkout@v4
15 | - name: Build application
16 | run: ci/build-app.sh
17 | - name: Test
18 | run: ci/unit-test-app.sh
19 | - name: Upload repo
20 | uses: actions/upload-artifact@v4
21 | with:
22 | name: code
23 | path: .
24 | include-hidden-files: true
25 |
26 | Linting:
27 | runs-on: ubuntu-latest
28 | needs: [Build]
29 | steps:
30 | - name: Download code
31 | uses: actions/download-artifact@v4
32 | with:
33 | name: code
34 | path: .
35 | - name: run linting
36 | uses: super-linter/super-linter/slim@v7
37 | env:
38 | DEFAULT_BRANCH: main
39 | # To report GitHub Actions status checks
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | DISABLE_ERRORS: true
42 |
43 | Docker-image:
44 | runs-on: ubuntu-latest
45 | needs: [Build]
46 | permissions:
47 | packages: write
48 | steps:
49 | - name: Download code
50 | uses: actions/download-artifact@v4
51 | with:
52 | name: code
53 | path: .
54 | - name: Validate Docker username is all lowercase
55 | id: validate_lower
56 | run: |
57 | if [[ "${{ env.github_username }}" =~ [A-Z] ]]; then
58 | echo "::error::Validation Failed: GitHub username '${{ env.github_username }}' cannot contain uppercase characters."
59 | exit 1
60 | else
61 | echo "Docker username format is valid."
62 | fi
63 | shell: bash
64 | - name: build docker
65 | run: bash ci/build-docker.sh
66 | - name: push docker
67 | run: bash ci/push-docker.sh
--------------------------------------------------------------------------------
/labs/extend-pipeline.md:
--------------------------------------------------------------------------------
1 | # Extending the pipeline
2 |
3 | After the web service is built, we want to run the unit tests to check if it works as expected.
4 |
5 | This part is made as a recap of the previous exercise, to remind you how to add a step to the pipeline.
6 |
7 | ## Learning goals
8 |
9 | - Add a step to the pipeline
10 |
11 | ## Exercise
12 |
13 | ### Tasks
14 |
15 | - Open the file `.github/workflows/main.yml`.
16 | - In the job named `Build`, after the step named `Build application`, add a new step named `Test`.
17 | - The step should `run` the script `ci/unit-test-app.sh`. If you want to know what the script is doing,
18 | look into [the script](../ci/unit-test-app.sh).
19 |
20 | ## Solution
21 |
22 | If you need to see the whole _*Solution*_ you can extend the section below.
23 |
24 |
25 | Solution
26 |
27 | ```YAML
28 | name: Main workflow
29 | on: push
30 | jobs:
31 | Build:
32 | runs-on: ubuntu-latest
33 | container: gradle:6-jdk11
34 | steps:
35 | - name: Clone down repository
36 | uses: actions/checkout@v4
37 | - name: Build application
38 | run: ci/build-app.sh
39 | - name: Test
40 | run: ci/unit-test-app.sh
41 | ```
42 |
43 |
44 |
45 | ## Results
46 |
47 | If the exercise is completed correctly, the output of `Test` step will look similar to this:
48 |
49 | ``` bash
50 | > Task :compileJava UP-TO-DATE
51 | > Task :processResources UP-TO-DATE
52 | > Task :classes UP-TO-DATE
53 |
54 | > Task :compileTestJava
55 | Note: Creating bean classes for 1 type elements
56 |
57 | > Task :processTestResources NO-SOURCE
58 | > Task :testClasses
59 | > Task :test
60 |
61 | Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
62 | Use '--warning-mode all' to show the individual deprecation warnings.
63 | See https://docs.gradle.org/6.9.4/userguide/command_line_interface.html#sec:command_line_warnings
64 |
65 | BUILD SUCCESSFUL in 6s
66 | 4 actionable tasks: 2 executed, 2 up-to-date
67 | ```
68 |
69 | Great, now you have a pipeline that builds and tests your web service :tada:
70 |
--------------------------------------------------------------------------------
/labs/systems-test.md:
--------------------------------------------------------------------------------
1 | # Systems test
2 |
3 | After having our releasable component (our docker image), we can now run our system tests to promote the artifact.
4 |
5 | In the repository we have made two different system tests; component test and performance test.
6 |
7 | Both require your registry username and password as `env` variables in order to work. The workflow already has `github_username` and `github_password` environment variables defined for this purpose.
8 |
9 | ## Tasks
10 |
11 | - Add job named `Component-test`
12 | - Add a step to run `bash ci/component-test.sh` script.
13 | This will run a docker-compose file with component tests.
14 |
15 | ```YAML
16 | - name: Execute component test
17 | run: bash ci/component-test.sh
18 | ```
19 |
20 | - Ensure that the job is dependent on the `Docker-image` job.
21 |
22 | - Add another job named `Performance-test`
23 | - Add a step to run the `bash ci/performance-test.sh` script.
24 | This will run a docker-compose file with performance tests. (Same YAML structure as above)
25 | - It too needs to be dependent on `Docker-image` job.
26 |
27 | - Push the changes to GitHub and see that the tests are running.
28 |
29 | - Congratulations! You have now made a pipeline that builds, tests and releases your application!
30 |
31 | ### Solution
32 |
33 | If you struggle and need to see the whole ***Solution*** you can extend the section below.
34 |
35 |
36 | Solution
37 |
38 | ```YAML
39 | Component-test:
40 | runs-on: ubuntu-latest
41 | needs: Docker-image
42 | steps:
43 | - name: Download code
44 | uses: actions/download-artifact@v4
45 | with:
46 | name: code
47 | path: .
48 | - name: Execute component test
49 | run: bash ci/component-test.sh
50 | Performance-test:
51 | runs-on: ubuntu-latest
52 | needs: Docker-image
53 | steps:
54 | - name: Download code
55 | uses: actions/download-artifact@v4
56 | with:
57 | name: code
58 | path: .
59 | - name: Execute performance test
60 | run: bash ci/performance-test.sh
61 | ```
62 |
63 |
64 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "net.ltgt.apt-eclipse" version "0.21"
3 | id "com.github.johnrengelman.shadow" version "5.0.0"
4 | id "application"
5 | }
6 |
7 |
8 |
9 | version "0.1"
10 | group "example.micronaut"
11 |
12 | repositories {
13 | mavenCentral()
14 | maven { url "https://jcenter.bintray.com" }
15 | }
16 |
17 | configurations {
18 | // for dependencies that are needed for development only
19 | developmentOnly
20 | }
21 |
22 | dependencies {
23 | annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
24 | annotationProcessor "io.micronaut:micronaut-inject-java"
25 | annotationProcessor "io.micronaut:micronaut-validation"
26 | implementation platform("io.micronaut:micronaut-bom:$micronautVersion")
27 | implementation "io.micronaut:micronaut-inject"
28 | implementation "io.micronaut:micronaut-validation"
29 | implementation "io.micronaut:micronaut-runtime"
30 | implementation "javax.annotation:javax.annotation-api"
31 | implementation "io.micronaut:micronaut-http-server-netty"
32 | implementation "io.micronaut:micronaut-http-client"
33 | runtimeOnly "ch.qos.logback:logback-classic:1.2.3"
34 | testAnnotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
35 | testAnnotationProcessor "io.micronaut:micronaut-inject-java"
36 | testImplementation platform("io.micronaut:micronaut-bom:$micronautVersion")
37 | testImplementation "org.junit.jupiter:junit-jupiter-api"
38 | testImplementation "io.micronaut.test:micronaut-test-junit5"
39 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
40 | }
41 |
42 | test.classpath += configurations.developmentOnly
43 |
44 | mainClassName = "example.micronaut.Application"
45 | // use JUnit 5 platform
46 | test {
47 | useJUnitPlatform()
48 | }
49 | tasks.withType(JavaCompile){
50 | options.encoding = "UTF-8"
51 | options.compilerArgs.add('-parameters')
52 | }
53 |
54 | shadowJar {
55 | mergeServiceFiles()
56 | }
57 |
58 | run.classpath += configurations.developmentOnly
59 | run.jvmArgs('-noverify', '-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote')
60 |
--------------------------------------------------------------------------------
/trainer/.github/workflows/docker-image.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ github.actor }}
5 | github_password: ${{ secrets.GITHUB_TOKEN }} # Must be made available to the workflow
6 | GIT_COMMIT: ${{ github.sha }}
7 | jobs:
8 | Build:
9 | runs-on: ubuntu-latest
10 | container: gradle:6-jdk11
11 | steps:
12 | - name: Validate GitHub username is all lowercase
13 | id: validate_lower
14 | run: |
15 | if [[ "${{ env.github_username }}" =~ [A-Z] ]]; then
16 | echo "::error::Validation Failed: GitHub username '${{ env.github_username }}' cannot contain uppercase characters."
17 | exit 1
18 | else
19 | echo "GitHub username format is valid."
20 | fi
21 | shell: bash
22 | - name: Clone down repository
23 | uses: actions/checkout@v4
24 | - name: Build application
25 | run: ci/build-app.sh
26 | - name: Test
27 | run: ci/unit-test-app.sh
28 | - name: Upload repo
29 | uses: actions/upload-artifact@v4
30 | with:
31 | name: code
32 | path: .
33 | include-hidden-files: true
34 | Linting:
35 | runs-on: ubuntu-latest
36 | needs:
37 | - Build
38 | steps:
39 | - name: Download code
40 | uses: actions/download-artifact@v4
41 | with:
42 | name: code
43 | path: .
44 | - name: run linting
45 | uses: super-linter/super-linter/slim@v7
46 | env:
47 | DEFAULT_BRANCH: main
48 | # To report GitHub Actions status checks
49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 | DISABLE_ERRORS: true
51 | Docker-image:
52 | runs-on: ubuntu-latest
53 | needs: [Build]
54 | permissions:
55 | packages: write
56 | steps:
57 | - name: Download code
58 | uses: actions/download-artifact@v4
59 | with:
60 | name: code
61 | path: .
62 | - name: ls
63 | run: ls -la ci
64 | - name: build docker
65 | run: bash ci/build-docker.sh
66 | - name: push docker
67 | run: bash ci/push-docker.sh
68 |
--------------------------------------------------------------------------------
/trainer/.github/workflows/component-test.yaml:
--------------------------------------------------------------------------------
1 | name: Java CI
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ secrets.DOCKER_USERNAME }}
5 | github_password: ${{ secrets.DOCKER_PASSWORD }}
6 | jobs:
7 | Clone-down:
8 | name: Clone down repo
9 | runs-on: ubuntu-latest
10 | container: gradle:6-jdk11
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Upload Repo
14 | uses: actions/upload-artifact@v4
15 | with:
16 | name: code
17 | path: .
18 | include-hidden-files: true
19 | Test:
20 | runs-on: ubuntu-latest
21 | needs: Clone-down
22 | container: gradle:6-jdk11
23 | steps:
24 | - name: Download code
25 | uses: actions/download-artifact@v4
26 | with:
27 | name: code
28 | path: .
29 | - name: Test with Gradle
30 | run: chmod +x ci/unit-test-app.sh && ci/unit-test-app.sh
31 | Build:
32 | runs-on: ubuntu-latest
33 | needs: Clone-down
34 | container: gradle:6-jdk11
35 | steps:
36 | - name: Download code
37 | uses: actions/download-artifact@v4
38 | with:
39 | name: code
40 | path: .
41 | - name: Build with Gradle
42 | run: chmod +x ci/build-app.sh && ci/build-app.sh
43 | - name: Upload Repo
44 | uses: actions/upload-artifact@v4
45 | with:
46 | name: code
47 | path: .
48 | Docker-image:
49 | runs-on: ubuntu-latest
50 | needs: [Build,Test]
51 | steps:
52 | - name: Download code
53 | uses: actions/download-artifact@v4
54 | with:
55 | name: code
56 | path: .
57 | - name: build docker
58 | run: chmod +x ci/build-docker.sh && export GIT_COMMIT="GA-$GITHUB_SHA" && ci/build-docker.sh
59 | - name: push docker
60 | run: chmod +x ci/push-docker.sh && export GIT_COMMIT="GA-$GITHUB_SHA" && ci/push-docker.sh
61 | Component-test:
62 | runs-on: ubuntu-latest
63 | needs: Docker-image
64 | steps:
65 | - name: Download code
66 | uses: actions/download-artifact@v4
67 | with:
68 | name: code
69 | path: .
70 | - name: Execute component test
71 | run: chmod +x ci/component-test.sh && GIT_COMMIT="GA-$GITHUB_SHA" && ci/component-test.sh
72 |
--------------------------------------------------------------------------------
/trainer/.github/workflows/systems-tests.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ github.actor }}
5 | github_password: ${{ secrets.GITHUB_TOKEN }} # Needs to be made available to the workflow
6 | GIT_COMMIT: ${{ github.sha }}
7 | jobs:
8 | Build:
9 | runs-on: ubuntu-latest
10 | container: gradle:6-jdk11
11 | steps:
12 | - name: Clone down repository
13 | uses: actions/checkout@v4
14 | - name: Build application
15 | run: ci/build-app.sh
16 | - name: Test
17 | run: ci/unit-test-app.sh
18 | - name: Upload repo
19 | uses: actions/upload-artifact@v4
20 | with:
21 | name: code
22 | path: .
23 | include-hidden-files: true
24 | Linting:
25 | runs-on: ubuntu-latest
26 | needs: [Build]
27 | steps:
28 | - name: Download code
29 | uses: actions/download-artifact@v4
30 | with:
31 | name: code
32 | path: .
33 | - name: run linting
34 | uses: super-linter/super-linter/slim@v7
35 | env:
36 | DEFAULT_BRANCH: main
37 | # To report GitHub Actions status checks
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 | DISABLE_ERRORS: true
40 | Docker-image:
41 | runs-on: ubuntu-latest
42 | needs: [Build]
43 | permissions:
44 | packages: write
45 | steps:
46 | - name: Download code
47 | uses: actions/download-artifact@v4
48 | with:
49 | name: code
50 | path: .
51 | - name: build docker
52 | run: bash ci/build-docker.sh
53 | - name: push docker
54 | run: bash ci/push-docker.sh
55 | Component-test:
56 | runs-on: ubuntu-latest
57 | needs: Docker-image
58 | steps:
59 | - name: Download code
60 | uses: actions/download-artifact@v4
61 | with:
62 | name: code
63 | path: .
64 | - name: Execute component test
65 | run: bash ci/component-test.sh
66 | Performance-test:
67 | runs-on: ubuntu-latest
68 | needs: Docker-image
69 | steps:
70 | - name: Download code
71 | uses: actions/download-artifact@v4
72 | with:
73 | name: code
74 | path: .
75 | - name: Execute performance test
76 | run: bash ci/performance-test.sh
--------------------------------------------------------------------------------
/trainer/.github/workflows/pr-workflow.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | env: # Set the secret as an input
10 | github_username: ${{ github.actor }}
11 | github_password: ${{ secrets.GITHUB_TOKEN }} # Needs to be made available to the workflow
12 | GIT_COMMIT: ${{ github.sha }}
13 | jobs:
14 | Build:
15 | runs-on: ubuntu-latest
16 | container: gradle:6-jdk11
17 | steps:
18 | - name: Clone down repository
19 | uses: actions/checkout@v4
20 | - name: Build application
21 | run: ci/build-app.sh
22 | - name: Test
23 | run: ci/unit-test-app.sh
24 | - name: Upload repo
25 | uses: actions/upload-artifact@v4
26 | with:
27 | name: code
28 | path: .
29 | include-hidden-files: true
30 | Linting:
31 | runs-on: ubuntu-latest
32 | needs: [Build]
33 | steps:
34 | - name: Download code
35 | uses: actions/download-artifact@v4
36 | with:
37 | name: code
38 | path: .
39 | - name: run linting
40 | uses: super-linter/super-linter/slim@v7
41 | env:
42 | DEFAULT_BRANCH: main
43 | # To report GitHub Actions status checks
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | DISABLE_ERRORS: true
46 | Docker-image:
47 | runs-on: ubuntu-latest
48 | needs: [Build]
49 | permissions:
50 | packages: write
51 | steps:
52 | - name: Download code
53 | uses: actions/download-artifact@v4
54 | with:
55 | name: code
56 | path: .
57 | - name: build docker
58 | run: bash ci/build-docker.sh
59 | - name: push docker
60 | run: bash ci/push-docker.sh
61 | Component-test:
62 | runs-on: ubuntu-latest
63 | needs: Docker-image
64 | steps:
65 | - name: Download code
66 | uses: actions/download-artifact@v4
67 | with:
68 | name: code
69 | path: .
70 | - name: Execute component test
71 | run: bash ci/component-test.sh
72 | Performance-test:
73 | runs-on: ubuntu-latest
74 | needs: Docker-image
75 | steps:
76 | - name: Download code
77 | uses: actions/download-artifact@v4
78 | with:
79 | name: code
80 | path: .
81 | - name: Execute performance test
82 | run: bash ci/performance-test.sh
83 |
--------------------------------------------------------------------------------
/app/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/labs/build-cache.md:
--------------------------------------------------------------------------------
1 | # Reusing build cache
2 |
3 | Github Actions has a few different methods for reusing files and artifacts produced in a job, in downstream jobs or even in subsequent builds.
4 |
5 | This is needed because each job in Github Actions is running in a separate docker container or machine, and by default no files are shared between these.
6 |
7 | ## Caching
8 |
9 | The caching mechanism is persistent across multiple builds, and therefore a key is needed.
10 |
11 | One example is to store downloaded dependencies in a cache, to avoid downloading the same dependencies over and over. And since dependencies typically are defined in one central file, this file is hashed and used as a key. At Github Actions `actions/cache@v3` can be used to do it. Possible input parameters are:
12 |
13 | * `path` - (required) The file path on the runner to cache or restore. The path can be an absolute path or relative to the working directory.
14 | * `key` - (required) The key created when cache was saved and later used to find it.
15 | * `restore-keys` - (optional) An ordered list of alternative keys to use for finding the cache if no cache hit occurred for key.
16 |
17 | The example for gradle can be seen below.
18 |
19 | ``` yaml
20 | - name: Cache Gradle packages
21 | uses: actions/cache@v3
22 | with:
23 | path: |
24 | ~/.gradle/caches
25 | ~/.gradle/wrapper
26 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
27 | restore-keys: |
28 | ${{ runner.os }}-gradle-
29 | ```
30 |
31 | To use cache at the consecutive jobs, the same code (from above example) has to be added as a step.
32 |
33 | Limitations on using caching:
34 |
35 | * Github will delete all un-accessed caches for 7 days.
36 | * There is no limit on how many caches can be stored, however if the total space used by the repository exceeds 10GB GitHub will start deleting the oldest caches.
37 |
38 | ## Tasks
39 |
40 | In this exercise the artifacts management in the existing pipeline will be changed to use caching instead as follow:
41 |
42 | 1. Create a new Github Actions file under `./github/workflows/caching.yaml` with exact copy of the pipeline from exercise 7.
43 | 2. Instead of using upload and download artifacts at each job, use caching with an above example for Gradle.
44 | 3. Should artifact be uploaded at the end of the workflow?
45 |
46 | ## Resources
47 |
48 | More information about caching can be found here:
49 |
50 | * [caching-dependencies-to-speed-up-workflows](https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows)
51 | * [Cache action tool](https://github.com/actions/cache)
52 |
--------------------------------------------------------------------------------
/labs/systems-test/done/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ github.actor }}
5 | github_password: ${{ secrets.GITHUB_TOKEN }} # Must be made available to the workflow
6 | GIT_COMMIT: ${{ github.sha }}
7 |
8 | jobs:
9 | Build:
10 | runs-on: ubuntu-latest
11 | container: gradle:6-jdk11
12 | steps:
13 | - name: Clone down repository
14 | uses: actions/checkout@v4
15 | - name: Build application
16 | run: ci/build-app.sh
17 | - name: Test
18 | run: ci/unit-test-app.sh
19 | - name: Upload repo
20 | uses: actions/upload-artifact@v4
21 | with:
22 | name: code
23 | path: .
24 | include-hidden-files: true
25 |
26 | Linting:
27 | runs-on: ubuntu-latest
28 | needs: [Build]
29 | steps:
30 | - name: Download code
31 | uses: actions/download-artifact@v4
32 | with:
33 | name: code
34 | path: .
35 | - name: run linting
36 | uses: super-linter/super-linter/slim@v7
37 | env:
38 | DEFAULT_BRANCH: main
39 | # To report GitHub Actions status checks
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | DISABLE_ERRORS: true
42 |
43 | Docker-image:
44 | runs-on: ubuntu-latest
45 | needs: [Build]
46 | permissions:
47 | packages: write
48 | steps:
49 | - name: Download code
50 | uses: actions/download-artifact@v4
51 | with:
52 | name: code
53 | path: .
54 | - name: Validate Docker username is all lowercase
55 | id: validate_lower
56 | run: |
57 | if [[ "${{ env.github_username }}" =~ [A-Z] ]]; then
58 | echo "::error::Validation Failed: GitHub username '${{ env.github_username }}' cannot contain uppercase characters."
59 | exit 1
60 | else
61 | echo "Docker username format is valid."
62 | fi
63 | shell: bash
64 | - name: build docker
65 | run: bash ci/build-docker.sh
66 | - name: push docker
67 | run: bash ci/push-docker.sh
68 |
69 | Component-test:
70 | runs-on: ubuntu-latest
71 | needs: Docker-image
72 | steps:
73 | - name: Download code
74 | uses: actions/download-artifact@v4
75 | with:
76 | name: code
77 | path: .
78 | - name: Execute component test
79 | run: bash ci/component-test.sh
80 |
81 | Performance-test:
82 | runs-on: ubuntu-latest
83 | needs: Docker-image
84 | steps:
85 | - name: Download code
86 | uses: actions/download-artifact@v4
87 | with:
88 | name: code
89 | path: .
90 | - name: Execute performance test
91 | run: bash ci/performance-test.sh
--------------------------------------------------------------------------------
/trainer/.github/workflows/caching.yaml:
--------------------------------------------------------------------------------
1 | name: Java CI
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ secrets.DOCKER_USERNAME }}
5 | github_password: ${{ secrets.DOCKER_PASSWORD }}
6 | jobs:
7 | Clone-down:
8 | name: Clone down repo
9 | runs-on: ubuntu-latest
10 | container: gradle:6-jdk11
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Cache Gradle packages
14 | uses: actions/cache@v3
15 | with:
16 | path: |
17 | ~/.gradle/caches
18 | ~/.gradle/wrapper
19 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
20 | restore-keys: |
21 | ${{ runner.os }}-gradle-
22 | Test:
23 | runs-on: ubuntu-latest
24 | needs: Clone-down
25 | container: gradle:6-jdk11
26 | steps:
27 | - name: Cache Gradle packages
28 | uses: actions/cache@v3
29 | with:
30 | path: |
31 | ~/.gradle/caches
32 | ~/.gradle/wrapper
33 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
34 | - name: Test with Gradle
35 | run: chmod +x ci/unit-test-app.sh && ci/unit-test-app.sh
36 | Build:
37 | runs-on: ubuntu-latest
38 | needs: Clone-down
39 | container: gradle:6-jdk11
40 | steps:
41 | - name: Cache Gradle packages
42 | uses: actions/cache@v3
43 | with:
44 | path: |
45 | ~/.gradle/caches
46 | ~/.gradle/wrapper
47 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
48 | - name: Build with Gradle
49 | run: chmod +x ci/build-app.sh && ci/build-app.sh
50 | - name: Upload Repo
51 | uses: actions/upload-artifact@v3
52 | with:
53 | name: code
54 | path: .
55 | Docker-image:
56 | runs-on: ubuntu-latest
57 | needs: [Build,Test]
58 | steps:
59 | - name: Cache Gradle packages
60 | uses: actions/cache@v3
61 | with:
62 | path: |
63 | ~/.gradle/caches
64 | ~/.gradle/wrapper
65 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
66 | - name: build docker
67 | run: chmod +x ci/build-docker.sh && export GIT_COMMIT="GA-$GITHUB_SHA" && ci/build-docker.sh
68 | - name: push docker
69 | run: chmod +x ci/push-docker.sh && export GIT_COMMIT="GA-$GITHUB_SHA" && ci/push-docker.sh
70 | Component-test:
71 | runs-on: ubuntu-latest
72 | needs: Docker-image
73 | steps:
74 | - name: Cache Gradle packages
75 | uses: actions/cache@v3
76 | with:
77 | path: |
78 | ~/.gradle/caches
79 | ~/.gradle/wrapper
80 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
81 | - name: Execute component test
82 | run: chmod +x ci/component-test.sh && GIT_COMMIT="GA-$GITHUB_SHA" && ci/component-test.sh
83 |
--------------------------------------------------------------------------------
/trainer/.github/workflows/trivy-scan.yaml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on: push
3 | env: # Set the secret as an input
4 | github_username: ${{ github.actor }}
5 | github_password: ${{ secrets.GITHUB_TOKEN }} #Nees to be set to be made available to the workflow
6 | GIT_COMMIT: ${{ github.sha }}
7 | jobs:
8 | Build:
9 | runs-on: ubuntu-latest
10 | container: gradle:6-jdk11
11 | steps:
12 | - name: Clone down repository
13 | uses: actions/checkout@v4
14 | - name: Build application
15 | run: ci/build-app.sh
16 | - name: Test
17 | run: ci/unit-test-app.sh
18 | - name: Upload repo
19 | uses: actions/upload-artifact@v4
20 | with:
21 | name: code
22 | path: .
23 | include-hidden-files: true
24 | Linting:
25 | runs-on: ubuntu-latest
26 | needs: [Build]
27 | steps:
28 | - name: Download code
29 | uses: actions/download-artifact@v4
30 | with:
31 | name: code
32 | path: .
33 | - name: run linting
34 | uses: super-linter/super-linter/slim@v7
35 | env:
36 | DEFAULT_BRANCH: main
37 | # To report GitHub Actions status checks
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 | DISABLE_ERRORS: true
40 | Docker-image:
41 | runs-on: ubuntu-latest
42 | needs: [Build]
43 | permissions:
44 | packages: write
45 | steps:
46 | - name: Download code
47 | uses: actions/download-artifact@v4
48 | with:
49 | name: code
50 | path: .
51 | - name: build docker
52 | run: bash ci/build-docker.sh
53 | - name: push docker
54 | run: bash ci/push-docker.sh
55 | Security-scan:
56 | runs-on: ubuntu-latest
57 | needs: Docker-image
58 | steps:
59 | - name: Download code
60 | uses: actions/download-artifact@v4
61 | with:
62 | name: code
63 | path: .
64 | - name: Run Trivy vulnerability scanner
65 | uses: aquasecurity/trivy-action@master
66 | with:
67 | image-ref: 'ghcr.io/${{ github.actor }}/micronaut-app:latest'
68 | format: 'table'
69 | #exit-code: '1' #Defaults to 0 meaning that the action will not fail the build if vulnerabilities are found
70 | ignore-unfixed: true
71 | vuln-type: 'os,library'
72 | severity: 'CRITICAL,HIGH'
73 | Component-test:
74 | runs-on: ubuntu-latest
75 | needs: Docker-image
76 | steps:
77 | - name: Download code
78 | uses: actions/download-artifact@v4
79 | with:
80 | name: code
81 | path: .
82 | - name: Execute component test
83 | run: bash ci/component-test.sh
84 | Performance-test:
85 | runs-on: ubuntu-latest
86 | needs: Docker-image
87 | steps:
88 | - name: Download code
89 | uses: actions/download-artifact@v4
90 | with:
91 | name: code
92 | path: .
93 | - name: Execute performance test
94 | run: bash ci/performance-test.sh
--------------------------------------------------------------------------------
/labs/matrix-builds.md:
--------------------------------------------------------------------------------
1 | # Build app on multiple environments
2 |
3 | Github actions provides a method to run your tests with various input. This can
4 | be useful if you want to make sure that the code builds on different versions of,
5 | e.g., Java or Node. To avoid repeating the same thing in `YAML`, you can use
6 | [`strategy`](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategy)
7 |
8 | The app in this repository is Java-based and it is running on Java 11. The example how we can use it for different versions of Node:
9 |
10 | ```yaml
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | node: [8, 10, 12]
17 | steps:
18 | - uses: actions/setup-node@v4
19 | with:
20 | node-version: ${{ matrix.node }}
21 | ```
22 |
23 | With this, the job will run three separate jobs for different versions of NodeJS; 8, 10 and 12.
24 | There can be similar solution for container based pipelines:
25 |
26 | ```YAML
27 | name: matrix-example
28 | on: [workflow_dispatch]
29 |
30 | jobs:
31 | job:
32 | runs-on: ubuntu-latest
33 | strategy:
34 | matrix:
35 | container: ["ubuntu:bionic", "fedora:31", "opensuse/leap:42.3", "centos:8"]
36 | container:
37 | image: ${{ matrix.container }}
38 | steps:
39 | - name: check os
40 | run: cat /etc/os-release
41 | ```
42 |
43 | In this example pipeline runs-on `ubuntu-latest` for 4 different containers: `"ubuntu:bionic", "fedora:31", "opensuse/leap:42.3", "centos8"`. This way we can skip repeating the workflows.
44 |
45 | ## Tasks
46 |
47 | we would like to build our application on different versions of Java.
48 |
49 | ### Add a new job that builds on various versions
50 |
51 | - You can add a new file on `.github/workflows/matrix.yml`, which will only include `Clone-down`and `Build` job.
52 |
53 |
54 | Build job
55 |
56 | ```YAML
57 | name: Matrix workflow
58 | on: push
59 | jobs:
60 | Build:
61 | runs-on: ubuntu-latest
62 | container: gradle:6-jdk11
63 | steps:
64 | - name: Clone down repository
65 | uses: actions/checkout@v4
66 | - name: Build application
67 | run: ci/build-app.sh
68 | - name: Test
69 | run: ci/unit-test-app.sh
70 | ```
71 |
72 |
73 |
74 | ---
75 |
76 | - Edit build job to run for different versions of Java. Add matrix for types of containers as: `["gradle:6-jdk8", "gradle:6-jdk11", "gradle:6-jdk17"]` to your build job.
77 |
78 | ```yaml
79 |
80 | strategy:
81 | matrix:
82 | container: ["gradle:6-jdk8", "gradle:6-jdk11", "gradle:6-jdk17"]
83 |
84 | ```
85 |
86 | ---
87 |
88 | - Remember to edit container name:
89 |
90 | ```yaml
91 | container:
92 | image: ${{ matrix.container }}
93 | ```
94 |
95 |
96 | Solution
97 |
98 | ``` yaml
99 | name: Matrix workflow
100 | on: push
101 | jobs:
102 | Build:
103 | runs-on: ubuntu-latest
104 | strategy:
105 | matrix:
106 | container: ["gradle:6-jdk8", "gradle:6-jdk11", "gradle:6-jdk17"]
107 | container:
108 | image: ${{ matrix.container }}
109 |
110 | steps:
111 | - name: Clone down repository
112 | uses: actions/checkout@v4
113 | - name: Build application
114 | run: ci/build-app.sh
115 | - name: Test
116 | run: ci/unit-test-app.sh
117 | ```
118 |
119 |
120 |
121 | ## Results
122 |
123 | You should have 3 jobs under `Build` job for different version of Java.
124 |
125 | ### Questions
126 |
127 | - Are there versions of Java which do not work?
128 |
--------------------------------------------------------------------------------
/labs/composite-action.md:
--------------------------------------------------------------------------------
1 | # Composite actions
2 |
3 | In this hands-on lab you'll create a small composite action that runs a few shell steps plus the Python helper located at `ci/print_summary.py`.
4 |
5 | The goal is to learn how to: create a composite action, accept inputs, expose outputs, and call it from a workflow.
6 |
7 | This lab contains the following steps:
8 |
9 | - Create the composite action metadata
10 | - Use the composite action in a simple workflow
11 | - Run the workflow locally (or on GitHub Actions) and inspect the output
12 |
13 | ## Create the composite action
14 |
15 | 1. Create a new directory `.github/actions/summary-action` in the repository.
16 | 2. Add an `action.yml` file with the following behaviour:
17 |
18 | - Accept an input `numbers` which is a JSON array encoded as a string (for simplicity)
19 | - Pipe that string to the Python helper `ci/print_summary.py` via stdin
20 | - Save the printed JSON summary to an output parameter called `summary`
21 |
22 | For more information on action metadata format and the composite action syntax, see the GitHub Docs:
23 |
24 |
25 | Solution
26 |
27 | ```yaml
28 | name: Summary action
29 | description: "Pipe numbers into a Python helper and print a small summary"
30 | inputs:
31 | numbers:
32 | description: 'JSON array of numbers as a string, e.g. "[1,2,3]"'
33 | required: true
34 | outputs:
35 | summary:
36 | description: 'JSON summary produced by the helper'
37 | value: ${{ steps.set-output.outputs.summary }}
38 | runs:
39 | using: "composite"
40 | steps:
41 | - name: Run python summarizer (stdin)
42 | shell: bash
43 | run: |
44 | echo "${{ inputs.numbers }}" | python3 ci/print_summary.py > summary.json
45 | cat summary.json
46 |
47 | - name: Set output
48 | id: set-output
49 | shell: bash
50 | run: echo "summary=$(cat summary.json)" >> $GITHUB_OUTPUT
51 | ```
52 |
53 |
54 |
55 | Notes:
56 |
57 | - The composite action uses the `composite` runner so it can string together multiple steps.
58 | - We store the temporary file path in an intermediate output (optional). The important part is that the final step writes the `summary` to `$GITHUB_OUTPUT` so the action exposes the output.
59 |
60 | ## Use the composite action
61 |
62 | 1. Create a workflow `.github/workflows/use-summary.yml` with a manual trigger (`workflow_dispatch`).
63 | 2. Add a job that calls the action with a `numbers` value.
64 |
65 | Example consumer (solution):
66 |
67 |
68 | Solution
69 |
70 | ```yaml
71 | name: Use summary action
72 |
73 | on: [workflow_dispatch]
74 |
75 | jobs:
76 | call-summary:
77 | runs-on: ubuntu-latest
78 | steps:
79 | - uses: actions/checkout@v4
80 |
81 | - name: Call summary composite action
82 | id: summary
83 | uses: ./.github/actions/summary-action
84 | with:
85 | numbers: '[10, 20, 30, 40]'
86 |
87 | - name: Print action output
88 | run: |
89 | echo "Summary output: ${{ steps.summary.outputs.summary }}"
90 | ```
91 |
92 |
93 |
94 | ## Run the workflow
95 |
96 | - You can dispatch the workflow from the GitHub UI (Workflow -> Run workflow) or run the steps locally using a runner that supports Actions (for example, act). The important part is that the workflow demonstrates how inputs and outputs flow through the composite action.
97 |
98 | ## Summary
99 |
100 | You've created a composite action that runs multiple steps and produces an output. You also learned how to call that composite action from a workflow and read its outputs.
101 |
--------------------------------------------------------------------------------
/labs/setup.md:
--------------------------------------------------------------------------------
1 | # Github Actions Katas
2 |
3 | This series of katas will go through the basic steps in github actions, making you able to make CI builds in the end.
4 |
5 | ## Learning Goals
6 |
7 | - Creating an instance of the template repository
8 | - Creating a workflow file seeing GitHub Actions in action
9 |
10 | ## Exercise
11 |
12 | ### Overview
13 |
14 | In this exercise we are creating your own instance of this templated repository, and creating a workflow.
15 |
16 |
17 | :bulb: If you want to clone the newly created repository down on your machine, you need to have git set up there. Here are the commands to set it up
18 |
19 | You need to provide your email and name to git with the following commands.
20 |
21 | ``` bash
22 | git config --global user.email "you@example.com"
23 | git config --global user.name "Your Name"
24 | ```
25 |
26 | When you do a git clone, then you will be asked for your username and password. If you want to avoid that, you can set up an ssh key. [Here is a guide on how to do that](https://help.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh). It will take you 5-10 minutes though, so if you are in a hurry, just use the username and password.
27 |
28 |
29 |
30 | ### Tasks
31 |
32 | #### Create a repository
33 |
34 | - Go to _Code_ tab of this repository
35 | - Click _Use this template_ and _Create a new repository_
36 |
37 | 
38 |
39 | - Fill in the details for your new repository:
40 |
41 | - Owner: Select your GitHub user as the owner.
42 | - Name: `github-actions-katas` or something similar.
43 | - Description: Add a description if you want.
44 | - Visibility: **Public**. This is important! Some of the exercises later on will fail if the visibility of the repository is not public.
45 | - Press _Create repository_.
46 | - Once the repository is created, open the _Setup_ exercise again in this new repository.
47 |
48 | The new repository has now been created as a public repository under your own user, and you should be
49 | reading this in the new repository.
50 |
51 | > :bulb: **From this point forward, all actions should be performed in the repository you just created, not the template repository**
52 |
53 | #### Create a simple workflow
54 |
55 | In this part of the exercise we will create a simple workflow that just prints "Hello, world!" to
56 | the build log.
57 |
58 | - Click on the _Actions_ tab
59 | - Click on the _Setup the workflow yourself_ link
60 |
61 | 
62 |
63 | The file `.github/workflows/main.yml` will be opened as an empty file in the GitHub web editor.
64 |
65 | - Copy the following content into the file and save it:
66 |
67 | ``` yaml
68 | # This is a basic workflow to help you get started with Actions
69 |
70 | name: CI
71 |
72 | # Controls when the workflow will run
73 | on:
74 | # Triggers the workflow on push or pull request events but only for the "main" branch
75 | push:
76 | branches: [ "main" ]
77 | pull_request:
78 | branches: [ "main" ]
79 |
80 | # Allows you to run this workflow manually from the Actions tab
81 | workflow_dispatch:
82 |
83 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
84 | jobs:
85 | # This workflow contains a single job called "build"
86 | build:
87 | # The type of runner that the job will run on
88 | runs-on: ubuntu-latest
89 |
90 | # Steps represent a sequence of tasks that will be executed as part of the job
91 | steps:
92 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
93 | - uses: actions/checkout@v4
94 |
95 | # Runs a single command using the runners shell
96 | - name: Run a one-line script
97 | run: echo Hello, world!
98 |
99 | # Runs a set of commands using the runners shell
100 | - name: Run a multi-line script
101 | run: |
102 | echo Add other actions to build,
103 | echo test, and deploy your project.
104 | ```
105 |
106 | - Click _Commit changes_ - found in the _upper right corner_ of the screen - and commit to the
107 | `main` branch
108 | - Go to the _Actions_ tab and see the workflow running
109 | - Click on the workflow and see the output of the workflow
110 |
111 | ## Summary
112 |
113 | Congratulations! You have now created your first workflow!
114 | It does not do much, but in the next exercise we will start building on it.
115 |
--------------------------------------------------------------------------------
/labs/reusable-workflow.md:
--------------------------------------------------------------------------------
1 | # Reusable workflow that uses a composite action
2 |
3 | This lab builds on the composite action exercise. The reusable workflow will call the composite action `summary-action` (created earlier) to compute a small summary of a list of numbers and expose that summary as outputs. A consuming workflow will call the reusable workflow and then print the outputs.
4 |
5 | Follow these steps:
6 |
7 | ## Create the reusable workflow
8 |
9 | 1. Create `.github/workflows/reusable.yml`.
10 | 2. Use the `workflow_call` trigger and add an input `numbers` of type `string` (JSON array encoded as a string). The input should have a default of `'[1,2,3]'`.
11 | 3. The workflow should run a job `summarize` which:
12 | - checks out the repository
13 | - calls the composite action `./.github/actions/summary-action` with the provided input
14 | - formats the JSON summary into a concise human-readable string
15 | - publishes both the JSON `summary` and the human-readable `summary_text` as job outputs
16 | 4. The workflow should expose outputs that map to the job outputs.
17 |
18 |
19 | Solution: reusable.yml
20 |
21 | ```yaml
22 | name: Reusable summary workflow
23 |
24 | on:
25 | workflow_call:
26 | inputs:
27 | numbers:
28 | description: 'JSON array of numbers as a string'
29 | type: string
30 | required: true
31 | default: '[1,2,3]'
32 | outputs:
33 | summary:
34 | description: 'Summary produced by the composite action (JSON)'
35 | value: ${{ jobs.summarize.outputs.summary }}
36 | summary_text:
37 | description: 'Human-readable summary text'
38 | value: ${{ jobs.summarize.outputs.summary_text }}
39 |
40 | jobs:
41 | summarize:
42 | runs-on: ubuntu-latest
43 | outputs:
44 | summary: ${{ steps.call-summary.outputs.summary }}
45 | summary_text: ${{ steps.format.outputs.summary_text }}
46 | steps:
47 | - uses: actions/checkout@v4
48 |
49 | - name: Call summary composite action
50 | id: call-summary
51 | uses: ./.github/actions/summary-action
52 | with:
53 | numbers: ${{ inputs.numbers }}
54 |
55 | - name: Format summary (create human-readable text)
56 | id: format
57 | shell: bash
58 | run: |
59 | printf '%s' '${{ steps.call-summary.outputs.summary }}' > summary.json || true
60 | echo "summary_text=$(cat summary.json)" >> $GITHUB_OUTPUT
61 |
62 | python3 ./ci/format_summary.py summary.json > summary_text.txt
63 | echo "summary_text=$(cat summary_text.txt)" >> $GITHUB_OUTPUT
64 | ```
65 |
66 |
67 |
68 | ## Create a consumer workflow
69 |
70 | 1. Create `.github/workflows/use-reusable.yml` with a `workflow_dispatch` trigger.
71 | 2. Add a job `call-reusable` that uses the reusable workflow `./.github/workflows/reusable.yml` and passes a custom `numbers` value.
72 | 3. Add a second job `show-summary` that depends on `call-reusable` and prints the outputs.
73 |
74 |
75 | Solution: use-reusable.yml (consumer)
76 |
77 | ```yaml
78 | name: Use reusable summary
79 |
80 | on: [workflow_dispatch]
81 |
82 | jobs:
83 | call-reusable:
84 | uses: ./.github/workflows/reusable.yml
85 | with:
86 | numbers: '[5, 15, 25, 35, 45]'
87 |
88 | show-summary:
89 | runs-on: ubuntu-latest
90 | needs: [call-reusable]
91 | steps:
92 | - name: Print reusable workflow outputs
93 | run: |
94 | echo "Reusable summary (JSON): ${{ needs.call-reusable.outputs.summary }}"
95 | echo "Reusable summary (text): ${{ needs.call-reusable.outputs.summary_text }}"
96 | ```
97 |
98 |
99 |
100 | ## Try it
101 |
102 | - Make sure `ci/print_summary.py` is executable (if running on a GitHub runner the `python3` call will work).
103 | - Commit the `action.yml` for the composite action, the `reusable.yml` workflow, and the consumer workflow.
104 | - Dispatch `use-reusable.yml` from the GitHub Actions UI and inspect the `show-summary` job logs to see the outputs.
105 |
106 | ## Notes and tips
107 |
108 | - The composite action uses stdin in the example to keep wiring trivial.
109 | - In real actions you might want to validate inputs more strictly and avoid writing secrets to disk.
110 | - This example shows how composite actions and reusable workflows can be composed to build small, testable building blocks.
111 |
--------------------------------------------------------------------------------
/labs/build-app.md:
--------------------------------------------------------------------------------
1 | # Building the application
2 |
3 | Github Actions is configured through the [YAML files](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions).
4 |
5 | :bulb: The trickiest part of writing the configuration files is typically getting the indentation right.
6 |
7 | ## learning goals
8 |
9 | - Understand the basic structure of a workflow file
10 | - Understand the basic structure of a job
11 | - Understand the basic structure of a step
12 |
13 | ## Building a CI pipeline in GitHub Actions
14 |
15 | In this workshop we will use a small Java web service which is built and tested using Gradle.
16 |
17 | The details of the implementation are not relevant for the purposes of these exercises. We will
18 | instead focus on how to build a CI pipeline using GitHub Actions, that builds, tests and packages
19 | the webservice.
20 |
21 | The application is found in the `app` directory. Scripts to build, test and package the web service
22 | are found in the `ci` directory.
23 |
24 | We ultimately want a pipeline that has the following jobs:
25 |
26 | - **Build and test:** Clones down and run the gradle build command found in [ci/build-app.sh](../ci/build-app.sh), and thereafter runs the gradle test command found in [ci/unit-test-app.sh](../ci/unit-test-app.sh)
27 | - **Build docker:** runs both [building of the docker image](../ci/build-docker.sh), and [pushes it up to the hub](../ci/push-docker.sh)
28 | - **Component test:** runs a [docker-compose file](../component-test/docker-compose.yml) with a [python test](../component-test/test_app.py) to test the application.
29 | - **Performance test:** runs a [docker-compose file](../performance-test/docker-compose.yml) with a [k6 performance tester](../performance-test/single-request.js) to test the application.
30 |
31 | We are not going to do it all in one go, but rather step by step.
32 |
33 | ### A basic example
34 |
35 | Examine the following example workflow definition:
36 |
37 | ```yaml
38 | name: Main workflow
39 | on: push
40 | jobs:
41 | Build:
42 | runs-on: ubuntu-latest
43 | container: gradle:6-jdk11
44 | steps:
45 | - name: Clone down repository
46 | uses: actions/checkout@v4
47 | - name: Build application
48 | run: ci/build-app.sh
49 | ```
50 |
51 | A line-by-line explanation of the above:
52 |
53 | - **Line 1**: Specifies the name of the workflow, in this case, "Main workflow".
54 | - **Line 2**: Specifies that the workflow should be triggered on a `push` event.
55 | - **Line 3**: Introduces the `jobs` section where all job definitions reside.
56 | - **Line 4-5**: Defines a job named `Build` that runs on an Ubuntu VM.
57 | - **Line 6**: Specifies that the job should run in a container with the image `gradle:6-jdk11`.
58 | - **Line 7**: Defines the steps that should be executed in the job.
59 | - **Line 8**: Defines a step named `Clone down repository` ...
60 | - **Line 9**: ... which uses the action `actions/checkout@v4`, to clone down the repository's content
61 | to the runner, enabling subsequent steps to access it.
62 | - **Line 10**: Defines a step named `Build application` ...
63 | - **Line 11**: ... which runs the `build-app.sh` script found in the `ci` directory of the repository.
64 |
65 | This workflow is a basic example that provides insights into the event type, branch reference, and repository structure when code is pushed to it.
66 |
67 | If you want to see what `build-app.sh` is doing, look into [the script](../ci/build-app.sh).
68 |
69 | ## Task
70 |
71 | - Replace the contents of `.github/workflows/main.yml` with the above example.
72 | - Add and commit the file and push it to GitHub.
73 |
74 |
75 | :bulb: Git commands to do it if you are using the terminal
76 |
77 | ```bash
78 | git add .github/workflows/main.yml
79 | git commit -m "Add basic workflow to build the web service"
80 | git push
81 |
82 | ```
83 |
84 |
85 |
86 | - Go to Github Actions tab of the repository and check the action status.
87 | - When the build is green, click the `Build` job entry to see the workflow log.
88 | - Expand the `Build application` step to see the output of the build.
89 |
90 | ### Results
91 |
92 | See that the build status is green and the job log looks something like this:
93 |
94 | ```bash
95 | Run ./ci/build-app.sh
96 | Starting a Gradle Daemon (subsequent builds will be faster)
97 | > Task :clean UP-TO-DATE
98 |
99 | > Task :compileJava
100 | Note: Creating bean classes for 3 type elements
101 |
102 | > Task :processResources
103 | > Task :classes
104 | > Task :shadowJar
105 |
106 | Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
107 | Use '--warning-mode all' to show the individual deprecation warnings.
108 | See https://docs.gradle.org/6.9.4/userguide/command_line_interface.html#sec:command_line_warnings
109 |
110 | BUILD SUCCESSFUL in 15s
111 | 4 actionable tasks: 3 executed, 1 up-to-date
112 | ```
113 |
114 | ## Summary
115 |
116 | Congratulations, you have now built the Java web service!
117 |
118 | But we have some way to go yet, we want to build a Docker image, and run some tests on it as well.
119 |
--------------------------------------------------------------------------------
/labs/starter-workflows-and-organization-sharing.md:
--------------------------------------------------------------------------------
1 | # Starter Workflows & Organizational Sharing
2 |
3 | ## **Starter Workflows**
4 |
5 | - **What:** Starter workflows allow everyone in your organization who has permission to create workflows to do so more quickly and easily.
6 |
7 | - **Why?**
8 | - Saves time
9 | - Promotes consistency
10 | - Serves as an exemplar for following best practices
11 |
12 | ### **Task**
13 |
14 | Let's dive in and create an organization, a starter workflow, and then run it!
15 |
16 | - Go to github.com and create a new organization from the `+` dropdown menu
17 |
18 | 
19 |
20 | - Select the free tier option ("create a free organization").
21 | - Set the organization name to anything you wish (must be unique across Github), and use your email for the contact.
22 | - Set `this organization belongs to` "My personal account".
23 | - Solve the captcha / robot detector test.
24 | - Accept the TOS and click next.
25 | - On the Welcome page click `skip this step` on the bottom.
26 | - Click the submit option on the bottom of the page to bypass the survey information.
27 | - Click the `Repository` tab.
28 | - Create a new repository.
29 | - Choose `.github` as the repository name (*required in order to make the magic work*).
30 | - 📝 Set visibility to public
31 | - Click the `Create repository` button on the bottom.
32 | - Create a directory named `workflow-templates`.
33 |
34 |
35 | Create a file named /workflow-templates/my-org-ci.yml
36 |
37 | ```YAML
38 | name: Octo Organization CI
39 |
40 | on:
41 | push:
42 | branches: [ $default-branch ]
43 | pull_request:
44 | branches: [ $default-branch ]
45 |
46 | jobs:
47 | build:
48 | runs-on: ubuntu-latest
49 |
50 | steps:
51 | - uses: actions/checkout@v3
52 |
53 | - name: Run a one-line script
54 | run: echo Hello from Octo Organization
55 | ```
56 |
57 |
58 |
59 |
60 | Create a file named /workflow-templates/my-org-ci.properties.json
61 |
62 | ```JSON
63 | {
64 | "name": "Octo Organization Workflow",
65 | "description": "Octo Organization CI starter workflow.",
66 | "iconName": "example-icon",
67 | "categories": [
68 | "node", "js"
69 | ],
70 | "filePatterns": [
71 | "package.json$",
72 | "^Dockerfile",
73 | ".*\\.md$",
74 | ".*\.ya?ml"
75 | ]
76 | }
77 | ```
78 |
79 |
80 |
81 | ---
82 |
83 | ## Using the Starter Workflow
84 |
85 | - Go to the Actions tab on the .github (or any repo owned by the org) and you should see a section "By Organization name"
86 |
87 | Click configure.
88 |
89 | Click start commit.
90 |
91 | Commit to main branch (create new file).
92 |
93 | Go back to Actions and click on build and then you should see the steps.
94 |
95 | ---
96 |
97 | ## Organizational Sharing
98 |
99 | Assets that can be shared organization wide:
100 |
101 | - starter workflows (as above)
102 | - self-hosted runners (covered in the lab [selfhosted-runner](./selfhosted-runner.md) )
103 | - secrets & variables
104 |
105 | ## Task - add a organization level variable or secret
106 |
107 | - Navigate to main organization page and click on `Settings`
108 | - Find the `Secrets and variables` section, expand, and click `Actions`
109 |
110 | 
111 | - Notice the top tab allows you to select between a secret (default) and a variable
112 | - Click the *New organization secret* button
113 | - Select a `Name` and `Value`, and observe that you can scope the secret to public (the default for some reason ), private, or selected repos within the organization
114 | - Note that secrets are redacted (with `***`) from log outputs, so in order to verify access, try a workflow similar to the following:
115 |
116 | Verify secret access `/.github/workflows/test-secret.yaml`
117 |
118 | ```YAML
119 | name: Access Organization Secret
120 | on:
121 | push:
122 | jobs:
123 | test-secret:
124 | runs-on: ubuntu-latest
125 | steps:
126 | - shell: bash
127 | env:
128 | KEEP_IT_SECRET: ${{ secrets.KEEP_IT_SAFE }}
129 | run: |
130 | if [ "$KEEP_IT_SECRET" = "12345" ]; then
131 | echo "I know your secret!"
132 | else
133 | echo "still trying to guess..."
134 | fi
135 | ```
136 |
137 |
138 |
139 | sample log output
140 |
141 | 
142 |
143 | - Note how even the hard-coded string "12345" is redacted from the output.
144 |
145 | ## Resources
146 |
147 | [Creating Starter Workflows](https://docs.github.com/en/actions/using-workflows/creating-starter-workflows-for-your-organization) starter workflows
148 |
149 | [Using Starter Workflows](https://docs.github.com/en/actions/using-workflows/using-starter-workflows) starter workflows
150 |
151 | [Organizational sharing](https://docs.github.com/en/actions/using-workflows/sharing-workflows-secrets-and-runners-with-your-organization) - workflows, self-hosted runners, secrets & variables
152 |
--------------------------------------------------------------------------------
/labs/pr-workflow.md:
--------------------------------------------------------------------------------
1 | # Pull Request based workflow
2 |
3 | > Note: In this exercise we use the term "main" to refer to the main stabilization branch, or the default branch. This can also have the name "trunk" or "master", but all is referring back to the same thing.
4 |
5 | The defacto-standard workflow nowadays is the
6 | Pull Request workflow (also known as the
7 | [GitHub flow](https://guides.github.com/introduction/flow/))
8 |
9 | It utilizes the pull request feature to merge code from developer branches to main.
10 |
11 | ## Securing your main branch
12 |
13 | 
14 |
15 | We do not want everybody to be able to push
16 | directly to main, without the CI server checking
17 | the quality of the code. So we need our Git
18 | repository to block incoming pushes directly to
19 | main. In that way we can ensure that the only
20 | way in to main is through a PR. This can be done
21 | in
22 | [GitHub](https://help.github.com/en/github/administering-a-repository/enabling-required-status-checks)
23 | under `settings->Branches`, Branch protection
24 | rules and then click on `add rule`.
25 |
26 | ### Tasks for branch protection
27 |
28 | - Go to your repository on GitHub enter settings.
29 | - Go to `Branches` and add a branch protection rule.
30 | - Give it the pattern `main`.
31 | - `Require status checks to pass before merging`
32 | Will block the pull request from merging until
33 | the tests have passed.
34 | - `Require branches to be up to date before merging`
35 | The branch must be up to date with the base
36 | branch before merging.
37 | - Add the following jobs to the `Status checks that are required` selection: `Build`,`Docker-image`. In that way, no one can push to main without having the tests pass.
38 | - `Do not allow bypassing the above settings` Makes the rules apply
39 | to everyone (yes, you too!).
40 | - OPTIONAL: `Require linear history` requires the
41 | PR branch to be rebased with the target branch,
42 | so a linear history can be obtained.
43 | [Further explanation here](https://www.bitsnbites.eu/a-tidy-linear-git-history/).
44 | This is a very strict way of using git, and is
45 | only here for inspiration for experiments.
46 |
47 | - Try to push to main to verify that you cannot.
48 |
49 | ## Triggering the build on a PR
50 |
51 | So how do we then get the CI server to run the tests on a PR?
52 | Right now our pipeline gets triggered by any push to any branch.
53 |
54 | We want our pipeline to trigger on both pushes and pull requests towards main only.
55 |
56 | The way the pipelines gets triggered is by using the `on` field in the workflow.
57 |
58 | ### Tasks for triggering builds
59 |
60 | - Take a look at the [documentation on what events that can trigger a pipeline](https://docs.github.com/en/actions/reference/events-that-trigger-workflows).
61 |
62 | - By using the resource above, make the pipeline only trigger on pushes and PR's to `main` branch.
63 |
64 |
65 | Hint if you get stuck
66 |
67 | ``` yaml
68 | on:
69 | # Trigger the workflow on push or pull request,
70 | # but only for the main branch
71 | push:
72 | branches:
73 | - main
74 | pull_request:
75 | branches:
76 | - main
77 | ```
78 |
79 |
80 |
81 | - Try to make a couple of different branches that you push up to github and make pull requests on. It could be that you in one broke the unit tests to see that the CI system caught that, and another where you make your branch diverge from what is on main now, triggering the build-in github rules.
82 |
83 | > Note: When you make a pull request on your
84 | > forked repository, GitHub will make target
85 | > branch the main of _the original repo_. You
86 | > need to change it manually to make the pull
87 | > request based on your fork.
88 | >
89 | > 
90 |
91 | Looking at your pull requests, you should see that the pipeline is triggered for each of them, and that only if the tests are passing that you can merge the PR.
92 |
93 | ]
94 |
95 | ## What about the rest?
96 |
97 | You want to run tests on your development branches as well, even if you are not pushing to main right away.
98 |
99 | For that we need to add another workflow to the repository.
100 |
101 | We want to run a slightly shorter workflow, excluding the component and performance tests, and the push of docker images.
102 |
103 | We want to run in on all branches that has the prefix `dev/`.
104 |
105 | ### Tasks
106 |
107 | - Copy your workflow file to a new file called `workflows/dev-workflow.yml`.
108 | - Change the `on` field to:
109 |
110 | ``` yaml
111 | on:
112 | push:
113 | branches:
114 | - "dev/**"
115 | ```
116 |
117 | - delete the `Component-test:` job and the `Performance-test:` jab from the `jobs` section.
118 | - delete the `name: push docker` step from the `Docker-image` job.
119 |
120 | ## Trying it out
121 |
122 | Congratulations! If everything works as intended,
123 | you now have a full "grown up" pipeline, with
124 | conditions, and security that your main branch
125 | always contains tested code. Go ahead and try it
126 | out, to see what it feels like.
127 |
128 | ## Further reading
129 |
130 | -
131 |
--------------------------------------------------------------------------------
/labs/selfhosted-runner.md:
--------------------------------------------------------------------------------
1 | # Local runners
2 |
3 | Github actions provides a lot of minutes you can run your pipeline on their machines.
4 | But it can be that you want to run your pipeline locally as well.
5 |
6 | There are especially two reasons for this:
7 |
8 | * You want faster hardware to run your pipeline, shortening the feedback loop of the development process.
9 | * You have hardware requirements that needs to be met (special network card, USB peripheral, etc.).
10 |
11 | You still reap the benefits of a central CI/CD controller, but have the possibility to run locally.
12 |
13 | We want to take parts of your pipeline and run it locally, to see if that will speed up the process.
14 |
15 | ## Tasks
16 |
17 | You will be running a selfhosted runner on your local machine or on a VM if you have
18 | one available. To verify that the runner works, you will set up and run a workflow that
19 | matches to OS of your local machine.
20 |
21 | ### Setup a workflow to test with
22 |
23 | The first step is to set up and test the workflow that you intend to run locally.
24 | There are separate, prepared workflow files available depending on the OS you
25 | har on your local machine.
26 |
27 | 1. Copy one of the following files to your `.github/workflows` folder:
28 |
29 | * Windows: `labs/selfhosted-runners/hello-world-windows.yml`
30 | * MacOS or Linux: `labs/selfhosted-runners/hello-world-unix.yml`
31 |
32 | The workflows are initially set up to run on appropriate Windows or Linux runners
33 | hosted by GitHub.
34 |
35 | 2. Commit and push the changes, so that the workflow is available in the GitHub UI.
36 | 3. Go the the Actions tab in the GitHub UI and run the `Hello Selfhosted Runner` workflow.
37 | 4. Verify that the workflow has run successfully and examine the output.
38 |
39 | ### Setup a local runner
40 |
41 | Now that you have a workflow to test with, you need to deploy an instance of the GitHub Runner
42 | application on your local machine and configure it to connect to your GitHub account.
43 |
44 | 1. In the `github-actions-katas` repository on your personal GitHub account, click on Settings > Actions > Runners
45 | 2. Click the `"New self-hosted runner"` button in the upper right-hand corner.
46 | 3. Choose an OS and Architecture that matches your local machine
47 | * On Windows and Linux, the achitecuter will likely be `x64`
48 | * On MacOS, the architecture will likely be `ARM64`
49 | 4. Follow the download instructions and configure instructions in a new terminal window.
50 |
51 | The instructions will guide you to
52 |
53 | 1. create a folder for the local runner
54 | 2. download and extract the necessary binary
55 | 3. configure it to access your repository using a token generated as part of the instructions.
56 | Press enter in each step of the configuration wizard to choose the default options.
57 | 4. run the local runner instance
58 |
59 | You should see something like this at the end:
60 |
61 | ```bash
62 | $ ./run.sh
63 |
64 | √ Connected to GitHub
65 |
66 | Current runner version: '2.329.0'
67 | 2025-11-04 21:17:44Z: Listening for Jobs
68 | ```
69 |
70 | Now you are ready to use the runner in your workflow.
71 |
72 | ### Run a workflow against the local runner
73 |
74 | To verify that workflows run on your local runner, we need to update the test workflow
75 | and run it again. We will observe that the log in your local terminal will print
76 | status messages from the workflow.
77 |
78 | 1. Edit the `Hello Selfhosted Runner` workflow file in `.github/workflows`
79 | 2. Change the `runs-on` parameter in the component test from `ubuntu-latest` or
80 | `windows-latest` to `self-hosted`
81 |
82 | ```yaml
83 | hello-world:
84 | runs-on: [self-hosted]
85 | ```
86 |
87 | 3. Commit and push the change, then trigger the workflow manually.
88 | 4. In the terminal where you are running the GitHub Actions runner, you should see something like this:
89 |
90 | ```bash
91 | 2025-11-04 21:18:41Z: Running job: hello-world
92 | 2025-11-04 21:18:51Z: Job hello-world completed with result: Succeeded
93 | ```
94 |
95 | ### Removing the local runner
96 |
97 | Since this is just a test, and you don't want local runners available in public repositories,
98 | you need to remove the runner registration from GitHub and clean up the working directory
99 |
100 | 1. In the `github-actions-katas` repository on your personal GitHub account, click on Settings > Actions > Runners
101 | 2. Click on your newly created runner to open its status page. Here you can see any
102 | currently active jobs on the runner.
103 | 3. Press the big red Remove button. The dialog that appears will show you the necessary
104 | command to de-register the runner.
105 | 4. Copy the command to remove the runner.
106 | 5. Switch to the terminal window where the local runner is active.
107 | 6. Press `Crtl+C` to stop the runner.
108 | 7. Paste the `remove` command into the shell and execute it.
109 | * Observe that the runner is now removed from the GitHub UI.
110 | 8. Finally delete the folder that you created for the runner. This will clean up
111 | any work files and binaries that we have downloaded.
112 |
113 | Congratulations! You have now configured a local runner, used it in your workflow
114 | and cleaned it up again!
115 |
116 | ### Resources
117 |
118 | [Adding self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners)
119 |
--------------------------------------------------------------------------------
/labs/old-labs/reusable.md:
--------------------------------------------------------------------------------
1 | # Reusable workflows
2 |
3 | In this hands-on lab you will create a [reusable workflow](https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow) and a workflow that consumes it.
4 |
5 | You will learn to pass in parameters to the reusable workflow and use output parameters in the consuming workflow.
6 |
7 | This hands on lab consists of the following steps:
8 |
9 | - [Reusable workflows](#reusable-workflows)
10 | - [Creating a reusable workflow](#creating-a-reusable-workflow)
11 | - [Adding an output parameter](#adding-an-output-parameter)
12 | - [Consuming the reusable workflow](#consuming-the-reusable-workflow)
13 | - [Summary](#summary)
14 |
15 | ## Creating a reusable workflow
16 |
17 | 1. Create a [new file](/../../new/main) `.github/workflows/reusable.yml` (paste the file name with the path in the box).
18 | 2. Set the name to `Reusable workflow`.
19 |
20 |
21 | Solution
22 |
23 | ```yaml
24 | name: Reusable workflow
25 | ```
26 |
27 |
28 |
29 | 3. Add a `workflow_call` trigger with an [input parameter](https://docs.github.com/en/enterprise-cloud@latest/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_call) `who-to-greet` of the type `string` that is required. Set the default value to `World`.
30 |
31 |
32 | Solution
33 |
34 | ```yaml
35 | on:
36 | workflow_call:
37 | inputs:
38 | who-to-greet:
39 | description: 'The person to greet'
40 | type: string
41 | required: true
42 | default: World
43 | ```
44 |
45 |
46 |
47 | 4. Add a job named `reusable-job` that runs on `ubuntu-latest` that echos "Hello <input parameter>" to the console.
48 |
49 |
50 | Solution
51 |
52 | ```yaml
53 | jobs:
54 | reusable-job:
55 | runs-on: ubuntu-latest
56 | steps:
57 | - name: Greet someone
58 | run: echo "Hello ${{ inputs.who-to-greet }}"
59 | ```
60 |
61 |
62 |
63 | ## Adding an output parameter
64 |
65 | 1. Add an additional step with the id `time` that uses a workflow command to set an output parameter
66 | named `current-time` to the current date and time (use `$(date)` for that).
67 |
68 |
69 | Solution
70 |
71 | ```yaml
72 | - name: Set time
73 | id: time
74 | run: echo "time=$(date)" >> $GITHUB_OUTPUT
75 | ```
76 |
77 |
78 |
79 | 2. Add an output called `current-time` to the `reusable-job`.
80 |
81 |
82 | Solution
83 |
84 | ```yaml
85 | outputs:
86 | current-time: ${{ steps.time.outputs.time }}
87 | ```
88 |
89 |
90 |
91 | 3. Add an output parameter called `current-time` to `workflow_call` and set it to the outputs of the workflow command.
92 |
93 |
94 | Solution
95 |
96 | ```yaml
97 | outputs:
98 | current-time:
99 | description: 'The time when greeting.'
100 | value: ${{ jobs.reusable-job.outputs.current-time }}
101 | ```
102 |
103 |
104 |
105 |
106 | Complete Solution
107 |
108 | ```yaml
109 | name: Reusable workflow
110 |
111 | on:
112 | workflow_call:
113 | inputs:
114 | who-to-greet:
115 | description: 'The person to greet'
116 | type: string
117 | required: true
118 | default: World
119 | outputs:
120 | current-time:
121 | description: 'The time when greeting.'
122 | value: ${{ jobs.reusable-job.outputs.current-time }}
123 |
124 | jobs:
125 | reusable-job:
126 | runs-on: ubuntu-latest
127 | outputs:
128 | current-time: ${{ steps.time.outputs.time }}
129 | steps:
130 | - name: Greet someone
131 | run: echo "Hello ${{ inputs.who-to-greet }}"
132 | - name: Set time
133 | id: time
134 | run: echo "time=$(date)" >> $GITHUB_OUTPUT
135 | ```
136 |
137 |
138 |
139 | ## Consuming the reusable workflow
140 |
141 | 1. Create a [new file](/../../new/main) `.github/workflows/reuse.yml` (paste the file name with the path in the box).
142 | 2. Set the name to `Reuse other workflow` and add a manual trigger.
143 |
144 |
145 | Solution
146 |
147 | ```yaml
148 | name: Reuse other workflow
149 |
150 | on: [workflow_dispatch]
151 | ```
152 |
153 |
154 |
155 | 3. Add a job `call-workflow` that uses the reusable workflow and passes in your user name as an input parameter.
156 |
157 |
158 | Solution
159 |
160 | ```yaml
161 | jobs:
162 | call-workflow:
163 | uses: ./.github/workflows/reusable.yml
164 | with:
165 | who-to-greet: '@octocat'
166 | ```
167 |
168 |
169 |
170 | 4. Add another job `use-output` that writes the output parameter `current-time` to the console. (Hint: use the needs context to access the output)
171 |
172 |
173 | Solution
174 |
175 | ```yaml
176 | use-output:
177 | runs-on: ubuntu-latest
178 | needs: [call-workflow]
179 | steps:
180 | - run: echo "Time was ${{ needs.call-workflow.outputs.current-time }}"
181 | ```
182 |
183 |
184 |
185 | 5. Run the workflow and observe the output.
186 |
187 | ## Summary
188 |
189 | In this lab you have learned to create a reusable workflow and a workflow that consumes it.
190 |
191 | You also have learned to pass in parameters to the reusable workflow and to use output parameters in the consuming workflow.
192 |
193 | This exercise is part of the [GitHub official Github Actions training](https://github.com/ps-actions-sandbox/ActionsFundamentals)
194 |
--------------------------------------------------------------------------------
/app/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/labs/concurrency.md:
--------------------------------------------------------------------------------
1 | # Workflow Concurrency
2 |
3 | GitHub Actions can run multiple workflows simultaneously, but sometimes you want to control this behavior to prevent conflicts or resource contention. Workflow concurrency allows you to limit the number of workflow runs that can execute at the same time.
4 |
5 | :bulb: Concurrency is particularly useful for deployment workflows where you don't want multiple deployments running simultaneously.
6 |
7 | ## Learning Goals
8 |
9 | - Understand how to configure workflow concurrency groups
10 | - Learn how to cancel in-progress workflows when new ones start
11 | - Practice using workflow artifacts to pass data between jobs
12 | - Experience workflow summaries and output grouping
13 |
14 | ## Understanding Workflow Concurrency
15 |
16 | In this exercise, we will create a workflow that demonstrates concurrency control by:
17 |
18 | - Setting up a concurrency group to manage workflow runs
19 | - Creating jobs that generate and process artifacts
20 | - Using workflow summaries to display results
21 | - Simulating work with delays to observe concurrency behavior
22 |
23 | The workflow will have two jobs that work together: one that generates a directory tree listing and uploads it as an artifact, and another that downloads the artifact and creates a workflow summary.
24 |
25 | [Documentation](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs)
26 |
27 | ## Task
28 |
29 | 1. **Set Up the Workflow File**
30 | Create a new workflow file named `concurrency-lab.yml` in the `.github/workflows` directory of your repository. Set the workflow name to "Concurrency Demo" and configure it to be triggered manually using the `workflow_dispatch` event.
31 |
32 | 2. **Define Concurrency Settings**
33 | Add a concurrency group that uses the workflow name and reference. Enable the option to cancel any in-progress runs if a new one starts.
34 |
35 |
36 | Solution
37 |
38 | ```YAML
39 | concurrency:
40 | group: ${{ github.workflow }}-${{ github.ref }}
41 | cancel-in-progress: true
42 | ```
43 |
44 |
45 |
46 | 3. **Create the 'upload-tree' Job**
47 | Add a job that runs on the latest Ubuntu runner with the following steps:
48 | - Check out the repository
49 | - Generate directory tree using the provided script
50 | - Upload the tree output as an artifact
51 | - Simulate work by pausing for 10 seconds
52 |
53 |
54 | Solution
55 |
56 | ```YAML
57 | upload-tree:
58 | runs-on: ubuntu-latest
59 | steps:
60 | - name: Checkout
61 | uses: actions/checkout@v4
62 |
63 | - name: Generate directory tree
64 | run: helper-scripts/generate-tree.sh
65 |
66 | - name: Upload tree output
67 | uses: actions/upload-artifact@v4
68 | with:
69 | name: tree-output
70 | path: tree.txt
71 |
72 | - name: Simulate work
73 | run: sleep 10
74 | ```
75 |
76 |
77 |
78 | 4. **Create the 'add-summary' Job**
79 | Add a second job that depends on the first job and runs on the latest Ubuntu runner with the following steps:
80 | - Download the tree artifact
81 | - Simulate work by pausing for 10 seconds
82 | - Add a [workflow summary](https://github.blog/news-insights/product-news/supercharging-github-actions-with-job-summaries/) showing the directory tree
83 |
84 |
85 | Solution
86 |
87 | ```YAML
88 | add-summary:
89 | runs-on: ubuntu-latest
90 | needs: upload-tree
91 | steps:
92 | - name: Download tree output
93 | uses: actions/download-artifact@v4
94 | with:
95 | name: tree-output
96 |
97 | - name: Simulate work
98 | run: sleep 10
99 |
100 | - name: Add job summary with tree output
101 | run: |
102 | echo "### Job completed! :rocket:" >> $GITHUB_STEP_SUMMARY
103 | echo '### Project Directory Tree' >> $GITHUB_STEP_SUMMARY
104 | echo '```' >> $GITHUB_STEP_SUMMARY
105 | cat tree.txt >> $GITHUB_STEP_SUMMARY
106 | echo '```' >> $GITHUB_STEP_SUMMARY
107 | ```
108 |
109 |
110 |
111 | 5. **Test the Concurrency Behavior**
112 | Save and commit the workflow file, then go to the Actions tab in your GitHub repository. Trigger the workflow twice in quick succession to observe that only the jobs from the last triggered workflow will run, as earlier runs will be cancelled due to the concurrency settings.
113 |
114 |
115 | :bulb: Git commands to commit the files
116 |
117 | ```bash
118 | git add .github/workflows/concurrency-lab.yml ci/generate-tree.sh
119 | git commit -m "Add concurrency workflow demonstration"
120 | git push
121 | ```
122 |
123 |
124 |
125 | ### Results
126 |
127 | You should see that when you trigger the workflow multiple times rapidly:
128 | - Only the most recent workflow run completes
129 | - Previous runs are cancelled automatically
130 | - The workflow summary displays the project directory tree
131 | - The concurrency group prevents multiple simultaneous runs
132 |
133 |
134 | Complete Solution
135 |
136 | ```YAML
137 | name: Concurrency Demo
138 |
139 | on:
140 | workflow_dispatch:
141 |
142 | concurrency:
143 | group: ${{ github.workflow }}-${{ github.ref }}
144 | cancel-in-progress: true
145 |
146 | jobs:
147 | upload-tree:
148 | runs-on: ubuntu-latest
149 | steps:
150 | - name: Checkout
151 | uses: actions/checkout@v4
152 |
153 | - name: Generate directory tree
154 | run: helper-scripts/generate-tree.sh
155 |
156 | - name: Upload tree output
157 | uses: actions/upload-artifact@v4
158 | with:
159 | name: tree-output
160 | path: tree.txt
161 |
162 | - name: Simulate work
163 | run: sleep 10
164 |
165 | add-summary:
166 | runs-on: ubuntu-latest
167 | needs: upload-tree
168 | steps:
169 | - name: Download tree output
170 | uses: actions/download-artifact@v4
171 | with:
172 | name: tree-output
173 |
174 | - name: Simulate work
175 | run: sleep 10
176 |
177 | - name: Add job summary with tree output
178 | run: |
179 | echo "### Job completed! :rocket:" >> $GITHUB_STEP_SUMMARY
180 | echo '### Project Directory Tree' >> $GITHUB_STEP_SUMMARY
181 | echo '```' >> $GITHUB_STEP_SUMMARY
182 | cat tree.txt >> $GITHUB_STEP_SUMMARY
183 | echo '```' >> $GITHUB_STEP_SUMMARY
184 | ```
185 |
186 |
187 |
188 | ## Summary
189 |
190 | Congratulations! You have successfully implemented workflow concurrency controls. You've learned how to:
191 |
192 | - Configure concurrency groups to prevent simultaneous workflow runs
193 | - Use the `cancel-in-progress` option to automatically cancel outdated runs
194 | - Work with artifacts to pass data between jobs
195 | - Create workflow summaries for better visibility into your pipeline results
196 |
197 | This concurrency pattern is especially valuable for deployment workflows where you want to ensure only one deployment happens at a time, preventing conflicts and ensuring consistency.
198 |
199 |
--------------------------------------------------------------------------------
/labs/docker-image.md:
--------------------------------------------------------------------------------
1 | # Building Docker Images
2 |
3 | Next step is to have our application packaged as a docker image for easy distribution.
4 |
5 | We have some requirements for our pipeline step:
6 |
7 | - Should build our application as a docker image.
8 | - Should tag the image with both the git sha and "latest". (Do not use such general tags in real life!)
9 | - Should push the image to Githubs docker registry.
10 |
11 | In order for this to work, we need three environment variables:
12 |
13 | - `github_username` the username for the GitHub Container Registry (usually the GitHub actor).
14 | - `github_password` the password/token used to authenticate to the registry (usually a token).
15 | - `GIT_COMMIT` the name of the git commit that is being built.
16 |
17 | You can set these environment variables as global variables in your workflow through the `env` section.
18 |
19 | ```yaml
20 | env:
21 | github_username:
22 | github_password:
23 | GIT_COMMIT:
24 | ```
25 |
26 | The two scripts: `ci/build-docker.sh` and `ci/push-docker.sh` expect all three environment variables to be set.
27 |
28 | ## Build-in environment variables
29 |
30 | Many of the common information pieces for a build is set in default environment variables.
31 |
32 | Examples of these are:
33 |
34 | - The name of the repository
35 | - The name of the branch
36 | - The SHA of the commit
37 |
38 | You can see the ones you can use directly inside a step here:
39 |
40 | Github Actions also has a list of contexts.
41 |
42 | Contexts are a way to access information about workflow runs, runner environments, jobs, and steps.
43 | Each context is an object that contains properties, which can be strings or other objects.
44 | You can see them here:
45 |
46 | The default environment variables that GitHub sets are available to every step in a workflow.
47 | Contexts are also available before the steps, as when defining the `env` section of the workflow.
48 |
49 | ### Tasks
50 |
51 | - Add a new job named `Docker-image` that requires the `Build` to be completed.
52 | You need to add package write permissions so that your action can upload the container to the registry.
53 |
54 | ```yaml
55 | Docker-image:
56 | runs-on: ubuntu-latest
57 | needs: [Build]
58 | permissions:
59 | packages: write
60 | ```
61 |
62 | In order for us to create and push the docker image, we need the CI scripts, the Dockerfile and the Artifact. All of them are present in the `code` artifact created in the last exercise.
63 |
64 | - Add a step in `Docker-image` which downloads the `code` artifact.
65 |
66 |
67 | :bulb: Hint on how it looks like
68 |
69 | ```yaml
70 | - name: Download code
71 | uses: actions/download-artifact@v4
72 | with:
73 | name: code
74 | path: .
75 | ```
76 |
77 |
78 |
79 | - Add `github_username` and `github_password` as environmental variables on top of the workflow file.
80 |
81 | ```yaml
82 | name: Main workflow
83 | on: push
84 | env: # Set the secret as an input
85 | github_username: ${{ github.actor }}
86 | github_password: ${{ secrets.GITHUB_TOKEN }} # Must be made available to the workflow
87 | jobs:
88 | Build:
89 | ```
90 |
91 | > :bulb: The `github_username` should typically be set to the `github.actor` if you want to provide it dynamically to the runner. Otherwise you can hardcode it to your username.
92 | >
93 | > **NOTE**: If your GitHub username **contains uppercase letters**, provide it explicitly in lowercase in the workflow instead of using the built-in `github.actor`!
94 | > The reason is that image name components (including the owner/namespace) must be lowercase. This restriction is from Docker/OCI image naming rules. GitHub uses the repository owners name as the image namespace. Since GitHub usernames are case-insensitive, this will cause problems if they include uppercase letters.
95 |
96 | ```yaml
97 | # Do this if your GitHub username contains uppercase letters, e.g. "Elmeri":
98 | # github_username: ${{ github.actor }}
99 | github_username: elmeri # use lowercase for the registry/imagename component
100 | ```
101 |
102 |
103 | Optional: Check for uppercase letters in github_username
104 |
105 | ```yaml
106 | - name: Validate Docker username is all lowercase
107 | id: validate_lower
108 | run: |
109 | if [[ "${{ env.github_username }}" =~ [A-Z] ]]; then
110 | echo "::error::Validation Failed: GitHub username '${{ env.github_username }}' cannot contain uppercase characters."
111 | exit 1
112 | else
113 | echo "Docker username format is valid."
114 | fi
115 | shell: bash
116 | ```
117 |
118 |
119 |
120 | - Add GIT_COMMIT environment variable as well, that should contain the commit sha of the repository.
121 |
122 | > Tip! it needs the same "wrapping" (`${{}}`) as the other environment variables, and can be found in the `github` [context](https://docs.github.com/en/actions/learn-github-actions/contexts#about-contexts).
123 |
124 | - Run the `ci/build-docker.sh` and `ci/push-docker.sh` scripts.
125 |
126 | Ready steps looks like:
127 |
128 | ```yaml
129 | - name: build docker
130 | run: bash ci/build-docker.sh
131 | - name: push docker
132 | run: bash ci/push-docker.sh
133 | ```
134 |
135 | > Hint: The reason we have bash first is to bypass the file permissions. If you don't do this, you will get a permission denied error.
136 |
137 | - Submit your changes, and see that the image is built and pushed to the GitHub container registry.
138 |
139 | > Tip! You can find the image under the `Packages` tab on your profile.
140 |
141 | ## Using actions instead of scripts
142 |
143 | The above job can be also done by using actions: `docker/login-action@v3` and `docker/build-push-action@v5`, what will provide the same functionality. You can find it in the example below:
144 |
145 |
146 | Doing the same using Actions
147 |
148 | ```yaml
149 | on: push
150 | jobs:
151 | build-and-push-latest:
152 | runs-on: ubuntu-latest
153 | permissions:
154 | packages: write
155 | steps:
156 | - name: Login to DockerHub
157 | uses: docker/login-action@v3
158 | with:
159 | registry: ghcr.io
160 | username: ${{ github.actor }}
161 | password: ${{ secrets.GITHUB_TOKEN }}
162 | - name: Build and push
163 | uses: docker/build-push-action@v5
164 | with:
165 | context: app
166 | push: true
167 | tags: ghcr.io/${{ github.actor }}/micronaut-app:1.0-${{ github.sha }},ghcr.io/${{ github.actor }}/micronaut-app:latest
168 | ```
169 |
170 |
171 |
172 | ### Solution
173 |
174 | If you struggle and need to see the whole ***Solution*** you can click this [trainer's docker-image.yaml](../trainer/.github/workflows/docker-image.yaml)
175 |
176 | ### Results
177 |
178 | You should be able to see your docker image on your GitHub account as:
179 |
180 | 
181 |
--------------------------------------------------------------------------------
/labs/storing-artifacts.md:
--------------------------------------------------------------------------------
1 | # Storing artifacts
2 |
3 | ## Learning goal
4 |
5 | - Create multiple jobs
6 | - Use the Upload and download artifact action
7 | - Use Superlinter to lint your sourcecode
8 |
9 | ### Super linter
10 |
11 | Super-linter is a tool that can be used to lint your sourcecode. It is a combination of multiple linters, and can be used to lint multiple languages.
12 |
13 | It's invoked as a GitHub action, and can be found on [GitHub Marketplace](https://github.com/super-linter/super-linter).
14 |
15 | In this exercise we will use it to lint our sourcecode in a separate job.
16 |
17 | ### Upload and download artifacts
18 |
19 | When running multiple jobs in a workflow, the runner you get for each job is completely new.
20 |
21 | This means that any files created during a job, are not available in the next job, including the
22 | repository files.
23 |
24 | > :bulb: The following should not be mistaken for proper [artifact management](https://www.eficode.com/blog/artifactory-nexus-proget), or [release management](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) but it is useful for making the artifacts built by the pipeline available.
25 |
26 | To deal with artifacts, a `GitHub Actions Action` can be used, which can be found on [GitHub Marketplace](https://github.com/marketplace).
27 |
28 | To upload artifacts use the following syntax with `actions/upload-artifact@v4` [Link to documentation](https://github.com/marketplace/actions/upload-a-build-artifact):
29 |
30 | ```YAML
31 | - name: Upload a Build Artifact # Name of the step
32 | uses: actions/upload-artifact@v4 # Action to use
33 | with: # Parameters for the action
34 | name: my-artifact # Name of the artifact to upload. Optional. Default is 'artifact
35 | path: path/to/artifact/ # A file, directory or wildcard pattern that describes what to upload. Required.
36 | ```
37 |
38 | As artifacts can be uploaded it can also be downloaded from GitHub Actions with help of `actions/download-artifact@v4` as:
39 |
40 | ```YAML
41 | - name: Download a single artifact # Name of the step
42 | uses: actions/download-artifact@v4 # Action to use
43 | with: # Parameters for the action
44 | name: my-artifact # Name of the artifact to download. Optional. If unspecified, all artifacts for the run are downloaded.
45 | path: path/to/download/artifact/ # Destination path. Supports basic tilde expansion. # Optional. Default is $GITHUB_WORKSPACE
46 | ```
47 |
48 | You can find more information around the different parameters in the
49 | [documentation for the download-artifact action](https://github.com/actions/download-artifact).
50 |
51 | :bulb:
52 |
53 | More information about storing artifacts
54 | GitHub has an excellent guide on how you can use persistent storage over periods of builds here: https://docs.github.com/en/actions/guides/storing-workflow-data-as-artifacts
55 |
56 |
57 | ## Exercise
58 |
59 | ### Overview
60 |
61 | - Create a linter job
62 | - Use the `actions/upload-artifact` and `actions/download-artifact`
63 | - Use `super-linter/super-linter/slim` to lint your sourcecode
64 |
65 | ### Tasks
66 |
67 | - Add step named `Upload repo` to the existing job, which will upload an artifact with the name `code`, with the path `.` to use the current directory.
68 |
69 | ```YAML
70 | - uses: actions/upload-artifact@v4
71 | with:
72 | name: code
73 | path: .
74 | include-hidden-files: true
75 | ```
76 |
77 |
78 | complete solution
79 |
80 | ```YAML
81 | name: Main workflow
82 | on: push
83 | jobs:
84 | Build:
85 | runs-on: ubuntu-latest
86 | container: gradle:6-jdk11
87 | steps:
88 | - name: Clone down repository
89 | uses: actions/checkout@v4
90 | - name: Build application
91 | run: ci/build-app.sh
92 | - name: Test
93 | run: ci/unit-test-app.sh
94 | - name: Upload repo
95 | uses: actions/upload-artifact@v4
96 | with:
97 | name: code
98 | path: .
99 | include-hidden-files: true
100 | ```
101 |
102 |
103 |
104 | Commit and push that to your repository and check the actions tab.
105 |
106 | If all works out fine, your newest build should show something like, where you can find your uploaded artifact:
107 | 
108 |
109 | #### Super-linter
110 |
111 | We will now create a new job, which will use super-linter to lint our sourcecode.
112 |
113 | - add a new job named `Linting` to your workflow
114 | - Like the other job it will run on `ubuntu-latest`
115 | - It `needs` the `Build` step. Add a line under `runs-on` with `needs: [Build]`
116 | - It will have two steps, `Download code` and `Run linting`
117 | - `Download code` uses the `actions/download-artifact@v4` action to download the artifact `code` to the current directory `.`
118 | - `Run linting` uses the `super-linter/super-linter/slim@v7` action to lint the code. It needs two environment variables to work:
119 | - `DEFAULT_BRANCH` which should be set to `main`
120 | - `GITHUB_TOKEN` which should be set to `${{ secrets.GITHUB_TOKEN }}`
121 |
122 |
123 | complete solution
124 |
125 | ```YAML
126 | Linting:
127 | runs-on: ubuntu-latest
128 | needs: [Build]
129 | steps:
130 | - name: Download code
131 | uses: actions/download-artifact@v4
132 | with:
133 | name: code
134 | path: .
135 | - name: run linting
136 | uses: super-linter/super-linter/slim@v7
137 | env:
138 | DEFAULT_BRANCH: main
139 | # To report GitHub Actions status checks
140 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
141 | ```
142 |
143 |
144 |
145 | Push that up to your repository and check the actions tab.
146 |
147 | Ohh no! the linting failed! What happened?
148 |
149 | The log should show something like this:
150 |
151 | ```bash
152 | 2024-01-31 10:50:44 [INFO] ----------------------------------------------
153 | 2024-01-31 10:50:44 [INFO] ----------------------------------------------
154 | 2024-01-31 10:50:44 [INFO] The script has completed
155 | 2024-01-31 10:50:44 [INFO] ----------------------------------------------
156 | 2024-01-31 10:50:44 [INFO] ----------------------------------------------
157 | 2024-01-31 10:50:44 [ERROR] ERRORS FOUND in BASH:[3]
158 | 2024-01-31 10:50:45 [ERROR] ERRORS FOUND in DOCKERFILE_HADOLINT:[1]
159 | 2024-01-31 10:50:45 [ERROR] ERRORS FOUND in GITHUB_ACTIONS:[2]
160 | 2024-01-31 10:50:45 [ERROR] ERRORS FOUND in GOOGLE_JAVA_FORMAT:[5]
161 | 2024-01-31 10:50:46 [ERROR] ERRORS FOUND in JAVA:[5]
162 | 2024-01-31 10:50:46 [ERROR] ERRORS FOUND in JAVASCRIPT_STANDARD:[1]
163 | 2024-01-31 10:50:46 [ERROR] ERRORS FOUND in JSCPD:[1]
164 | 2024-01-31 10:50:47 [ERROR] ERRORS FOUND in MARKDOWN:[12]
165 | 2024-01-31 10:50:47 [ERROR] ERRORS FOUND in NATURAL_LANGUAGE:[9]
166 | 2024-01-31 10:50:47 [ERROR] ERRORS FOUND in PYTHON_BLACK:[1]
167 | 2024-01-31 10:50:47 [ERROR] ERRORS FOUND in PYTHON_FLAKE8:[1]
168 | 2024-01-31 10:50:48 [ERROR] ERRORS FOUND in PYTHON_ISORT:[1]
169 | 2024-01-31 10:50:48 [FATAL] Exiting with errors found!
170 | ```
171 |
172 | It seems like we have some linting errors in our code. As this is not a python/bash/javascript course, we will not fix them, but silence the linter with another environment variable.
173 |
174 | - Add the environment variable `DISABLE_ERRORS` to the `run linting` step, and set it to `true`
175 |
176 | ```YAML
177 | - name: run linting
178 | uses: super-linter/super-linter/slim@v7
179 | env:
180 | DEFAULT_BRANCH: main
181 | # To report GitHub Actions status checks
182 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
183 | DISABLE_ERRORS: true
184 | ```
185 |
186 | Push that up to your repository and see that the linting now passes, even though we have errors in our code.
187 |
188 | > :bulb: Disabling linting errors is of course not the correct solution in a real project, but for
189 | > this exercise it will have to do.
190 |
191 | Congratulations! You have now created a workflow with multiple jobs, and used artifacts to share data between them.
192 |
193 | ### Resources
194 |
195 |
196 |
--------------------------------------------------------------------------------
/labs/docker-action.md:
--------------------------------------------------------------------------------
1 | # Custom Docker Action
2 |
3 | In this hands-on lab you will create a custom Docker action and use it in a workflow.
4 |
5 | ## Learning Goals
6 |
7 | - Understand how to create a custom Docker action
8 | - Learn how to define action `inputs` and `outputs`
9 | - Understand how to create an `action.yml` metadata file
10 | - Understand how to test custom actions in workflows
11 | - Learn how to publish and version custom actions
12 |
13 | ## Introduction
14 |
15 | GitHub Actions allows you to create custom actions to encapsulate reusable functionality. There are three types of actions:
16 |
17 | - **Docker container actions** - Runs a container from a local `Dockerfile` or from a public registry. Ideal for specific environments, dependencies, and ensuring consistency with containerization.
18 | - **JavaScript actions** - Runs a JavaScript file directly on the GitHub Actions runner. Good for complex logic, API interactions, and leveraging Node.js libraries.
19 | - **Composite actions** - Combine multiple workflow steps into a single action. This is useful for reusing workflows and encapsulating common tasks.
20 |
21 | In this exercise, we'll focus on creating a Docker container action, which provides a consistent and reliable environment for your action code.
22 |
23 | ## Exercise
24 |
25 | ### Overview
26 |
27 | In this exercise you will:
28 |
29 | - Create a Docker-based custom action that greets a person
30 | - Define input and output parameters for the action
31 | - Create the necessary files (action.yml, Dockerfile, entrypoint.sh)
32 | - Test the action in a workflow
33 | - Optional: Publish the action for reuse
34 |
35 | ### Step by step instructions
36 |
37 | #### Create the action script
38 |
39 | The first step is to create a script that we will be running within the Docker container.
40 | This could contain a tool that requires complex dependenciex, but to keep it simple, we
41 | will use a simple shell script. The script will take a name as input and will output
42 | the current time in `$GITHUB_OUTPUT`.
43 |
44 | 1. Create a new directory called `hello-world-docker-action` in the root of your repository.
45 |
46 | 2. Create an `entrypoint.sh` script inside this directory. This will contain the logic.
47 |
48 | 3. Print a greeting using the input parameter to the console. GitHub Actions passes inputs to Docker container actions as command line arguments when you specify them in the `args` section of the action.yml file.
49 |
50 | 4. Now capture the output from the Linux `date` command into a `$time` variable.
51 |
52 | 5. Add `time=$time` to the file whose path is stored in `$GITHUB_OUTPUT`.
53 |
54 | 6. Make the script executable and test it locally by running it directly with a test argument.
55 |
56 |
57 | Solution
58 |
59 | ```bash
60 | #!/bin/bash
61 |
62 | echo "Hello, $1!"
63 | time=$(date)
64 | echo "time=$time" >> $GITHUB_OUTPUT
65 | ```
66 |
67 | To test the script locally:
68 |
69 | ```bash
70 | cd hello-world-docker-action
71 | chmod +x entrypoint.sh
72 | export GITHUB_OUTPUT=/tmp/github_output
73 | ./entrypoint.sh "World"
74 | cat $GITHUB_OUTPUT # Check the output was written
75 | ```
76 |
77 | > :bulb: We export the `GITHUB_OUTPUT` environment variable locally so the script can write to it during testing.
78 |
79 |
80 |
81 | > :bulb: The entrypoint script receives inputs as command line arguments. We use `$GITHUB_OUTPUT` to set output parameters that can be used by other steps in the workflow.
82 |
83 | #### Create the Dockerfile
84 |
85 | Now that we have a script that does what we want, let us create the Docker container that will
86 | run the tool in a controlled environment. This is a fairly simple `Dockerfile`, so feel
87 | free to copy the contents from the "soltion" section below.
88 |
89 | 1. In the same directory as before, create a `Dockerfile` that will define the container environment.
90 |
91 | 2. Copy the following content into the `Dockerfile`. It will ensure that the script
92 | and its dependencies are available within the container and that the container
93 | is set to call the script at startup.
94 |
95 | ```dockerfile
96 | FROM alpine:3.22
97 |
98 | # Install dependencies
99 | RUN apk --no-cache add bash
100 |
101 | # Copy the entrypoint script
102 | COPY entrypoint.sh /entrypoint.sh
103 |
104 | # Make the script executable
105 | RUN chmod +x /entrypoint.sh
106 |
107 | # Set the entrypoint
108 | ENTRYPOINT ["/entrypoint.sh"]
109 | ```
110 |
111 | 3. If you have Docker on your local machine, you can now test the `Dockerfile` by
112 | building and running the container.
113 |
114 |
115 | Solution
116 |
117 | To test the Dockerfile locally:
118 |
119 | ```bash
120 | cd hello-world-docker-action
121 | docker build -t hello-world-action .
122 | docker run -e GITHUB_OUTPUT=/tmp/github_output hello-world-action "Docker Test"
123 | ```
124 |
125 | > :bulb: We set the `GITHUB_OUTPUT` environment variable to a temporary file path so the script doesn't fail when writing the output locally.
126 |
127 |
128 |
129 | #### Create the action metadata file
130 |
131 | With the `Dockerfile` definition in place, we can add the `action.yml` metadata that makes
132 | this into a GitHub Action.
133 |
134 | 1. Create an `action.yml` file in the same directory as before.
135 |
136 | 2. Define the name and description for your action.
137 |
138 | 3. Add an input parameter called `who-to-greet` that is required and has a default value of `World`.
139 |
140 | 4. Add an output parameter called `time` that will contain the timestamp from your script.
141 |
142 | 5. Configure the action to run using Docker with the local `Dockerfile`.
143 |
144 | 6. Add an `args` section to pass the `who-to-greet` input as a command line argument to your container.
145 |
146 |
147 | Solution
148 |
149 | ```yaml
150 | name: 'Hello World Docker Action'
151 | description: 'Greet someone and record the time'
152 |
153 | inputs:
154 | who-to-greet:
155 | description: 'Who to greet'
156 | required: true
157 | default: 'World'
158 |
159 | outputs:
160 | time:
161 | description: 'The time we greeted you'
162 |
163 | runs:
164 | using: 'docker'
165 | image: 'Dockerfile'
166 | args:
167 | - ${{ inputs.who-to-greet }}
168 | ```
169 |
170 |
171 |
172 | > :bulb: The `action.yml` file defines the interface of your action. It specifies the inputs, outputs, and how the action should be run.
173 |
174 | #### Create a workflow to test the action
175 |
176 | 1. Create a workflow file `.github/workflows/test-docker-action.yml` to test your custom action.
177 |
178 | 2. Configure the workflow to trigger on pushes that change files in the `hello-world-docker-action` directory.
179 |
180 | 3. Add a manual trigger (workflow_dispatch) for testing purposes.
181 |
182 | 4. Create a job that runs on `ubuntu-latest`.
183 |
184 | 5. Add a step to checkout the repository.
185 |
186 | 6. Add a step to use your custom action with a specific greeting target.
187 | - Set an `id` for the step to ensure that it can be referenced later on.
188 | - Like other GitHub Actions, your custom action will be called with `uses:`.
189 | Point to the folder containing the action (`./hello-world-docker-action`)
190 | - Parameters to your action is defined in the `with:` section.
191 |
192 | 7. Add a step to display the output from your action.
193 |
194 | 8. Commit all files and push to trigger the workflow.
195 |
196 |
197 | Solution
198 |
199 | ```yaml
200 | name: Test Custom Action
201 |
202 | on:
203 | push:
204 | paths:
205 | - 'hello-world-docker-action/**'
206 | workflow_dispatch:
207 |
208 | jobs:
209 | test-action:
210 | runs-on: ubuntu-latest
211 | steps:
212 | - name: Checkout repository
213 | uses: actions/checkout@v4
214 |
215 | - name: Test custom action
216 | id: hello
217 | uses: ./hello-world-docker-action
218 | with:
219 | who-to-greet: 'GitHub Actions Student'
220 |
221 | - name: Use the output
222 | run: echo "The time was ${{ steps.hello.outputs.time }}"
223 | ```
224 |
225 | To commit and test:
226 |
227 | ```bash
228 | git add .
229 | git commit -m "Add custom Docker action"
230 | git push
231 | ```
232 |
233 |
234 |
235 | #### Advanced: Add more functionality
236 |
237 | 1. **Optional:** Enhance your action by adding more input parameters. Modify the `action.yml` to accept a `greeting-type` input:
238 |
239 |
240 | Solution
241 |
242 | ```yaml
243 | name: 'Hello World Docker Action'
244 | description: 'Greet someone and record the time'
245 |
246 | inputs:
247 | who-to-greet:
248 | description: 'Who to greet'
249 | required: true
250 | default: 'World'
251 | greeting-type:
252 | description: 'Type of greeting (hello, hi, hey)'
253 | required: false
254 | default: 'Hello'
255 |
256 | outputs:
257 | time:
258 | description: 'The time we greeted you'
259 | message:
260 | description: 'The full greeting message'
261 |
262 | runs:
263 | using: 'docker'
264 | image: 'Dockerfile'
265 | args:
266 | - ${{ inputs.greeting-type }}
267 | - ${{ inputs.who-to-greet }}
268 | ```
269 |
270 |
271 |
272 | 2. **Optional:** Update the `entrypoint.sh` script to use the new parameter:
273 |
274 |
275 | Solution
276 |
277 | ```bash
278 | #!/bin/bash
279 |
280 | greeting_type="$1"
281 | who_to_greet="$2"
282 |
283 | message="$greeting_type $who_to_greet"
284 | echo "$message"
285 |
286 | time=$(date)
287 | echo "time=$time" >> $GITHUB_OUTPUT
288 | echo "message=$message" >> $GITHUB_OUTPUT
289 | ```
290 |
291 |
292 |
293 | 3. **Optional:** Update your workflow to use the new input parameter and display the `message` output:
294 |
295 |
296 | Solution
297 |
298 | ```yaml
299 | name: Test Custom Action
300 |
301 | on:
302 | push:
303 | paths:
304 | - 'hello-world-docker-action/**'
305 | workflow_dispatch:
306 |
307 | jobs:
308 | test-action:
309 | runs-on: ubuntu-latest
310 | steps:
311 | - name: Checkout repository
312 | uses: actions/checkout@v4
313 |
314 | - name: Test custom action
315 | id: hello
316 | uses: ./hello-world-docker-action
317 | with:
318 | who-to-greet: 'GitHub Actions Student'
319 | greeting-type: 'Hi'
320 |
321 | - name: Use the outputs
322 | run: |
323 | echo "The time was: ${{ steps.hello.outputs.time }}"
324 | echo "The message was: ${{ steps.hello.outputs.message }}"
325 | ```
326 |
327 |
328 |
329 | ## Summary
330 |
331 | In this lab you have learned how to:
332 |
333 | - Create a custom Docker action with metadata file
334 | - Define input and output parameters for actions
335 | - Use a Dockerfile to containerize action logic
336 | - Test custom actions in GitHub workflows
337 | - Understand the basics of action publishing and versioning
338 |
339 | Custom actions are powerful tools for creating reusable automation components that can be shared across teams and projects.
340 |
341 | This exercise is inspired by the [GitHub official GitHub Actions training](https://github.com/ps-actions-sandbox/ActionsFundamentals)
342 |
--------------------------------------------------------------------------------