Skip to main content

GitHub Actions

About 8 min

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 NamePurposeNotes
Resource GroupLogical container for all resources (e.g., SweDemoGroup).Used as the scope for the Service Principal in the CLI setup.
SQL Server & DatabaseHosts 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 IdentityNew 'user' to manage and deploy our applicationsThis 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.

StepActionCommand/Purpose
a. Navigate in PortalGo 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 CommandSet 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 AreaNamePurposeValue 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 settingsALLOWED_ORIGINSA 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 Type SQLServer, containing the full connection string.
  • On the Application settings tab, add a new setting named ALLOWED_ORIGINS with 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 NamePurposeValue SourceDefined in
AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_IDOIDC AuthenticationUAMI & Subscription Page(*)Repository secrets
AZURE_WEBAPP_NAME, AZURE_WEBAPP_CLIENT_NAMEDeployment TargetsAzure Web App OverviewEnvironment secrets
API_URLFrontend API URLBackend Web App URLEnvironment secrets
AZURE_SQL_SERVER, AZURE_SQL_DB, AZURE_SQL_ADMIN, AZURE_SQL_PASSWORDEF Core Migration CredentialsSQL 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

NameTypeInstances/EnvironmentDescription
APP Resource GroupResource GroupSingleCollection of all needed services
Deploy IdentityUser Assigned Managed IdentitySingleIdentity needed to deploy App via Github Actions
SQL ServerAzure SQL ServerSingleHosts all SQL Databases
Azure SQL DatabaseAzure SQL DBDEV, TST & PRDDatabase for Backend API
Service Plan BEService Plan (Windows)SingleHosts all Backends
Backend APIWeb AppDEV, TST & PRDBackend-api deployment
Service Plan FEService Plan (Linux)SingleHosts all Frontends
Client FrontendWeb AppDEV, TST & PRDFrontend-client deployment
Admin FrontendWeb AppDEV, TST & PRDFrontend-admin deployment

Github Secrets

Secret NameInstances/EnvironmentDescription
AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_IDSingleOIDC Authentication
AZURE_WEBAPP_NAME, AZURE_WEBAPP_CLIENT_NAME, AZURE_WEBAPP_ADMIN_NAMEDEV, TST & PRDDeployment targets
API_URLDEV, TST & PRDURL of the API to use in Frontend
AZURE_SQL_SERVER, AZURE_SQL_DB, AZURE_SQL_ADMIN, AZURE_SQL_PASSWORDDEV, TST & PRDDB Auth to use in migrations script

Github workflows

Workflow NameInstances/EnvironmentDescription
CI.ymlSingleTest application on Pull Request
<env>-build-and-deploy.ymlDEV, TST & PRDDeploy to target environment depending on branch
OR build-and-deploy.yml(*)SingleDetermine 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.