zkbuild-action

June 29, 2026 · View on GitHub

Automated TwinCAT CI/CD without Jenkins: Stop building TwinCAT in the IDE. Get reproducible, tested, automated builds in your GitHub workflow.

zkbuild-action is a GitHub Action that builds, tests, and packages TwinCAT PLCs directly from your repository. No Jenkins/Buildsystem setup. No complex infrastructure. Push code → automatic build + unit tests → artifacts ready to deploy.

Table of Contents

Why You Need This

  • Reproducible builds: Same output every time, on any machine
  • Automated testing: Run TcUnit tests in CI; fail the build if tests fail
  • No manual builds: Eliminate the "forgot to rebuild" and "works on my machine" problems
  • Audit trail: Every build is logged and traceable (required for compliance)
  • Zero DevOps: Works with GitHub. No build servers to maintain

The Problem It Solves

Most TwinCAT engineers build locally in the IDE:

1. Engineer writes code in TwinCAT IDE (local machine)
2. Manual testing on hardware
3. Activate to target in the IDE
5. Hope nothing breaks in production

This breaks at scale. You need:

  • Multiple engineers working on the same code → build conflicts
  • Regulatory traceability → manual builds leave no audit trail
  • New team members → "how do I build this?" becomes a mystery

zkbuild-action solves this: Push code → Automated build + tests → Versioned artifacts.

Quick Start

1. Register (Free for Open Source):

Register here to get credentials. You'll receive 30 free builds/month for public repos.

2. Create a GitHub Workflow:

Add this file to your repo: .github/workflows/build.yml

name: Build/Test
on:
  push:
    branches:
      - main
      - 'release/**'
  pull_request_target:
  workflow_dispatch:
