├── 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 | ![Use this template](../img/template.png) 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 | ![hello-world](../img/hello-world.png) 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 | ![Screenshot workflow](img/create-organization.png) 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 | ![Screenshot variables & secrets](img/secrets-and-variables.png) 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 | ![workflow secret output](img/workflow-secret.png) 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 | ![Securing your branch](../img/branch-protection.png) 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 | > ![Change pull request base branch](../img/pr-chooser.png) 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 | ![PR with passing tests](../img/actions-checks.png)] 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 | ![GitHub Container Registry](img/github-container.png) 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 | ![Uploading artifact](img/storing-artifact.png) 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 | --------------------------------------------------------------------------------