Appearance
Are you an LLM? You can read better optimized documentation at /deployment/ci_cd.md for this page in Markdown format
CI/CD
Note: This project is no longer officially maintained as of January 2023. The CI/CD configuration described here reflects the state of the project at that time.
This document provides a detailed overview of the Continuous Integration and Continuous Deployment (CI/CD) pipelines for TheExampleApp. It covers the existing CircleCI configuration, guidance for alternative setups like GitHub Actions, and best practices for maintaining a robust and secure automated workflow.
Continuous integration and deployment
The project is configured for Continuous Integration (CI) to automate the build and testing process upon every code change. This ensures code quality, consistency, and early detection of regressions. The primary goals of the CI pipeline are:
- Build Verification: Compile the .NET Core application to ensure there are no build errors.
- Automated Testing: Execute a multi-layered testing strategy, including unit, integration, and end-to-end (E2E) tests.
- Deployment Automation: While the current CI configuration focuses on build and test, the project is designed for automated deployments to platforms like Heroku and Azure.
The CI process is orchestrated via CircleCI, as defined in the .circleci/config.yml file.
CircleCI configuration
The project's CI pipeline is managed by CircleCI. The configuration file (.circleci/config.yml) defines a single job, build, which encapsulates all the necessary steps to validate the application.
.circleci/config.yml structure
The configuration uses CircleCI's version 2 syntax. The core of the pipeline is the build job, which runs in a custom Docker environment.
yaml
# .circleci/config.yml
version: 2
jobs:
build:
docker:
# The build runs inside a custom Docker image pre-loaded with .NET Core SDK and other dependencies.
- image: roblinde/the-example-app-build-assets
steps:
- checkout
# ... build and test steps follow1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Using a custom Docker image (roblinde/the-example-app-build-assets) is a key optimization. This image comes pre-installed with the .NET Core 2.1 SDK and other required tooling, which significantly reduces the setup time for each build.
Build Workflow
The workflow consists of a sequential series of steps designed to build the application and run all associated tests.
Checkout Code: The
checkoutstep pulls the source code from the repository.Build Application: The
dotnet buildcommand compiles the entire solution, including the main web application and all test projects.yaml- run: name: Build command: dotnet build1
2
3
Test Automation
The solution includes three separate test projects to ensure comprehensive coverage. For a deeper dive into the testing strategy, see the Testing Overview.
Unit Tests (
TheExampleApp.Tests): These tests are executed first for fast feedback. They validate individual components in isolation. The project uses xUnit (v2.3.1) as the testing framework and Moq (v4.7.145) for mocking dependencies.yaml- run: name: Unit tests command: dotnet test TheExampleApp.Tests1
2
3Integration Tests (
TheExampleApp.IntegrationTests): These tests verify the interactions between different parts of the application, such as the controllers and the Contentful service layer.yaml- run: name: Integration tests command: dotnet test TheExampleApp.IntegrationTests1
2
3End-to-End (E2E) Tests: The solution includes a dedicated
TheExampleApp.E2eTestsproject for end-to-end testing. These tests can be executed using the standarddotnet testcommand.yaml- run: name: E2E tests command: dotnet test TheExampleApp.E2eTests1
2
3Note: The CI configuration may also include steps to run browser-based E2E tests from an external repository (
the-example-app-e2e-tests) using Cypress. These tests start the application in the background and simulate user interactions:yaml# Start the web application in the background - run: name: Run app command: | cd TheExampleApp dotnet run background: true # Clone the separate E2E test repository - run: name: Clone e2e tests command: | git clone https://github.com/contentful/the-example-app-e2e-tests.git ./test/e2e cd test/e2e # Install Node.js dependencies, using cache for speed - restore_cache: keys: - package-v2-{{ checksum "./test/e2e/package.json" }} - package-v2 - run: name: Install e2e tests dependencies command: | cd test/e2e npm i - save_cache: key: package-v2-{{ checksum "./test/e2e/package.json" }} paths: - ./test/e2e/node_modules # Execute the Cypress tests - run: name: Run e2e tests command: | cd test/e2e node_modules/.bin/cypress run --project /root/project --env LANGUAGE=dotnet1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36Key Details for Browser-Based E2E Tests:
- An external E2E test suite is maintained in a separate repository (
the-example-app-e2e-tests) and can be cloned into the build environment. - The ASP.NET Core application is started in the background to serve the UI for Cypress.
- The
cypress runcommand is executed with a specific project path (--project /root/project) and an environment variable (LANGUAGE=dotnet) to tailor the tests for this backend. - For more information on the E2E test suite, refer to the E2E Testing documentation.
- An external E2E test suite is maintained in a separate repository (
Deployment Automation
The provided .circleci/config.yml does not include a deployment step. It is a CI-only pipeline. To implement Continuous Deployment, you would add a new job (e.g., deploy) that is triggered only on merges to the main branch. This job would be responsible for publishing the application to a hosting provider like Heroku or Azure.
For more details on deployment strategies, see the Deployment Overview.
GitHub Actions
While this project uses CircleCI, a similar workflow can be easily configured using GitHub Actions. This is useful if the team decides to migrate CI/CD providers. Below is an example workflow that mirrors the functionality of the existing CircleCI configuration.
To implement this, create a file at .github/workflows/build-and-test.yml.
yaml
# .github/workflows/build-and-test.yml
name: .NET Core Build and Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
# The version must match the project's target framework
dotnet-version: '2.1.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run Unit & Integration Tests
run: |
dotnet test TheExampleApp.Tests --no-build --verbosity normal
dotnet test TheExampleApp.IntegrationTests --no-build --verbosity normal
e2e-tests:
runs-on: ubuntu-latest
needs: build # Ensure build job completes successfully first
services:
# Run the web app as a service container
webapp:
image: mcr.microsoft.com/dotnet/core/sdk:2.1
volumes:
- ${{ github.workspace }}:/app
working-dir: /app/TheExampleApp
ports:
- 3000:80 # Map port 3000 on host to 80 in container
# Use environment variables for secrets
env:
ContentfulOptions:DeliveryApiKey: ${{ secrets.CONTENTFUL_DELIVERY_API_KEY }}
ContentfulOptions:PreviewApiKey: ${{ secrets.CONTENTFUL_PREVIEW_API_KEY }}
ContentfulOptions:SpaceId: ${{ secrets.CONTENTFUL_SPACE_ID }}
command: dotnet run --urls "http://*:80"
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout E2E tests repository
uses: actions/checkout@v3
with:
repository: contentful/the-example-app-e2e-tests
path: test/e2e
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16' # Specify a version compatible with Cypress
cache: 'npm'
cache-dependency-path: 'test/e2e/package-lock.json'
- name: Install E2E dependencies
run: npm ci
working-directory: test/e2e
- name: Run Cypress E2E Tests
uses: cypress-io/github-action@v5
with:
working-directory: test/e2e
# Override baseUrl to point to the service container
config: baseUrl=http://localhost:3000
# Pass environment variables to Cypress
env:
LANGUAGE: dotnet1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
This example demonstrates how to set up build and E2E test jobs, using service containers for the web app and GitHub secrets for managing credentials.
Best practices
Adhering to the following best practices is crucial for a secure, reliable, and maintainable CI/CD pipeline.
Automated Testing
The project's multi-layered testing approach is a core strength.
- Unit Tests: Provide fast feedback on isolated logic.
- Integration Tests: Ensure that services, like the Contentful client, are correctly integrated.
- E2E Tests: Validate user-facing functionality from end to end.
All three test suites must pass for any change to be considered mergeable. This non-negotiable quality gate prevents regressions and maintains application stability.
Environment-based Deployments
A production-grade pipeline should distinguish between different environments (e.g., staging, production). This can be achieved by:
- Branching Strategy: Trigger staging deployments from a
developbranch and production deployments from themainbranch or Git tags. - Configuration per Environment: Use the CI/CD platform's environment variable features to inject environment-specific settings, such as different Contentful Space IDs or API keys for staging vs. production. See Environment Variables for more on how the app consumes these.
Secret Management
Properly managing secrets like API keys is critical for security.
Warning: The
cypress.jsonfile in the repository contains hardcoded Contentful API keys. This is a significant security vulnerability and is done for demonstration purposes only. Never commit secrets directly to source control.
json
// cypress.json - FOR DEMONSTRATION ONLY. DO NOT DO THIS IN PRODUCTION.
{
"env": {
"CONTENTFUL_SPACE_ID": "qz0n5cdakyl9",
"CONTENTFUL_DELIVERY_TOKEN": "df2a18b8a5b4426741408fc95fa4331c7388d502318c44a5b22b167c3c1b1d03",
// ... more hardcoded secrets
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Correct Approach:
- Store secrets in the CI/CD provider: Use CircleCI Contexts or GitHub Repository/Organization Secrets to store all API keys and sensitive credentials.
- Inject secrets as environment variables: The CI/CD platform will securely inject these secrets into the build environment at runtime. The application and test runners should be configured to read these values from the environment.
- Application Configuration: For deployed environments (Heroku, Azure), configure environment variables directly in the hosting platform's settings. The ASP.NET Core application is already set up to read from
appsettings.jsonand override values with environment variables, which is the standard and recommended practice. For more details, see Environment Variables.