GitHub Actions
GitHub Actions
Run Automated Tests on PR
This section outlines the setup for a Continuous Integration (CI) workflow in GitHub Actions. This workflow runs automated unit, integration, and end-to-end (E2E) tests for both the frontend and backend applications whenever code is pushed to the main branch or a Pull Request (PR) is opened. This prevents regressions and ensures code quality before deployment.
Workflow Initialization
The CI workflow is named CI and is triggered by pushes to main and by any pull_request. The necessary permissions for the workflow are set to read the repository contents and read action metadata.
name: CI
on:
push:
branches:
- main
pull_request:
permissions:
actions: read
contents: read
jobs:
Add Backend Tests
The backend-tests job focuses on the .NET Web API. It checks out the code, sets up the required .NET SDK version, and executes the unit and integration tests using the dotnet test command.
backend-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: "9.0.x"
- name: Run dotnet tests
run: |
dotnet test
Add Frontend Tests
The frontend-tests job is responsible for the Angular/Nx application. It requires Node.js and npm to install dependencies, and it installs Playwright for E2E testing. It leverages the Nx build system for caching and running all lint, test, build, and E2E tasks efficiently.
Setting Up Node, Dependencies, and Test Runner
The first steps involve checking out the code, setting up Node.js with package caching, installing all Node dependencies, and installing the necessary Playwright browsers.
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Optimization: Filter to only check out essential files for CI to save time
filter: tree:0
# Fetch all history for Nx to correctly determine affected projects
fetch-depth: 0
# This enables task distribution via Nx Cloud
# Run this command as early as possible, before dependencies are installed
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
# Uncomment this line to enable task distribution
# - run: npx nx start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"
# Cache node_modules using a recommended setup for Node
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
# Install all Node dependencies
- run: npm ci --legacy-peer-deps
# Install Playwright browser dependencies for E2E tests
- run: npx playwright install --with-deps
Running Tests with Nx
The final step uses the Nx CLI (npx nx run-many) to execute multiple targets (linting, unit tests, building, and E2E tests) across all relevant projects, ensuring comprehensive testing of the frontend code.
# Run the test tasks using Nx
# The "-t" flag stands for "target"
- run: npx nx run-many -t lint build # test e2e --> comment tests out until you learned how to write unit and e2e tests in Angular
# Optional: Add self-healing CI with Nx Cloud to automatically fix failures
- run: npx nx fix-ci
if: always()
Continuous Deployment using Azure CLI
This guide provides a structured, step-by-step process for setting up a secure Continuous Deployment (CD) pipeline using GitHub Actions and Azure with OpenID Connect (OIDC) authentication.
Setting Up Azure Resources
Before configuring the pipeline, you must provision the necessary Azure infrastructure.
| Resource Name | Purpose | Notes |
|---|---|---|
| Resource Group | Logical container for all resources (e.g., SweDemoGroup). | Used as the scope for the Service Principal in the CLI setup. |
| SQL Server & Database | Hosts the backend application data. | Requires SQL admin credentials (AZURE_SQL_ADMIN, AZURE_SQL_PASSWORD) for EF Core migrations. |
| Azure Web App (Backend) | Hosts the .NET Web API (e.g., swe-demo-api). | Target for the backend-deploy job. |
| Azure Web App (Frontend) | Hosts the Angular/Nx client (e.g., swe-demo-app-client). | Target for the frontend-deploy job. This requires a startup command (see next section). |
| User Assigned Managed Identity | New 'user' to manage and deploy our applications | This requires additional setup in order to be able to deploy (see next section). |
Extra Steps on Azure for Frontend (Angular Routing Fix)
The Angular application relies on client-side routing (HTML5 History Mode). When deploying built static assets to a standard Azure Web App, the default server may not correctly handle sub-routes (like /products), resulting in 404 errors on direct access.
To fix this, you need to configure the frontend Web App to use a startup command that initiates a static file server and handles client-side routing fallback.
| Step | Action | Command/Purpose |
|---|---|---|
| a. Navigate in Portal | Go to the Frontend Web App (swe-demo-app-client) → Settings → Configuration(Preview) → Stack settings. | This section controls the environment and startup behavior of the container. |
| b. Set Startup Command | Set the Startup Command field to: | pm2 serve /home/site/wwwroot --no-daemon --spa |
pm2: This is the command-line interface for PM2, which manages and keeps applications alive forever.
serve: This tells PM2 to serve static files from a directory. It's a built-in feature that uses a lightweight static file server.
/home/site/wwwroot: This is the path to the directory containing the static website files (like index.html, CSS, JS, etc.).
--no-daemon: This flag runs PM2 in the foreground instead of as a background daemon. It's often used in environments like Docker or Azure App Service where the process needs to stay attached to the main thread.
--spa: This enables Single Page Application (SPA) mode. In SPA mode, all routes are redirected to index.html, which is necessary for client-side routing (e.g., with React, Angular, Vue).
Extra Steps on Azure for Backend (Environment variables)
The .NET Web API requires two critical configuration values: the database connection string and the allowed origins for Cross-Origin Resource Sharing (CORS). These must be set as Application Settings and Connection Strings in the Azure Portal for your Web App.
| Configuration Area | Name | Purpose | Value Source |
|---|---|---|---|
| Connection strings | <Name of CS in app> | Used by Entity Framework Core to connect to Azure SQL Database. | Can be found in your Azure SQL configuration. |
| Application settings | ALLOWED_ORIGINS | A comma-separated list of URLs the Web API permits for CORS requests (e.g., the URL of the deployed frontend). | Frontend Web App URL (eg swe-demo-app-client.azurewebsites.net). |
How to Set in Portal:
- Navigate to your Backend Web App in the Azure Portal.
- Go to Configuration.
- On the Connection strings tab, add a new entry with the name
<Name of CS in app>and TypeSQLServer, containing the full connection string. - On the Application settings tab, add a new setting named
ALLOWED_ORIGINSwith a value listing your frontend URLs.
Extra Steps on Azure for Deployment (User Assigned Managed Identity)
To make sure our Identity can deploy and manage our applications, it needs two additional setups:
- Give it 'contributer' rights to our resource group
- Make the github workflow can login using this Identity
Giving the Identity the correct rights: Open the resource group containing all of your applicaitons and select Access Control (IAM) in the sidebar. Add a new Role Assignment from the top bar. Select the Privileged administrator role Contributor. Next, assign this access to you newly created Managed Identity. Review and assign this access. Once assigned, check in the tab Role Assignments that your Identity has the Contributor role.
Make sure Github Actions can log in: Open your Managed Identity and select Federated credentials under Settings. Add a new credential for Github Actions for your repository with the Environment entity Production. Enter a name for this credential (e.g. ProductionDeploy) and add the credential.
Set Up the GitHub Action
Github Repository Setup
First we need to make sure Azure OIDC is connected to our repository. To do this we need to add the correct environment to our repository.
In you repository settings, find Environments and create a new environment. Name it Production, just like the step above to make sure our repo has the right to interact with Azure. Restrict this environment to the main branch of your repository.
Necessary GitHub Secrets (Required for all subsequent steps)
| Secret Name | Purpose | Value Source | Defined in |
|---|---|---|---|
AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID | OIDC Authentication | UAMI & Subscription Page(*) | Repository secrets |
AZURE_WEBAPP_NAME, AZURE_WEBAPP_CLIENT_NAME | Deployment Targets | Azure Web App Overview | Environment secrets |
API_URL | Frontend API URL | Backend Web App URL | Environment secrets |
AZURE_SQL_SERVER, AZURE_SQL_DB, AZURE_SQL_ADMIN, AZURE_SQL_PASSWORD | EF Core Migration Credentials | SQL Server Settings(**) | Environment secrets |
Client ID and Subsciption ID can be found on your User Assigned Managed Identity, Tenant ID can be found on your Subscription under the name *Parent management group
**Can also be replace with connection string, however, you have more control this way.
Workflow File Initialization
Create a file named .github/workflows/deployment.yml. The file defines two parallel jobs (backend-deploy and frontend-deploy) triggered on every push to the main branch.
The first lines define the workflow's identity, trigger, and security permissions.
name: Deploy application
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
This initial setup will make sure that your app will be deployed each time a push has been done to the main branch.
The permissions are necessary here to make sure the azure CLI can access our OIDC token.
Add Back-End Deploy
Start off by defining the environment for which you want to authenticate Azure and determine on which platform you want to run your deployment action.
The first steps in this backend-deploy are getting our code, setting up .NET and logging in on the Azure CLI. For this we use the provided action/checkout, action/setup-dotnet and azure/login actions. Make sure you provide the correct .NET version to the Setup step.
You can authenticate Azure CLI in a number of ways, but we will use our AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_SUBSCRIPTION_ID that we kept from creating our service principle.
backend-deploy:
environment: Production
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: "9.0.x"
- name: Restore and build app
run: |
dotnet restore
dotnet build
- name: Login to Azure using OIDC
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
Add Dotnet Migrations
The backend-deploy job includes a critical step to run Entity Framework Core migrations against the Azure SQL Database before deployment. This ensures the database schema is up-to-date.
Because the free tier databases might take a while to start up, it might be a good idea to build in some retries in your migrations in case the database takes too long to spin up.
- name: Run EF Core migrations
run: |
# Installs the EF Core command line tool
dotnet new tool-manifest
dotnet tool install dotnet-ef --version 9.*
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
for i in {1..5}; do
echo "Attempt $i to run EF migrations..."
# Executes the migration command using a connection string composed of secrets
dotnet ef database update --startup-project <Path to startup project> --project <Path to migrations project> --connection "Server=tcp:${{ secrets.AZURE_SQL_SERVER }}.database.windows.net,1433;Initial Catalog=${{ secrets.AZURE_SQL_DB }};Persist Security Info=False;User ID=${{ secrets.AZURE_SQL_ADMIN }};Password=${{ secrets.AZURE_SQL_PASSWORD }};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" && break
echo "Migration failed, retrying in 10 seconds..."
sleep 10
done
shell: bash
Add Deploy
The final steps of the backend-deploy job compile and publish the .NET application to the Azure Web App.
- name: Create publish build
run: dotnet publish -o publish
- name: Deploy to Azure Web App
# Uses the built-in Azure Web Apps deploy action
uses: azure/webapps-deploy@v2
with:
app-name: ${{ secrets.AZURE_WEBAPP_NAME }} # Backend Web App Name
package: ./publish # The output folder from the publish step
Add Front-end Deploy
The frontend-deploy job handles the Angular/Nx client, injecting environment variables (like the API URL) during the build phase before deploying the static assets.
Start this job as well with determining your environment and platform.
The front-end may only be deployed if the back-end deploy was successful. The option needs ensures this.
The first steps are to checkout the code en install all needed npm packages.
frontend-deploy:
environment: Production
runs-on: ubuntu-latest
needs: backend-deploy
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install dependencies
run: |
npm install
Add .env file
Because our app uses environment variables, we need to provide them in the build step. We are creating a new .env file in the project folder. You can either provide the variables directly or use Github secrets to mask your variables in this build step.
- name: Create .env file
run: |
touch <Path to project>/.env
echo NG_APP_TEST_VALUE="Testvalue from deploy" >> <Path to project>/.env
echo NG_APP_LEGO_API_URL=${{ secrets.API_URL }} >> <Path to project>/.env
cat <Path to project>/.env # Print for debugging
Add Deploy
In this last step, we build our app with NX, login to Azure again, because it is a different job, it runs on a different runner, so the previous authentication from the backend deploy does not persist. Lastly, we deploy our app to Azure.
- name: Build app
run: |
npx nx run <app name>:build
- name: Login to Azure using OIDC
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: ${{ secrets.AZURE_WEBAPP_CLIENT_NAME }}
package: ./dist/apps/<app name>/browser
Project setup
Because your project needs more than one front-end and more than one environment, you can find a suggestion on how to structure this below.
Azure services
| Name | Type | Instances/Environment | Description |
|---|---|---|---|
| APP Resource Group | Resource Group | Single | Collection of all needed services |
| Deploy Identity | User Assigned Managed Identity | Single | Identity needed to deploy App via Github Actions |
| SQL Server | Azure SQL Server | Single | Hosts all SQL Databases |
| Azure SQL Database | Azure SQL DB | DEV, TST & PRD | Database for Backend API |
| Service Plan BE | Service Plan (Windows) | Single | Hosts all Backends |
| Backend API | Web App | DEV, TST & PRD | Backend-api deployment |
| Service Plan FE | Service Plan (Linux) | Single | Hosts all Frontends |
| Client Frontend | Web App | DEV, TST & PRD | Frontend-client deployment |
| Admin Frontend | Web App | DEV, TST & PRD | Frontend-admin deployment |
Github Secrets
| Secret Name | Instances/Environment | Description |
|---|---|---|
AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID | Single | OIDC Authentication |
AZURE_WEBAPP_NAME, AZURE_WEBAPP_CLIENT_NAME, AZURE_WEBAPP_ADMIN_NAME | DEV, TST & PRD | Deployment targets |
API_URL | DEV, TST & PRD | URL of the API to use in Frontend |
AZURE_SQL_SERVER, AZURE_SQL_DB, AZURE_SQL_ADMIN, AZURE_SQL_PASSWORD | DEV, TST & PRD | DB Auth to use in migrations script |
Github workflows
| Workflow Name | Instances/Environment | Description |
|---|---|---|
CI.yml | Single | Test application on Pull Request |
<env>-build-and-deploy.yml | DEV, TST & PRD | Deploy to target environment depending on branch |
OR build-and-deploy.yml(*) | Single | Determine environment based on branch |
*Try to work it out so you only need ONE single build-and-deploy.yml file to deploy to every environment.