This repository actually contains a React-application built with vite that we want to automatically test and build in this part of the workshop.
You can browse through the files if you are interested in how the app itself works (this is not strictly necessary to understand the rest of the workshop though).
src/main.tsx
: is the main entry point of the applicationsrc/pages/Home.tsx
: is the route that contains most of what you see when starting the applicationsrc/pages/Home.test.ts
: containsvitest
tests which we will run with GitHub ActionsDockerfile
: a Docker file that package the application in a container for release in a later step.
If if you want to test the application, you can start a Codespaces and run it (npm run dev
) or test it (npm test
). If you want to run the application on your local machine, you will need to install Node.js.
To test the container, run docker build . -t local:latest
to build the image and then docker run -p 8080:8080 local:latest
to run it. Running these commands locally will require you to install Docker.
To develop a GitHub Workflow process that uses Actions to automate the Continuous Integration process, you can begin by adding a starter workflow to the repository:
- On the initial view of your repository, find and navigate to the Actions tab.
- Click
New workflow
- Search for
Node.js
- Click Configure on the
Node.js
starter workflow - From the yml-array in the
node-version
field, remove14.x
(our app is not compatible with this version)
Commit the node.js.yml
file to the main
branch to complete this process of creating our first CI workflow.
The `.github/workflows/node.js.yml` will include the contents from below:
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test
As you can see, we are now using a second Action in our workflow, actions/setup-node
, which is used to install a specific version of node onto the runner.
Let's use this example to quickly examine the notations of an Action reference:
actions/
references the owner of the action - which translates to a user or organization on GitHubsetup-node
references the name of the action - which translates to a repository on GitHub@v3
is the version of the action - which translates to a git tag or general reference (a branch or even a commit-sha) on the repository
This makes it very easy to navigate to the source code of any action by simply appending the owner
and name
to github.com, like https://github.com/{owner}/{name}. For the example above, this would be https://github.com/actions/setup-node.
Also take note that our workflow is running a matrix build strategy with 2 versions of node: 16 and 18. A matrix build lets you execute a job in paralellel with different input parameters. In this case, we are running the same job twice, but with different versions of node.
Your new Actions CI is running on every push, and since you just pushed a new commit with the workflow you created, you will already have a workflow running.
Note that we will need to run tests as part of our CI. You can find most of the tests of this application in the ../src/pages/Home.test.tsx
file which in parts looks like below:
// ... imports
describe("<Home />", (): void => {
afterEach((): void => {
cleanup();
});
it("renders the octocats returned from the API.", async (): Promise<void> => {
const inMemoryAPI = createInMemoryOctocatApi();
inMemoryAPI.addOctocats([
createTestOctocat({ id: "#1", name: "Octocat 1" }),
createTestOctocat({ id: "#2", name: "Octocat 2" }),
]);
renderWithProviders({ component: <Home />, inMemoryApi: inMemoryAPI });
expect(await screen.findByText("Octocat 1")).toBeDefined();
expect(screen.getByText("Octocat 2")).toBeDefined();
});
// ... more tests
});
The result of that last push to main should look like this image:
It is common when working on the CI part of your project to add more informations to the user, for example the tests "code coverage".
The approach is quite simple with GitHub Actions, you decide where and when you want to do a specific task, and you search for a specific Action in the GitHub Marketplace.
-
Search an Action in the GitHub Marketplace:
vitest coverage report
-
Click on the Vitest Coverage Report Action.
-
You can read the documentation, and integrate it to your workflow.
This is a good time to briefly talk about the permissions of a workflow. Any workflow that interacts with GitHub resources needs a permission to do so. By controlling permissions, GitHub users can ensure that only authorized users or processes are able to perform certain actions, such as calling an API with a private access key, execution certain automations, or deploying to production environments. This helps to prevent unauthorized access to sensitive data, reduce the risk of accidental or malicious changes, and maintain overall security and stability of the codebase. For example:
- The
actions/checkout
- Action requires read permissions to your repository to be able to do that checkout to the runner machine. - The Vitest Coverage Report Action wants to write a comment into a Pull Request and thus, needs the permissions for this as well.
Luckily, GitHub Workflows come with a base set of default permissions and the ability to easily extend different permissions with the permission
keyword - either on:
- the root of the workflow to set this permission for all jobs of this workflow.
- within a job definition itself to only specify the permissions for this job. This is the recommended approach from a security perspective, as gives the least required privileges to your workflows and jobs
These permissions will be applied to the so called GITHUB_TOKEN
- but we will talk about this at a later stage.
For now, all you need to know is: As soon as you specify the permissions
-keyword, the default permissions do not apply anymore, meaning you need to specifically configure all permissions you require in the job or workflow. Let's do this in the next step.
-
In the
main
branch, edit the CI workflow.github/workflows/node.js.yml
-
Add the
permissions
keyword with the following permissions into the job section:build: name: "Build and Test" runs-on: ubuntu-latest permissions: # Required to allow actions/checkout to clone the repository onto the runner contents: read # Required to allow the vitest coverage action to write a comment into the Pull Request pull-requests: write # ... rest of the node.js.yml
-
Add the following step in the
build
job section of your workflow, right after thenpm test
step:# ... rest of the node.js.yml - run: npm run test - uses: davelosert/vitest-coverage-report-action@v1 with: vite-config-path: vite.config.ts
-
While you are at it - why don't give the job a more speaking
name
:jobs: build: name: "Build and Test" runs-on: ubuntu-latest # ... rest of the node.js.yml
-
Commit the
node.js.yml
file.
As this is a frontend project, we don't really need a matrix-build here (this is more suited for backend projects that might be running on several Node.js version). Removing the matrix build will also make the test - and coverage reporting - happen only once.
Try to remove the matrix build yourself and make the Action only run on Version 16.x. Extend this section to see the solution.
jobs:
build:
name: "Build and Test"
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run test
- name: 'Report Coverage'
uses: davelosert/vitest-coverage-report-action@v1
with:
vite-config-path: vite.config.ts
-
Go to the main page of the repository.
-
Click on
./src/main.tsx
, and edit the file (add a comment for example). -
Scroll down and click Create a new branch for this commit and start a pull request.
-
Click Propose Changes.
-
Click Create pull request.
-
Wait for the CI to run and you will see a new comment in your pull request with the code coverage.
As you can see, the test-coverage of this project is quite low. Sometimes, we want to enforce a certain coverage on a project, meaning we do not want allow to merge a PR if it reduces the coverage below a certain threshold.
Let's try this out in this project:
-
On the Branch you created above, go into the
vite.config.ts
(located at the root level of the repository) and within thetest.coverage
-section, edit it to provide some thresholds like this:coverage: { reporter: ["text", "json", "json-summary"], lines: 100, branches: 100, functions: 100, statements: 100 },
-
Having the coverage thresholds set, our workflow will now fail after the next commit on the
npm test
step. However, as we still want to report the coverage, we need to run thevitest-coverage-report-action
even if the previous step fails. We can this by adding aif: always()
statement to the step:- name: 'Report Coverage' uses: davelosert/vitest-coverage-report-action@v1 if: always() with: vite-config-path: vite.config.ts
-
Commit the changes and wait for the workflow to run through
The coverage
-step should be failing now. However, this does not yet prevent you from being able to merge this PR. The button to merge is still clickable:
For this to work, we have to make our target branch main
a protected branch and enforce the build
Action to be succesful before a merge can be done:
-
Within your repository, go to
Settings
and then toBranches
. -
Under
Branch protection rules
, click onAdd Rule
. -
For the
Branch name pattern
, typemain
. -
Check the
Require status checks to pass before mergin
. -
In the appearing search-box, search for
Build and Test
(or whatever name you gave the Job in Step 3.2) and select that job. (Note that you might also see the Jobs of the previous Matrix Build with a specific Node-Version. You can ignore those.) -
Scroll down and click
Create
.
If you go back to the PR now, you will see that the merge button is inactive and can not be clicked anymore.
As an administrator, you still can force-merge. 'Normal' users in your repo don't have this option.
Note This will now not only prevent people from merging a branch to
main
if the coverage-thresholds are not met, but also if the whole workflow fails for other reasons, e.g. if the build is not working anymore or if the tests are failing in general - which usually is a desired outcome.
So from here on you have two options:
- Write some more tests (if you are into React 😉)
- Remove the (admitetly insane) thresholds or lower them to make the workflow pass
In this lab you have learned how to:
- 👍 Add a new workflow for CI.
- 👍 Search a new GitHub Action, for Code Coverage.
- 👍 Understand and make use of
permissions
- 👍 Add a new GitHub Action to your workflow.
- 👍 (optionally) Prevent merges on failing tests or coverage thresholds by using Branch Protection rules.
Next :