jobs:
  Build:
    name: Build/Test
    runs-on: ubuntu-latest
    steps:
      - name: Build
        uses: Zeugwerk/zkbuild-action@1.0.0
        with:
          username: ${{ secrets.ACTIONS_ZGWK_USERNAME }}
          password: ${{ secrets.ACTIONS_ZGWK_PASSWORD }}
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: artifact
          path: |
            **/*library 
      - name: Publish Unittest
        uses: EnricoMi/publish-unit-test-result-action@v1
        with:
          files: archive/test/TcUnit_xUnit_results.xml
  1. Add credentials: Store your Zeugwerk username/password as GitHub Secrets (don't commit them!)
  2. Push: Commit this workflow and watch builds run automatically

How It Works

sequenceDiagram
    participant Dev as Developer
    participant Repo as Git Repository (GitHub)
    participant Action as zkbuild-action (GitHub Action)
    participant CI as Zeugwerk CI/CD

    Dev->>Repo: 🚀 Push commit / create tag
    Repo-->>Action: 🔔 Webhook trigger

    Action->>CI: ⚙️ Start build pipeline

    Note over CI: Zeugwerk CI/CD Pipeline Execution

    CI->>CI: 📝 Generate changelog
    CI->>CI: 🛠️ Build (.library file if needed)
    CI->>CI: 🧪 Run unit tests
    CI->>CI: 📦 Collect Artifacts

    CI-->>Action: 📊 Build artifacts + test reports
    Action-->>Repo: ⬆️ Upload artifacts & reports

    Repo-->>Dev: ✅ Results visible in repository

No Jenkins. No servers. No maintenance.

Configuration

Inputs

InputRequiredDescription
usernameYesZeugwerk account username
passwordYesZeugwerk account password
tcversionNoTwinCAT version (default: TC3.1)

Outputs

ArtifactLocation
.library file(s)archive/<repo>/<tcversion>/<plc_name>_<version>.library
Test results (JUnit XML)archive/test/TcUnit_xUnit_results.xml
Build logsAvailable in GitHub Actions UI

Basic (Single PLC, no dependencies)

Create .Zeugwerk/config.json in your repo:

{
  "fileversion": 1,
  "solution": "MyProject.sln",
  "projects": [
    {
      "name": "MyProject",
      "plcs": [
        {
          "name": "MainPLC",
          "version": "1.0.0.0",
          "type": "Library"
        }
      ]
    }
  ]
}

(Use Twinpack to generate this automatically)

With Unit Tests

zkbuild-action runs unit tests automatically. Two options:

Option A: Tests in your PLC code (recommended)

  • Implement function blocks that extend Testbench.IUnittest or for Zeugwerk-Framework users ZCore.IUnittest.
  • zkbuild automatically finds and runs them
  • Requires ZCore package or Testbench (do not mix!)

Example:

FUNCTION_BLOCK MyFunctionTest EXTENDS MyFunction IMPLEMENTS ZCore.IUnittest

METHOD TestPositiveValue
VAR_INPUT
  assertions : ZCore.IAssertions;
END_VAR

assertions.EqualsDint(42, MyFunction_instance.Calculate(42), 'Test failed');
END_METHOD

END_FUNCTION_BLOCK

See Documentation for details about the supported method signatures.

Option B: Separate test project

  • Create a tests/ folder with a separate .plcproj
  • Use TcUnit
  • zkbuild runs both projects

Pricing

  • Free: 30 builds/month for public repositories
  • Commercial: Custom pricing for private repos and higher volume. Contact us

How It Compares

SolutionSetup TimeCostComplexityIncludes Tests?
Local TwinCAT IDE0FreeNoneNo
Self-hosted Jenkins2-4 weeks€0-5k + timeHighNo (you add it)
GitHub Actions (DIY)2-4 weeks€0-5k + time + €0-20/moHighNo (you add it)
zkbuild-action1 hourCustomLowYes ✓

Examples

Troubleshooting

"Build failed: credentials not found"

  • Check that ZEUGWERK_USERNAME and ZEUGWERK_PASSWORD are set in your GitHub Secrets

"TwinCAT version mismatch"

  • Use tcversion: TC3.1.4024 to specify an exact version

"Tests aren't running"

  • Ensure function blocks implement Testbench.IUnittest OR ZCore.IUnittestOR use a separatetests/` folder with TcUnit

Advanced Features: Config with Dependencies

Config with (automatically resolved, downloaded and installed) Dependencies

{
  "fileversion": 1,
  "solution": "TwinCAT Project1.sln",
  "projects": [
    {
      "name": "TwinCAT Project1",
      "plcs": [
        {
          "version": "1.0.0.0",
          "name": "Untitled1",
          "type": "Library",
          "packages": [
            {
              "version": "1.2.19.0",
              "repository": "bot",
              "name": "ZCore",
              "branch": "release/1.2",
              "target": "TC3.1",
              "configuration": "Distribution",
              "distributor-name": "Zeugwerk GmbH"
            }
          ],
          "references": {
            "*": [
              "Tc2_Standard=*",
              "Tc2_System=*",
              "Tc3_Module=*"
            ]
          }
        }
      ]
    }
  ]
}

Advanced Features: Patches

Zeugwerk CI supports to apply patches to the source code of the repository, which are applied before compiling the code. At the moment the following types of patches are supported

  • Platform patches: are applied for specific TwinCAT Versions
  • Argument patches: can be used for feature flags. At the moment this feature is not enabled for the free-to-use zkbuild, if you are interested in using it for your open source project, you may submit an issue.

To use patches, a config.json file has to be used and patches have to be configured in the configuration file as follows

{
  "fileversion": 1,
  "solution": "TwinCAT Project1.sln",
  "projects": [
    {
      "name": "TwinCAT Project1",
      "plcs": [
        {
          ...
          "name": "Untitled1",
          "patches":
            "platform": {
              "TC3.1.4024.56": [
                "git-patch-for-specific-twincat-version.patch",
                "search-and-replace.for-specific-twincat-version.replacement"
              ]
            },
            "argument": {
              "": [
                "git-patch.patch",
                "search-and-replace.replacement"
              ]
            },
          ...
        }
      ]
    }
  ]
}

Git patches

Any file with the extension .patch is applied as a Git patch. You can use git diff > some-changes.patch to create the patch and the CI system, will use git apply -3 some-changes.patch to apply the patch. If the patch can not be applied correctly, the pipeline will go to failure.

Further Reading


Search-and-replace patch

We support a mechanismn to do search-and-replace over many files in the source code before it is compiled. This is simpler than using a proper git patch. A replacement patch is a json file, which has the following format and has the file extension .replacement

{
  "filter": "*.TcPOU",
  "search": "pattern",
  "replace": "replacement"
}
  • filter can be used to filter for specific files, the examples filters for all POUs in the source code
  • search is a the regular expression pattern to match, special characters have to be escaped
  • replace is the replacement string, which may contain identifiers to refer to captures of the regex ($1 ... $N). It is also possible to use enviornment variables here. Zeugwerk CI uses Jenkins, enviornment variables can be referred to with {{env.ENVIORNMENT_VARIABLE}}. See here for a list of possible enviornmemt variables.