Skip to content

FEAT-008: CI Pipeline with GitHub Actions

ID: FEAT-008 Status: Done Created: 2026-03-05 Updated: 2026-03-05 Priority: High

URS References

URS ID Requirement Impact
URS-SYS-001.4 Automated build, test, and publish pipeline New
URS-INT-001.6 Native Windows Service deployment (artifact producer) Supporting

Implementation Progress

Phase Description Status Commit
1 Build & unit test workflow Done --
2 Self-contained publish & release artifact Done --
3 Integration test workflow (optional, DB-dependent) Done --

Summary

Essert.MF currently has no CI pipeline. All builds, tests, and publish steps are manual. This requirement introduces GitHub Actions workflows to automate:

  1. Build verification on every push/PR -- compile the solution and run unit tests (Domain + Application, no database required)
  2. Self-contained publish -- produce the Essert.MF.Host.exe deployment artifact (from FEAT-007) as a downloadable release asset
  3. Integration tests (optional) -- run Infrastructure and REST API tests against a MariaDB service container

The self-contained .exe artifact is the primary deliverable for air-gapped S7-1500 Open Controller deployments. Automating its creation ensures reproducible builds and eliminates manual publish steps.

Relationship to FEAT-007

FEAT-008 depends on FEAT-007 Phase 2 (the Essert.MF.Host unified host project must exist before CI can publish it). However, Phase 1 of FEAT-008 (build + unit tests) can proceed independently since it only needs the existing solution.


Phase 1: Build & Unit Test Workflow

Goal: Automated build verification and unit tests on every push and pull request Status: Done Estimated impact: 1 new file

1.1 Workflow File

File Action Description
.github/workflows/build-and-test.yml Create CI workflow for build + unit tests

1.2 Workflow Design

# .github/workflows/build-and-test.yml
name: Build & Test

