Fall Semester 2025 - Supplementary Exercise 4
- DUE: November 19 (Wednesday), 2025 before start of class
During the semester, we learned various ways in which we can automate testing. But all that automation is of no use if your software organization as a whole does not invoke those automated test scripts diligently. Preferably, those test scripts should be run before every single source code change to the repository, and for good measure, regularly every night or every weekend just in case. Now, there are many reasons why this does not happen if left to individual developers:
-
Developers are human beings so they forget. Or, they remember to run some tests, but not all the test suites that are relevant to the changes they have made.
-
Developers are sometimes on a tight schedule, so they are tempted to skip testing that may delay them, especially if they are not automated. They justify their actions by telling themselves that they will fix the failing tests "as soon as possible", or that the test cases are not testing anything important, or that failing test cases in modules under the purview of another team "is not my problem".
In Part 1 of this exercise, we will learn how to build an automated "pipeline" of events that get triggered automatically under certain conditions (e.g. a source code push). A pipeline can automate the entire process from source code push to software delivery to end users, making sure that a suite of tests are invooked as part of the process before software is delivered. Pipelines that are built for this purpose are called CI/CD (Continuous Integration / Continuous Delivery) pipelines, because they enable continuous delivery of software to the end user at at high velocity while still maintaining high quality. We will learn how to build a fully functioning pipeline for the (Rent-A-Cat application)[../exercises/2] that we tested for Exercise 2 on our GitHub repository.
In Part 2, we will learn how to use dockers to both test and deploy software as part of a CI/CD pipeline. Dockers are virtualized execution environments which can emulate the execution environments in the deployment sites (OS, libraries, webservers, databases, etc.) so that software can be tested in situ. In our case, we will create a docker image out of the (Rent-A-Cat website)[cs1632.appspot.com] that we tested for Deliverable 3 for testing and deployment.
GitHub Classroom Link: TBD
In Part 1, you will learn how to create a pipeline from scratch based on the Rent-A-Cat application for Exercise 2, using the CI/CD support provided by your GitHub repository through GitHub Actions. GitHub Actions is just one example CI/CD framework. Other widely used CI/CD frameworks include GitLab Pipelines and Jenkins. Regardless of which you choose, they work in mostly similar ways: there is a YAML configuration file that describes actions in the pipeline and the actions are performed on one or more Runner machines which are typically Docker containers. The only thing that differs is the YAML file syntax. Hence, by learning GitHub Actions, you will be able to translate that knowledge to other frameworks as well. In GitHub Actions lingo, pipelines are called workflows, and the two terms will be used interchangeably.
Let's first start with a very basic workflow which prints "Hello World" inside the Runner. Click on the "Actions" menu on your GitHub repository webpage (it is near the top). Then click on the "New workflow" button on the left hand side. You will be presented with a plethora of "starter" workflows for different purposes. Search for "manual" in the search box and you should see a single workflow named "Manual workflow" by GitHub Actions in the search results. Click on the "Configure" button on the workflow. Then click on the "Start commit" button once you are done reviewing the workflow and then commit the file. Note that this creates a workflow YAML file ".github/workflows/manual.yml" under your repository.
Please refer to the following tutorial to see exactly where to click: https://docs.github.com/en/actions/using-workflows/using-starter-workflows
Now let's take a close look at the manual.yml YAML file. At below are the file contents:
# This is a basic workflow that is manually triggered
name: Manual workflow
# Controls when the action will run. Workflow runs when manually triggered using the UI or API.
on:
workflow_dispatch:
# Inputs the workflow accepts.
inputs:
name:
# Friendly description to be shown in the UI instead of 'name'
description: 'Person to greet'
# Default value if no value is explicitly provided
default: 'World'
# Input has to be provided for the workflow to run
required: true
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "greet"
greet:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Runs a single command using the runners shell
- name: Send greeting
run: echo "Hello ${{ github.event.inputs.name }}"
Please read the comments starting with # carefully. In essence, a workflow is composed of one or more jobs. Jobs are run on individual Runners (in parallel by default). A job is composed of one or more sequential steps which are performed on the same Runner. The "Manual workflow" is composed of a single job named "greet" which is in turn composed of a single step "Send greeting".
Now let's put this workflow into action by executing it on a Runner! click on the "Actions" menu again and you should see a new workflow named "Manual workflow" on your lefthand side. Click on it. Then click on the "Run workflow" button. You will get a pop up with an option to change "Person to greet". Leave everything as-is and click on the green "Run workflow" button again. After a couple of seconds, you will see a new "Manual workflow" run appear on the list of runs with an orange dot and the orange dot will soon turn into a green checkmark. The orange dot indicates that the workflow is under execution and the green checkmark indicates that it completed successfully. A failure is indicated by a red crossmark (which we will encounter later). Now click on the "Manual workflow" run link to see the details of the run. You should see a screen that looks like this:
The screen shows an overview of the workflow. The workflow is composed of a single "greet" job as configured in the YAML file. The green checkmark beside the job indicates success. Now click on the "greet" job to peek into the job and you should see the below screen:
As you see, the job is composed of 3 steps: "Set up job", "Send greeting", and "Complete job". The steps "Setup job" and "Complete job" are implicitly inserted into every job even though they are not specified in the YAML file. I've expanded the first two steps for viewing. The purpose of "Set up job" is to set up the Runner Docker container within which the job is to run. You can see that the container was created using the ubuntu-20.04 Docker image, and that is because "run-on: ubuntu-latest" was specified on the YAML file and 20.04 happened to tbe latest version. The "Send greeting" step runs the commandline specified in the "run:" entry in the YAML file.
The reason that we were able to trigger this workflow manually was because of the following lines on the YAML file:
on:
workflow_dispatch:
Typically, workflows are triggered automatically in response to some repository event but it is useful to be able to sometimes trigger them manually.
Again, refer to the below tutorial if you are confused on where to click: https://docs.github.com/en/actions/quickstart
Now let's get down to business and try writing a CI workflow for our Maven project. This time we are going to have two jobs: 1) A "maven_test" job for invoking "mvn test" on our project and 2) A "update_dependence_graph" job for updating the package dependence graph in your GitHub repository. The dependence graph is used by a GitHub bot called Dependabot to notify the repository owner of packages used by the repository that have become stale or have outstanding security vulnerabilities.
Click on the "Actions" menu again and then click on the "New workflow" button. Now, instead of choosing a pre-existing "starter" workflow like before, click on the "set up a workflow yourself" link. This will take you to a page for editing ".github/workflows/main.yml" from a blank slate. Please change the file name to "maven-ci.yml" instead and then paste the following into the content box before commiting the file:
name: Maven CI
# Triggers manually or on push or pull request on the main branch
# (in other words, on a code integration event.)
on:
workflow_dispatch:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
# Runs the Maven test phase on the project
maven_test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# The uses: keyword invokes a GitHub action:
# https://github.com/marketplace/actions/checkout
- name: Checkout repository
uses: actions/checkout@v3
# This invokes the Setup Java JDK GitHub action:
# https://github.com/marketplace/actions/setup-java-jdk
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'zulu'
cache: maven
- name: Test with Maven
run: mvn test
# https://github.com/marketplace/actions/upload-a-build-artifact
- name: Upload jacoco results as artifact
uses: actions/upload-artifact@v4
with:
name: Jacoco coverage results
path: target/site/jacoco
# Uploads dependency graph to GitHub to receive Dependabot alerts
update_dependence_graph:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# https://github.com/marketplace/actions/maven-dependency-tree-dependency-submission
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@v3
Alternatively, you could have just created and committed the file ".github/workflows/maven-ci.yml" and it would have had the same effect. Now go ahead and click on "Actions" and you will see the Maven CI workflow created and already running! Soon, it will show a failure:
We will get to the failure soon, but first let's try to understand why the workflow was triggered without you manually running it. Note the phrase "Maven CI #1: Commit 8e12112 pushed by wonsunahn" below the commit message "Create maven-ci.yml". It is clear that this workflow was triggered due to a pushed commit. These lines at the top of maven-ci.yml file are what automatically triggered the workflow on a push:
# Triggers manually or on push or pull request on the main branch
# (in other words, on a code integration event.)
on:
workflow_dispatch:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
CI pipelines are those that run in response to a code integration event. Going forward, any push or creation of a pull request will trigger this workflow. A pull request is a request to merge a branch into the main trunk of the repository and is in that sense also a code integration event.
Now let's click on the failed run link and see exactly which job(s) failed:
It looks like both of our jobs failed! Let's first look at the "maven_test" job by clicking on it:
And then let's expand the failing "Test with Maven" step and scroll to the end to see what the failure was:
You can see that the "mvn test" command failed because of a code coverage error from the Jacoco plugin. This is the same code coverage error we suffered in Exercise 2 at the beginning, if you remember. To solve this issue, copy over the "src" directory from your completed Exercise 2 and overwrite the existing "src" directory. Then commit and push your changes. This will automatically trigger another CI run:
The workflow failed again so let's click on the run to take a look:
This time, the "maven_test" job passed, yay! That means our code passed all our JUnit tests with at least 20% coverage.
Now time to look at the still failing "update_dependence_graph" job:
Note the phrase "Goal requires a project to execute but there is no POM in this directory". The depgraph Maven plugin needs the pom.xml file to extract the dependence graph but it looks like there is none under the current directory. Why would this be? Let's take a closer look at the job definition on our maven-ci.yml file once more:
# Uploads dependency graph to GitHub to receive Dependabot alerts
update_dependence_graph:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# https://github.com/marketplace/actions/maven-dependency-tree-dependency-submission
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@v2
Note that there is nowhere that we bring in the pom.xml file! But how about the previous checkout action?
- name: Checkout repository
uses: actions/checkout@v3
Besides, weren't we able to successfully run "mvn test" because there was a pom.xml file present? The answer is again, you need to remember that these are two different jobs which run on different Docker containers. The "update_dependence_graph" job builds a new container from scratch based on the ubuntu-latest image, and hence will not have the pom.xml file. One way to solve this issue is to checkout the repository again within this job as well, but that feels like a waste of work since we are repeating the same work over again. A better solution is to use GitHub caches.
A cache is a temporary key-value storage on the cloud that can store file(s) or folder(s) that are accessible by later jobs, as long as they have the correct key. Any cache entries that have not been access more than 7 days are automatically deleted by GitHub. Let's see what is in our current cache:
We already have a cache entry! Where did this come from? Focus on this part of the maven-ci.yml file:
# This invokes the Setup Java JDK GitHub action:
# https://github.com/marketplace/actions/setup-java-jdk
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'zulu'
cache: maven
Note the entry "cache: maven". To find out what it does, you can read the following section on the Setup Java JDK action page: https://github.com/marketplace/actions/setup-java-jdk#caching-packages-dependencies
Essentially, it caches all Maven package dependencies for JDK 11 and associates all those files with the key "setup-java-${{ platform }}-${{ packageManager }}-${{ fileHash }}", where the fileHash is the hash of the pom.xml file. It makes sense to associate the files with the hash of pom.xml since the package files would not change unless pom.xml changes. We are caching those files so that the next time a job needs to install JDK 11, it can just restore the files from the cache. Try manually running the "Maven CI" workflow and you can confirm that is indeed what happens:
One use of caches is to make job executions more efficient in this way. Another use of caches is to communicate data between jobs and this second use is what we will leverage to transfer the pom.xml file from our first "maven_test" job to second "update_dependence_graph" job. Do the following:
-
Add the below lines to maven-ci.yml at the end of the "maven_test" job:
- name: Cache build uses: actions/cache@v3 with: key: cached-build-${{github.sha}} path: .This action will store all the files in the current path (which will include pom.xml as well as all source files and class files built under the target/ folder) and associate them with the key "cached-build-${{github.sha}}". The ${{github.sha}} pre-defined GitHub variable gives you the current commit hash. Essentially, you are associating the build with the commit that triggered the build, which makes sense, right?
-
Add the below lines to maven-ci.yml at the beginning of the steps of the "update_dependence_graph" job:
- name: Restore cached build uses: actions/cache@v3 with: key: cached-build-${{github.sha}} path: .This action will be able to find the previously cached build files using the same key so that they can be used for dependence graph analysis.
After making these changes, commit and push. This will trigger a new Maven CI workflow as before. Peek inside the "update_dependence_graph" job and you will see that is still fails, and the reason is because the cache entry was not found:
Was this because the cache entry was not properly created? Nope, we do see that exact cache entry with the key:
So then what happened? Remember I said that jobs run in parallel by default? The "maven_test" job and the "update_dependency_graph" job run concurrently so there is no synchronization between the cache write step and the cache read step. Depending on which job runs faster, the cache might be there or might not be there (essentially a race condition). To solve this issue, insert a needs entry to enforce a dependence on the "maven_test" job:
update_dependence_graph:
needs: [maven_test] # Enforces that maven_test runs first
runs-on: ubuntu-latest
Again, commit and push maven-ci.yml, which will trigger the Maven CI workflow. If you peek inside the run, this is how it looks like now:
Note how now the "update_dependence_graph" job happens sequentially after the "maven_test" job, solving our race condition. But why is the job still failing? Let's take a peek inside the job:
As you can see, this time the cache is successfully restored so that isn't the problem.
The issue is actually with resource accessibility:
The issue is that the job needs write permissions to the repository to update the dependence graph, but currently it is set up for only read permissions. Note these lines for the "update_dependence_graph" job:
permissions:
contents: read
If you expand the "Set up job" step in the job run, you can plainly see that "GITHUB_TOKEN Permissions" only include read access for the Contents.
As a a matter of security, it is good policy to have as restrictive permissions as possible for any GitHub job. Jobs often rely on 3rd party actions and if those action scripts are compromised, the last line of defense is the permissions. By setting it to read-only, we can prevent malicious action scripts from furtively updating parts of the repository.
In this case, we clearly need write permissions for this job, so let's update the permissions as such:
permissions:
contents: write
Write permissions always includes read permissions as well. This will again trigger a Maven CI workflow and this time, the run is finally successful!
The reason that I created a separate job for updating the dependence graph was exactly because it needed write permissions and I didn't want to relax the permission restrictions for the original "maven_test" job.
Now that the Maven CI workflow has run successfully, let's check that our dependency graph is properly updated in our repository. Go to the "Insights" menu and then click on "Dependency graph", and you should be able to see all dependencies defined in your pom.xml file and all your workflows:
The Dependabot whose job is to scour these dependencies is not enable by default however, and you need to go into Settings (the "Code security and analysis" page) to change that:
Click "Enable" on all the buttons that have to do with Dependabot. When you click on Enable for "Dependabot version updates", you will be asked to create ".github/ dependabot.yml" with the following content:
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "maven" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
The only thing you need to do here is type "maven" for the package-ecosystem (as seen above). This configures Dependabot to run a weekly schedule to check for updates and security vulnerabilities for dependent packages. After enabling all options, the settings page should look like this:
As soon as you commit dependabot.yml, Dependabot will kick into action and within a few seconds creates several branches to patch up pom.xml to update some package versions, and corresponding pull requests for those branches:
Those orange dots beside each pull request means that the Maven CI workflow to verify that pull request is still running. Soon enough those orange dots will turn into green check marks if the CI tests pass.
Try clicking on the first pull request and you will see what I mean. Scroll down until you see the "Merge pull request" button, and above it you will see that two successful checks have been performed, all part of our Maven CI workflow:
You will see similar on the other pull request. Since Maven CI has verified that the updated package dependencies did not break our build, we can safely press the "Merge pull request" to merge the branch into our main trunk. After all is said and done, you should see two closed pull requests:
Now we can be confident that all our dependent packages are up-to-date and do not contain any outstanding vulnerabilities.
Dependabot was instrumental in mitigating the damage due to the recent Log4j vulnerability by alerting thousands of repositories and creating thousands of pull requests.
Now that we have a robust CI pipeline in place, we are ready to CD (Continuous Delivery) as well. We could choose to automatically publish our package every time there is a code integration event, but typically software organizations wait for a major release to redeploy. The below YAML file does exactly that:
name: Maven Package
on:
workflow_dispatch:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'zulu'
cache: maven
- name: Build with Maven
run: mvn -B package
- name: Publish to GitHub Packages Apache Maven
run: mvn deploy
env:
GITHUB_TOKEN: ${{ github.token }}
Create a new workflow file maven-publish.yml using the above text in exactly the same way you created maven-ci.yml previously. Note that this workflow is now triggered when a new release is created. It uses the $ {{ github.token }} to authenticate and publish the package. The GitHub pre-defined variable $ {{ github.token }} is automatically generated on each workflow run and is given the permissions specified in the "permissions:" entry.
After the maven-publish.yml is committed and pushed, you also need to edit the pom.xml file and add the below text after the <scm>...</scm> element:
<distributionManagement>
<repository>
<id>github</id>
<name>GitHub Apache Maven Packages</name>
<url>https://maven.pkg.github.com/CS1632-Fall2025/supplementary-exercise-4-ci-cd-pipelines-wonsunahn</url>
</repository>
</distributionManagement>
Replace the repository name within the <url>...</url> element with your own repository name. Please make sure you leave the https://maven.pkg.github.com/CS1632-Fall2025/ part as-is and only change the repository name, as https://maven.pkg.github.com/ is the base URL for the BitHub Maven package registry.
You also need to find the artifactId and append your PittID to it so that it becomes unique. If your PittID has a capital letter please convert it to lowercase (GitHub package names don't work with capital letters for some reason). For example, if you PittID was wahn (like mine), please change it to:
<artifactId>cs1632-cicd-pipelines-wahn</artifactId>
Now we are ready to create a new release to see if this works! Go to the "<> Code" tab on your GItHub repository and then click on the "Create a new release" link under the "Releases" section at the bottom right:
The click on "Choose a tag", and then type "v1.0" in the box and click on "+ Create new tag: v1.0 on publish". The should create the "v1.0" tag. Then on the "Release title" box, write "Release v1.0". Then click on the "Publish release" button. This should create your first release:
This triggers the "Maven Package" workflow which publishes the package:
In the end, you should see a new package registered on your github page:
If you click on the package on the bottom right, you should see the contents of your newly published package:
Let's try using the published Maven package by adding it as a dependency in a Maven project as directed in the package page. But before you are able to do that, you need to add a settings.xml file that tells Maven that the GitHub Maven package repository is one of the allowed repositories. You need to place the settings.xml file under the .m2 folder under your home directory, which is where Maven stores all its cached packages it downloads from Maven Central. The .m2 folder is typically located in the following location.
If you are using Windows:
C:\Users\<YOUR_USER_NAME>\.m2
If you are using Mac:
/Users/<YOUR_USER_NAME>/.m2
Under the above folder add the settings.xml file with the following content:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
http://maven.apache.org/xsd/settings-1.0.0.xsd">
<activeProfiles>
<activeProfile>github</activeProfile>
</activeProfiles>
<profiles>
<profile>
<id>github</id>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2</url>
</repository>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/CS1632-Fall2025/*</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<servers>
<server>
<id>github</id>
<username>USERNAME</username>
<password>TOKEN</password>
</server>
</servers>
</settings>
Please replace USERNAME with your GitHub user name and replace TOKEN with a Personal Access Token (PAT). If you don't have a PAT (likely), you will need to create one. To generate a new token, on GitHub.com, go to Account > Settings > Developer Settings > Personal Access Tokens > Tokens (classic) and then click on the Generate New Token button and then the classic option. Leave a Note to yourself to help you remember what this token is for. Also click on the "repo" and "write:packages" checkboxes. For the purposes of deploying the package, you only need "read:packages" permissions really, but we will use this token in Part 2 for other purposes too. If you forgot the PAT string, you will have to generate the token by clicking on "Regenerate Token" button.
After this you are ready to deploy the Maven package on a Maven project. Let's try doing this on the Exercise 2 project from whence we copied the RentACat source code. Open your Exercise 2 project on VSCode, and then copy the dependency text suggested on the package page of our published package and paste it within the Dependecies section. Now, try deleting all the implementation code under the src/main/java/edu/pitt/cs folder. You should not get any compilation errors since now all those classes are supplied by your package. Then try clicking on the VSCode Testing extension and running all the tests. All of them should pass! Now, please do not commit the code since you probably still want your Exercise 2 source code. You can discard the changes after you are satisfied.
When you have done all the tasks you can, please submit "Supplementary Exercise 4 Report" on GradeScope. The report consists of "Yes" or "No" questions on whether you were able to complete a task and reflections. If you were not able to complete a task, please mark "No". For the tasks that you said "No", I expect you to explain the issue that prevented you from fulfilling the task on the reflections questions at the end of Part 1 and Part 2.




























