We'll be working on top of this repository. Because you'll need to change some settings on the repository later in the workshop, it's recommended that you fork the repository instead of cloning it. To do that:
- Click the Fork button in the top right on the repository page.
- Select your GitHub user when it asks you where you should fork it to.
- This should take you to a fork of the repository on your account, e.g. https://github.com/MyUser/DevOps-Course-Workshop-Module-07-Learners where MyUser will be replaced by your username.
- You can now clone and push to that repository as normal.
This repository contains a minimal .NET Core app. You don't need to worry about exactly how the code works, but you should be able to build, test and run it. It uses npm which is a package manager for the Node JavaScript platform. If you are struggling to run the code locally, you should skip to step 3 before spending too much time trying to resolve the issue. It's only preferable to run it locally first to better understand what you want GitHub Actions to replicate.
- Run
dotnet build
from the terminal in the project folder. This will build the C# code.- If you get errors resolving NuGet packages try running
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
to add the main NuGet registry as a package source. You can usedotnet nuget list source
to see what package sources NuGet is using.
- If you get errors resolving NuGet packages try running
- From the DotnetTemplate.Web folder, run
npm install
(first time only) and thennpm run build
. This will build the TypeScript code.- If you are on Windows and see errors during installation containing "gyp ERR", you may need to first run
npm install --global windows-build-tools
(and restart your terminal).
- If you are on Windows and see errors during installation containing "gyp ERR", you may need to first run
- Run
dotnet run
in the DotnetTemplate.Web folder. This will start the app. - You can now see the website by going to http://localhost:5000/. You should see something like the image below.
- Run
dotnet test
inside the project folder. This will run the C# tests in the DotnetTemplate.Web.Tests project. - Run
npm t
inside the DotnetTemplate.Web folder. This will run the TypeScript tests in DotnetTemplate.Web/Scripts/spec. They're run using Jasmine. - Run
npm run lint
inside the DotnetTemplate.Web folder. This will run linting on the TypeScript code, using eslint. Linting refers to checking the codebase for mistakes, either functional or stylistic. This project's linting currently reports zero errors, two warnings.
- Create the config file for your continuous integration pipeline:
- Create a folder called ".github" at the root of the repository.
- Inside there, create a "workflows" folder.
- Inside there create a file: you can name it whatever you like, although it needs to have a .yml extension, e.g. continuous-integration-workflow.yml.
- Implement a basic workflow:
name: Continuous Integration
on: [push] # Will make the workflow run every time you push to any branch
jobs:
build:
name: Build and test
runs-on: ubuntu-latest # Sets the build environment a machine with the latest Ubuntu installed
steps:
- uses: actions/checkout@v2 # Adds a step to checkout the repository code
- Commit and push your changes to a branch on your repository.
- On your repository page, navigate to the Actions tab.
- You should see a table of workflows. This should have one entry with a name matching your latest commit message. Select this entry.
- On the next page click "Build and test" on the left. This should show you the full output of the workflow which ran when you pushed to your branch. See the documentation for more details on how to view the output from the workflow.
See the GitHub documentation for more details on how to set up GitHub Actions and https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions for more details on the syntax of the workflow file.
Currently our workflow only checks out the code, which isn't that useful. We want to add some more useful steps to the workflow file. Each step in the workflow file either needs to:
- Specify
run
to run a command as you would in the terminal, for example:
name: Continuous Integration
on: [push]
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Hello world # Name of step
run: echo 'Hello world' # Command to run
- Specify
uses
followed by the name of the action. The name of the action is of the formGitHubUsername/RepositoryName
and you can find them by searching the marketplace. Anyone can publish actions - you could create your own or fork an existing one. If it is supplied by GitHub themselves, the username will beactions
. For example:
name: Continuous Integration
on: [push]
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Hello world
uses: actions/[email protected] # Name of the action. This uses https://github.com/actions/hello-world-javascript-action
with: # This section is needed if you need to pass arguments to the action
who-to-greet: 'Mona the Octocat'
You should amend your workflow file so that it:
- Builds the C# code.
- Runs the C# tests.
- Builds the TypeScript code.
- Runs the linter on the TypeScript code.
- Runs the TypeScript tests.
To make sure people are aware when there are issues with the build, it can be useful to send a slack notification at the end of the workflow.
Before attempting this step please create your own personal slack workspace. This is free and can be set up here.
- Add a slack notification at the end of the workflow. To make this work you will need to use the slack app incoming webhooks, make sure this has been installed in the slack workspace you're using.
- Make the workflow post a different message if the workflow failed, so that it's obvious if the workflow failed.
- Make the workflow post a different message if the workflow was cancelled.
Add a workflow status badge to your repository.
Change your workflow so that it only runs when pushing to the main branch or by raising a PR. Is there a way to ensure that no one can update the main branch except through a PR that has passed the workflow?
There are two options for running Jenkins locally, you can either install Jenkins or run it through Docker. We would recommend running Jenkins through Docker and the instructions for that are here.
NB: step 4 of the Windows instructions wants you to follow step 4 of the Mac/Linux instructions
Once you've done the step above you should have Jenkins running on http://localhost:8080/. If you go to this url in a browser it should show you a setup page.
- Login with the password you got from the logs when starting Jenkins. Hint: You can run
docker logs your_container
to access a container's logs. Rundocker container ls
to view a list of running containers. - Now you have the option to select some initial plugins. For now, make sure you tick the GitHub plugin. We won't need any others right away, and you can add more later.
- Create an admin user.
- Use the default jenkins url (http://localhost:8080)
You should now see the Jenkins dashboard.
We now want to get Jenkins to build our app. To do this you need to create a job on Jenkins for our app and create a Jenkinsfile in your repository to define what the job should do.
From your Jenkins dashboard:
- Select New Item.
- Set it to a multibranch pipeline. This means the job will scan your repository for branches and run the job on any branch with a Jenkinsfile.
- Leave all the defaults other than setting the branch sources to GitHub. Leave the defaults for the branch source other than setting the repository url to your repository url. You may notice a warning about not using GitHub credentials at this point. This is fine, as we're just reading from a public repository we don't need credentials. If we were using a private repository or we were writing to the repository during the job, then we would need to set up credentials.
- Click Save to create the Jenkins job.
See https://www.jenkins.io/doc/book/pipeline/jenkinsfile/ for details on how to create a Jenkinsfile. We want to add the same steps as for the GitHub Actions workflow so that it:
- Builds the C# code.
- Runs the C# tests.
- Builds the TypeScript code.
- Runs the linter on the TypeScript code.
- Runs the TypeScript tests.
You have 2 options for installing .NET Core & npm inside jenkins:
- Make installation separate build stages
- This is not ideal as you will have to run the installation on each build
- Specify containers to run stages of the jenkins pipeline with .NET Core and npm pre-installed
- There are some pre-built images for npm (e.g.
node:14-alpine
) but for .NET Core you'll want to use either Microsoft's images or script the installation from a base image such as alpine linux. You may need to set an environment variableDOTNET_CLI_HOME
(e.g. to"/tmp/dotnet_cli_home"
) in your Jenkinsfile for the dotnet CLI to work correctly.
- There are some pre-built images for npm (e.g.
Hints
- You'll need to use a
dir
block for some steps to run them inside theDotnetTemplate.Web
directory. - If Jenkins starts rate limiting your repository scanning you can go to "Manage Jenkins" -> "Configure System" and change "Github API usage rate limiting strategy" to "Throttle at/near rate limit". Adding credentials to your pipeline configuration will also increase the limit.
- Commit and push your new Jenkinsfile.
- From your Jenkins dashboard select the job you created.
- Click "Scan Multibranch Pipeline Now". This will scan the repository for branches and run the job on any branch with a Jenkinsfile.
- Select your branch, which should appear under "Branches" once the scan is done.
- You should see a stage view of the build, showing each stage in the Jenkinsfile. If the stage succeeded it will be green, if it failed it will be red.
- Select the most recent build from the build history on the left.
- Click "Console Output" to view the full logs from the build.
We want high test coverage, meaningfully testing as much of the functionality of the application as possible. Code coverage is a more naive metric - it simply checks which lines of code were executed during the test run. But higher code coverage is usually a good thing and it can still usefully flag which parts of the codebase are definitely untested. So let's include code coverage in our CI pipeline.
First check it works manually. From the DotnetTemplate.Web folder, run the command npm run test-with-coverage
. This runs the frontend tests and calculates code coverage at the same time.
It produces two reports: one in HTML form that you can open in your browser (DotnetTemplate.Web/coverage/index.html) and one in XML that we will get Jenkins to parse.
Try adding code coverage to Jenkins:
- Install the Code Coverage API plugin on Jenkins.
- Change your Jenkins pipeline to run the tests with code coverage.
- Add a post build step to publish coverage. You can see a simple example of the command if you scroll down the Code Coverage API documentation, to the "pipeline example". You will want to use the "istanbulCoberturaAdapter", and the report to publish is "cobertura-coverage.xml" in the coverage folder.
- You should see a code coverage report appear on the build's page after it completes. Click through to see details.
Now let's enforce high code coverage:
- Configure it to fail the build for code coverage below 90%. You may find it easiest to use the Jenkins Snippet Generator.
- Push your change and watch the build go red!
- Edit the
DotnetTemplate.Web/Scripts/spec/exampleSpec.ts
file:
- Update the import statement:
import { functionOne, functionTwo } from '../home/example';
- Invoke functionTwo on a new line in the test
functionTwo();
- Push the change and observe the build go green again! You can also view the code coverage history.
Like for the GitHub Actions workflow, add slack notification to the Jenkins job. To make this work you will need to use the slack app jenkins ci, make sure this has been installed in the slack workspace you're using.
Note that their documentation may be slightly out of date and not quite match the page you see in Jenkins.
Can you create a single container that can be used as the sole build agent for the entire multistage Jenkins pipeline?