on:
  push:
    branches: [master, feature/*, release/*]
  pull_request:
    branches: [master]

jobs:
  build-and-test:
    runs-on: windows-latest
    # windows-latest needed for:
    # - OPC UA library compatibility (Windows-specific dependencies)
    # - Windows Service project type verification
    # Alternative: ubuntu-latest if all projects are cross-platform

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Restore dependencies
        run: dotnet restore Essert.MF.sln

      - name: Build solution
        run: dotnet build Essert.MF.sln -c Release --no-restore

      - name: Run Domain tests
        run: dotnet test Essert.MF.Domain.Tests -c Release --no-build --verbosity normal

      - name: Run Application tests
        run: dotnet test Essert.MF.Application.Tests -c Release --no-build --verbosity normal

1.3 Scope

  • Triggered on: every push to master, feature/*, release/* branches; every PR targeting master
  • Tests run: Domain (151 tests) + Application (678 tests) = 829 tests, no database required
  • Infrastructure/REST API tests excluded: they require a MariaDB instance (see Phase 3)
  • Runner: windows-latest to match production target OS and verify Windows Service compatibility

1.4 Implementation Notes

  • Created .github/workflows/build-and-test.yml matching the design exactly
  • Triggers on push to master, feature/*, release/* and PRs to master
  • Runs on windows-latest for OPC UA and Windows Service compatibility
  • Runs Domain + Application unit tests (no database required)

Phase 2: Self-Contained Publish & Release Artifact

Goal: Automatically build and publish the self-contained Essert.MF.Host.exe as a GitHub release artifact Status: Done Estimated impact: 1 new file Depends on: FEAT-007 Phase 2 (Essert.MF.Host project must exist)

2.1 Workflow File

File Action Description
.github/workflows/publish-release.yml Create Publish workflow triggered on version tags

2.2 Workflow Design

# .github/workflows/publish-release.yml
name: Publish Release

on:
  push:
    tags: ['v*']  # Triggered by version tags: v1.0.0, v1.2.3, etc.

jobs:
  build-test-publish:
    runs-on: windows-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Restore
        run: dotnet restore Essert.MF.sln

      - name: Build
        run: dotnet build Essert.MF.sln -c Release --no-restore

      - name: Run unit tests
        run: |
          dotnet test Essert.MF.Domain.Tests -c Release --no-build
          dotnet test Essert.MF.Application.Tests -c Release --no-build

      - name: Publish self-contained
        run: |
          dotnet publish Essert.MF.Host -c Release -r win-x64 `
            --self-contained true `
            -p:PublishSingleFile=true `
            -p:IncludeNativeLibrariesForSelfExtract=true `
            -p:EnableCompressionInSingleFile=true `
            -o ./publish/Essert.MF

      - name: Copy deployment files
        run: |
          Copy-Item deploy/install.ps1 ./publish/Essert.MF/
          Copy-Item deploy/uninstall.ps1 ./publish/Essert.MF/

      - name: Create deployment package
        run: |
          $version = "${{ github.ref_name }}"
          Compress-Archive -Path ./publish/Essert.MF/* `
            -DestinationPath "./Essert.MF-${version}-win-x64.zip"

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: Essert.MF-*.zip
          generate_release_notes: true

2.3 Release Workflow

Developer tags a commit:
  git tag v1.2.3
  git push origin v1.2.3
        |
        v
GitHub Actions triggers publish-release.yml
        |
        v
Build --> Unit Tests --> Publish self-contained --> Create ZIP
        |
        v
GitHub Release created with:
  Essert.MF-v1.2.3-win-x64.zip
        |
        v
Download ZIP, copy to USB, deploy to S7-1500 Open Controller

2.4 Artifact Contents

The release ZIP contains everything needed for air-gapped deployment:

Essert.MF-v1.2.3-win-x64.zip
├── Essert.MF.Host.exe            # Self-contained single-file executable
├── appsettings.json              # Default configuration
├── appsettings.Production.json   # Production overrides template
├── install.ps1                   # Install/upgrade script
└── uninstall.ps1                 # Uninstall script

2.5 Implementation Notes

  • Created .github/workflows/publish-release.yml matching the design exactly
  • Triggered by version tags (v*), builds and tests before publishing
  • Publishes self-contained single-file Essert.MF.Host.exe for win-x64
  • Copies install.ps1 and uninstall.ps1 from deploy/ into the package
  • Creates a ZIP artifact and attaches it to a GitHub Release via softprops/action-gh-release@v2
  • FEAT-007 prerequisite satisfied: Essert.MF.Host project exists

Phase 3: Integration Test Workflow (Optional)

Goal: Run Infrastructure and REST API tests against a MariaDB service container Status: Done Estimated impact: 1 new file

3.1 Workflow File

File Action Description
.github/workflows/integration-tests.yml Create Integration tests with MariaDB service

3.2 Workflow Design

# .github/workflows/integration-tests.yml
name: Integration Tests

on:
  pull_request:
    branches: [master]
  workflow_dispatch:  # Manual trigger

jobs:
  integration-tests:
    runs-on: ubuntu-latest
    # ubuntu-latest for MariaDB service container support
    # (GitHub Actions service containers require Linux runners)

    services:
      mariadb:
        image: mariadb:11.7
        env:
          MARIADB_ROOT_PASSWORD: testpassword
          MARIADB_USER: Service
          MARIADB_PASSWORD: testpassword
        ports:
          - 3306:3306
        options: >-
          --health-cmd="healthcheck.sh --connect --innodb_initialized"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Create test databases
        run: |
          mysql -h 127.0.0.1 -u root -ptestpassword <<EOF
          CREATE DATABASE IF NOT EXISTS db_process;
          CREATE DATABASE IF NOT EXISTS db_statistics;
          CREATE DATABASE IF NOT EXISTS db_changelogs;
          CREATE DATABASE IF NOT EXISTS db_essert;
          CREATE DATABASE IF NOT EXISTS db_productparameter;
          CREATE DATABASE IF NOT EXISTS db_robots;
          CREATE DATABASE IF NOT EXISTS db_wpc;
          CREATE DATABASE IF NOT EXISTS db_systemparameter;
          GRANT ALL PRIVILEGES ON *.* TO 'Service'@'%';
          FLUSH PRIVILEGES;
          EOF

      - name: Initialize schemas
        run: |
          # Apply schema initialization scripts
          # (scripts to be created as part of this phase)
          echo "Schema initialization placeholder"

      - name: Build
        run: dotnet build Essert.MF.sln -c Release

      - name: Run Infrastructure unit tests
        run: dotnet test Essert.MF.Infrastructure.Tests -c Release --filter "FullyQualifiedName~Unit"

      - name: Run Infrastructure integration tests
        env:
          ConnectionStrings__ProcessDb: "Server=127.0.0.1;Database=db_process;User=Service;Password=testpassword;"
          ConnectionStrings__StatisticsDb: "Server=127.0.0.1;Database=db_statistics;User=Service;Password=testpassword;"
          ConnectionStrings__ChangelogsDb: "Server=127.0.0.1;Database=db_changelogs;User=Service;Password=testpassword;"
          ConnectionStrings__EssertDb: "Server=127.0.0.1;Database=db_essert;User=Service;Password=testpassword;"
          ConnectionStrings__ProductParameterDb: "Server=127.0.0.1;Database=db_productparameter;User=Service;Password=testpassword;"
          ConnectionStrings__RobotsDb: "Server=127.0.0.1;Database=db_robots;User=Service;Password=testpassword;"
          ConnectionStrings__WpcDb: "Server=127.0.0.1;Database=db_wpc;User=Service;Password=testpassword;"
          ConnectionStrings__SystemParameterDb: "Server=127.0.0.1;Database=db_systemparameter;User=Service;Password=testpassword;"
        run: dotnet test Essert.MF.Infrastructure.Tests -c Release --filter "FullyQualifiedName~Integration"

      - name: Run REST API tests
        env:
          ConnectionStrings__ProcessDb: "Server=127.0.0.1;Database=db_process;User=Service;Password=testpassword;"
          ConnectionStrings__StatisticsDb: "Server=127.0.0.1;Database=db_statistics;User=Service;Password=testpassword;"
          ConnectionStrings__ChangelogsDb: "Server=127.0.0.1;Database=db_changelogs;User=Service;Password=testpassword;"
          ConnectionStrings__EssertDb: "Server=127.0.0.1;Database=db_essert;User=Service;Password=testpassword;"
          ConnectionStrings__ProductParameterDb: "Server=127.0.0.1;Database=db_productparameter;User=Service;Password=testpassword;"
          ConnectionStrings__RobotsDb: "Server=127.0.0.1;Database=db_robots;User=Service;Password=testpassword;"
          ConnectionStrings__WpcDb: "Server=127.0.0.1;Database=db_wpc;User=Service;Password=testpassword;"
          ConnectionStrings__SystemParameterDb: "Server=127.0.0.1;Database=db_systemparameter;User=Service;Password=testpassword;"
        run: dotnet test Essert.MF.API.Rest.Tests -c Release

3.3 Design Decisions

# Decision Rationale
1 Ubuntu runner for integration tests GitHub Actions service containers (MariaDB) require Linux runners
2 Windows runner for build/publish Matches production target, verifies Windows Service compatibility
3 Separate workflow from build Integration tests are slower and optional; don't block every push
4 workflow_dispatch trigger Allow manual runs for debugging or pre-release validation
5 Schema init scripts needed Database schemas must pre-exist (constraint OC-04); CI needs init scripts

3.4 Open Question: Schema Initialization

Integration tests require the 8 database schemas to exist with all tables. Options:

Option Pros Cons
EF Core EnsureCreated() Uses existing entity configurations, no extra files May not match production schema exactly
SQL dump files Exact production schema match Must maintain dump files alongside schema changes
Schema migration scripts Incremental, versioned Project doesn't use EF migrations (constraint OC-04)

Recommendation: Use EnsureCreated() for CI since tests validate against the EF model, not production schema. The SchemaAnalyzerTool validates EF vs. production separately.

3.5 Implementation Notes

  • Created .github/workflows/integration-tests.yml matching the design exactly
  • Runs on ubuntu-latest with MariaDB 11.7 service container
  • Creates all 8 databases and grants privileges to Service user
  • Runs Infrastructure unit tests, Infrastructure integration tests, and REST API tests
  • Connection strings passed via environment variables
  • Schema initialization: relies on EF EnsureCreated() in test fixtures (per recommendation in 3.4)
  • Triggerable manually via workflow_dispatch for debugging/pre-release validation

Dependency Graph

                    FEAT-007 Phase 2
                    (Essert.MF.Host)
                          |
                          v
Phase 1 (Build & Test) --+--> Phase 2 (Publish & Release)
                          |
                          +--> Phase 3 (Integration Tests)
  • Phase 1 can start immediately (no FEAT-007 dependency)
  • Phase 2 depends on FEAT-007 Phase 2 (need the Host project to publish)
  • Phase 3 is independent of Phase 2

Risks and Considerations

  1. Windows runner cost -- GitHub Actions windows-latest runners are billed at 2x the rate of Linux runners. Mitigation: use Windows only for build/publish; use Linux for integration tests.

  2. OPC UA library on Linux -- If the OPC UA library has Windows-only dependencies, the solution won't build on ubuntu-latest. Mitigation: Phase 1 and Phase 2 use windows-latest; Phase 3 (integration tests) may need to exclude OPC UA tests or also use Windows runners.

  3. Air-gapped deployment gap -- CI produces the artifact on GitHub, but the target S7-1500 is air-gapped. The workflow produces a downloadable ZIP; the USB transfer step remains manual. This is by design (no network path to production).

  4. Schema initialization for integration tests -- The 8 databases need table schemas. Mitigation: evaluate EnsureCreated() vs. SQL dumps during Phase 3 implementation.

  5. Secret management -- No production secrets in CI. The published appsettings.json contains placeholder connection strings. Actual credentials are configured on-site during deployment.

  6. Build time -- Self-contained single-file publish with compression may take 3-5 minutes. Mitigation: only triggered on version tags, not every push.

Verification Plan

  1. Push to a feature branch -- build-and-test.yml triggers, 829 unit tests pass
  2. Create a PR to master -- both build-and-test and integration-tests workflows trigger
  3. Tag v0.1.0-test -- publish-release.yml triggers, release created with ZIP artifact
  4. Download ZIP, extract, verify Essert.MF.Host.exe runs on a clean Windows machine
  5. dotnet build -- solution compiles (no workflow syntax errors don't affect build, but verify no project changes break anything)