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:
- Build verification on every push/PR -- compile the solution and run unit tests (Domain + Application, no database required)
- Self-contained publish -- produce the
Essert.MF.Host.exedeployment artifact (from FEAT-007) as a downloadable release asset - 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 targetingmaster - 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-latestto match production target OS and verify Windows Service compatibility
1.4 Implementation Notes¶
- Created
.github/workflows/build-and-test.ymlmatching the design exactly - Triggers on push to
master,feature/*,release/*and PRs tomaster - Runs on
windows-latestfor 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.ymlmatching the design exactly - Triggered by version tags (
v*), builds and tests before publishing - Publishes self-contained single-file
Essert.MF.Host.exeforwin-x64 - Copies
install.ps1anduninstall.ps1fromdeploy/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.Hostproject 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.ymlmatching the design exactly - Runs on
ubuntu-latestwith MariaDB 11.7 service container - Creates all 8 databases and grants privileges to
Serviceuser - 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_dispatchfor 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¶
-
Windows runner cost -- GitHub Actions
windows-latestrunners are billed at 2x the rate of Linux runners. Mitigation: use Windows only for build/publish; use Linux for integration tests. -
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 usewindows-latest; Phase 3 (integration tests) may need to exclude OPC UA tests or also use Windows runners. -
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).
-
Schema initialization for integration tests -- The 8 databases need table schemas. Mitigation: evaluate
EnsureCreated()vs. SQL dumps during Phase 3 implementation. -
Secret management -- No production secrets in CI. The published
appsettings.jsoncontains placeholder connection strings. Actual credentials are configured on-site during deployment. -
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¶
- Push to a feature branch --
build-and-test.ymltriggers, 829 unit tests pass - Create a PR to master -- both build-and-test and integration-tests workflows trigger
- Tag
v0.1.0-test--publish-release.ymltriggers, release created with ZIP artifact - Download ZIP, extract, verify
Essert.MF.Host.exeruns on a clean Windows machine dotnet build-- solution compiles (no workflow syntax errors don't affect build, but verify no project changes break anything)