Skip to content
Latchkey

Build Windows Installer workflow (Autonomy-Logic/openplc-runtime)

The Build Windows Installer workflow from Autonomy-Logic/openplc-runtime, explained and optimized by Latchkey.

C

CI health: C - fair

Run this on Latchkey for self-healing, caching, and up to 58% lower cost.

Grade your own workflow free or run it on Latchkey →
Source: Autonomy-Logic/openplc-runtime.github/workflows/windows-installer.ymlLicense MITView source

What it does

This is the Build Windows Installer workflow from the Autonomy-Logic/openplc-runtime repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Build Windows Installer

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      version:
        description: 'Version number for the installer'
        required: false
        default: '4.0'

jobs:
  build-windows-installer:
    # Skip pre-release tags (those containing a hyphen, e.g. v4.1.0-rc.1)
    if: ${{ !contains(github.ref_name, '-') || github.event_name == 'workflow_dispatch' }}
    runs-on: windows-latest
    permissions:
      contents: write

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

      - name: Setup MSYS2
        uses: msys2/setup-msys2@v2
        with:
          msystem: MSYS
          update: true
          install: >-
            base-devel
            gcc
            make
            cmake
            pkg-config
            python
            python-pip
            python-setuptools
            git
            sqlite3

      - name: Cache MSYS2 packages
        uses: actions/cache@v4
        with:
          path: D:\a\_temp\msys64\var\cache\pacman\pkg
          key: msys2-packages-${{ runner.os }}-${{ hashFiles('.github/workflows/windows-installer.yml') }}
          restore-keys: |
            msys2-packages-${{ runner.os }}-

      - name: Install OpenPLC Runtime using install.sh
        shell: msys2 {0}
        run: |
          echo "=========================================="
          echo "Installing OpenPLC Runtime via install.sh"
          echo "=========================================="

          # Run the install script which handles all dependencies and build
          ./install.sh

          echo "Installation complete!"

      - name: Prepare installer payload
        shell: pwsh
        run: |
          Write-Host "Preparing installer payload..."

          # Create payload directory structure
          New-Item -ItemType Directory -Force -Path "windows\payload\msys64"
          New-Item -ItemType Directory -Force -Path "windows\payload\openplc-runtime"

          # Copy MSYS2 installation
          Write-Host "Copying MSYS2 installation (this may take a while)..."
          $msys2Path = "D:\a\_temp\msys64"
          if (Test-Path $msys2Path) {
            Copy-Item -Path "$msys2Path\*" -Destination "windows\payload\msys64" -Recurse -Force
          } else {
            Write-Host "MSYS2 path not found at $msys2Path, trying C:\msys64..."
            Copy-Item -Path "C:\msys64\*" -Destination "windows\payload\msys64" -Recurse -Force
          }

          # Copy OpenPLC Runtime (excluding unnecessary files)
          Write-Host "Copying OpenPLC Runtime..."
          $excludeDirs = @('.git', '.github', '.mypy_cache', '.ruff_cache', '.vscode', 'tests', '__pycache__')

          Get-ChildItem -Path "." -Exclude $excludeDirs | Where-Object {
            $_.Name -ne "windows" -and $_.Name -ne ".git"
          } | ForEach-Object {
            if ($_.PSIsContainer) {
              Copy-Item -Path $_.FullName -Destination "windows\payload\openplc-runtime\$($_.Name)" -Recurse -Force
            } else {
              Copy-Item -Path $_.FullName -Destination "windows\payload\openplc-runtime\" -Force
            }
          }

          # Clean up to reduce size
          Write-Host "Cleaning up payload to reduce size..."

          # Remove pacman cache
          $pacmanCache = "windows\payload\msys64\var\cache\pacman\pkg"
          if (Test-Path $pacmanCache) {
            Remove-Item -Path "$pacmanCache\*" -Force -Recurse -ErrorAction SilentlyContinue
          }

          # Remove unnecessary MSYS2 directories
          $msys2Cleanup = @(
            "windows\payload\msys64\var\log",
            "windows\payload\msys64\tmp"
          )
          foreach ($dir in $msys2Cleanup) {
            if (Test-Path $dir) {
              Remove-Item -Path "$dir\*" -Force -Recurse -ErrorAction SilentlyContinue
            }
          }

          # Calculate payload size
          $payloadSize = (Get-ChildItem -Path "windows\payload" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
          Write-Host "Payload size: $([math]::Round($payloadSize, 2)) MB"

      - name: Install Inno Setup
        shell: pwsh
        run: |
          Write-Host "Installing Inno Setup..."
          choco install innosetup -y --no-progress

          # Add Inno Setup to PATH
          $env:PATH = "C:\Program Files (x86)\Inno Setup 6;$env:PATH"
          [Environment]::SetEnvironmentVariable("PATH", $env:PATH, "Process")

      - name: Create placeholder icon
        shell: pwsh
        run: |
          # Create a simple placeholder icon if one doesn't exist
          # In production, you should include a proper icon file
          if (-not (Test-Path "windows\icon.ico")) {
            Write-Host "Creating placeholder icon..."
            # Download a generic icon or create one
            # For now, we'll modify the setup.iss to not require an icon
          }

      - name: Build installer with Inno Setup
        shell: pwsh
        run: |
          Write-Host "Building installer..."

          # NOTE: $version here only labels the installer (filename +
          # Add/Remove Programs AppVersion). The version the RUNTIME
          # advertises (discovery scan, /api/version) comes from the
          # repo-root VERSION file baked into the payload, NOT from the
          # tag. That file must be bumped by hand for every release -- see
          # webserver/version.py. (A CI-only injection here would not help
          # manual `git clone && ./install.sh` installs, which are common.)
          #
          # Set version from input or tag
          $version = "${{ github.event.inputs.version }}"
          if ([string]::IsNullOrEmpty($version)) {
            $version = "${{ github.ref_name }}".TrimStart('v')
            if ([string]::IsNullOrEmpty($version) -or $version -eq "${{ github.ref_name }}") {
              $version = "4.0"
            }
          }
          Write-Host "Building version: $version"

          # Create output directory
          New-Item -ItemType Directory -Force -Path "windows\output"

          # Run Inno Setup compiler
          & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
            /DMyAppVersion="$version" `
            /O"windows\output" `
            /F"OpenPLC_Runtime_$($version)_Setup" `
            "windows\setup.iss"

          if ($LASTEXITCODE -ne 0) {
            Write-Error "Inno Setup compilation failed with exit code $LASTEXITCODE"
            exit 1
          }

          Write-Host "Installer built successfully!"
          Get-ChildItem "windows\output"

      - name: Upload installer artifact
        uses: actions/upload-artifact@v4
        with:
          name: windows-installer
          path: windows/output/*.exe
          retention-days: 30

      - name: Create GitHub Release
        if: startsWith(github.ref, 'refs/tags/')
        uses: softprops/action-gh-release@v1
        with:
          files: windows/output/*.exe
          draft: false
          prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

name: Build Windows Installer
 
on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      version:
        description: 'Version number for the installer'
        required: false
        default: '4.0'
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  build-windows-installer:
    timeout-minutes: 30
    # Skip pre-release tags (those containing a hyphen, e.g. v4.1.0-rc.1)
    if: ${{ !contains(github.ref_name, '-') || github.event_name == 'workflow_dispatch' }}
    runs-on: windows-latest
    permissions:
      contents: write
 
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
 
      - name: Setup MSYS2
        uses: msys2/setup-msys2@v2
        with:
          msystem: MSYS
          update: true
          install: >-
            base-devel
            gcc
            make
            cmake
            pkg-config
            python
            python-pip
            python-setuptools
            git
            sqlite3
 
      - name: Cache MSYS2 packages
        uses: actions/cache@v4
        with:
          path: D:\a\_temp\msys64\var\cache\pacman\pkg
          key: msys2-packages-${{ runner.os }}-${{ hashFiles('.github/workflows/windows-installer.yml') }}
          restore-keys: |
            msys2-packages-${{ runner.os }}-
 
      - name: Install OpenPLC Runtime using install.sh
        shell: msys2 {0}
        run: |
          echo "=========================================="
          echo "Installing OpenPLC Runtime via install.sh"
          echo "=========================================="
 
          # Run the install script which handles all dependencies and build
          ./install.sh
 
          echo "Installation complete!"
 
      - name: Prepare installer payload
        shell: pwsh
        run: |
          Write-Host "Preparing installer payload..."
 
          # Create payload directory structure
          New-Item -ItemType Directory -Force -Path "windows\payload\msys64"
          New-Item -ItemType Directory -Force -Path "windows\payload\openplc-runtime"
 
          # Copy MSYS2 installation
          Write-Host "Copying MSYS2 installation (this may take a while)..."
          $msys2Path = "D:\a\_temp\msys64"
          if (Test-Path $msys2Path) {
            Copy-Item -Path "$msys2Path\*" -Destination "windows\payload\msys64" -Recurse -Force
          } else {
            Write-Host "MSYS2 path not found at $msys2Path, trying C:\msys64..."
            Copy-Item -Path "C:\msys64\*" -Destination "windows\payload\msys64" -Recurse -Force
          }
 
          # Copy OpenPLC Runtime (excluding unnecessary files)
          Write-Host "Copying OpenPLC Runtime..."
          $excludeDirs = @('.git', '.github', '.mypy_cache', '.ruff_cache', '.vscode', 'tests', '__pycache__')
 
          Get-ChildItem -Path "." -Exclude $excludeDirs | Where-Object {
            $_.Name -ne "windows" -and $_.Name -ne ".git"
          } | ForEach-Object {
            if ($_.PSIsContainer) {
              Copy-Item -Path $_.FullName -Destination "windows\payload\openplc-runtime\$($_.Name)" -Recurse -Force
            } else {
              Copy-Item -Path $_.FullName -Destination "windows\payload\openplc-runtime\" -Force
            }
          }
 
          # Clean up to reduce size
          Write-Host "Cleaning up payload to reduce size..."
 
          # Remove pacman cache
          $pacmanCache = "windows\payload\msys64\var\cache\pacman\pkg"
          if (Test-Path $pacmanCache) {
            Remove-Item -Path "$pacmanCache\*" -Force -Recurse -ErrorAction SilentlyContinue
          }
 
          # Remove unnecessary MSYS2 directories
          $msys2Cleanup = @(
            "windows\payload\msys64\var\log",
            "windows\payload\msys64\tmp"
          )
          foreach ($dir in $msys2Cleanup) {
            if (Test-Path $dir) {
              Remove-Item -Path "$dir\*" -Force -Recurse -ErrorAction SilentlyContinue
            }
          }
 
          # Calculate payload size
          $payloadSize = (Get-ChildItem -Path "windows\payload" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
          Write-Host "Payload size: $([math]::Round($payloadSize, 2)) MB"
 
      - name: Install Inno Setup
        shell: pwsh
        run: |
          Write-Host "Installing Inno Setup..."
          choco install innosetup -y --no-progress
 
          # Add Inno Setup to PATH
          $env:PATH = "C:\Program Files (x86)\Inno Setup 6;$env:PATH"
          [Environment]::SetEnvironmentVariable("PATH", $env:PATH, "Process")
 
      - name: Create placeholder icon
        shell: pwsh
        run: |
          # Create a simple placeholder icon if one doesn't exist
          # In production, you should include a proper icon file
          if (-not (Test-Path "windows\icon.ico")) {
            Write-Host "Creating placeholder icon..."
            # Download a generic icon or create one
            # For now, we'll modify the setup.iss to not require an icon
          }
 
      - name: Build installer with Inno Setup
        shell: pwsh
        run: |
          Write-Host "Building installer..."
 
          # NOTE: $version here only labels the installer (filename +
          # Add/Remove Programs AppVersion). The version the RUNTIME
          # advertises (discovery scan, /api/version) comes from the
          # repo-root VERSION file baked into the payload, NOT from the
          # tag. That file must be bumped by hand for every release -- see
          # webserver/version.py. (A CI-only injection here would not help
          # manual `git clone && ./install.sh` installs, which are common.)
          #
          # Set version from input or tag
          $version = "${{ github.event.inputs.version }}"
          if ([string]::IsNullOrEmpty($version)) {
            $version = "${{ github.ref_name }}".TrimStart('v')
            if ([string]::IsNullOrEmpty($version) -or $version -eq "${{ github.ref_name }}") {
              $version = "4.0"
            }
          }
          Write-Host "Building version: $version"
 
          # Create output directory
          New-Item -ItemType Directory -Force -Path "windows\output"
 
          # Run Inno Setup compiler
          & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
            /DMyAppVersion="$version" `
            /O"windows\output" `
            /F"OpenPLC_Runtime_$($version)_Setup" `
            "windows\setup.iss"
 
          if ($LASTEXITCODE -ne 0) {
            Write-Error "Inno Setup compilation failed with exit code $LASTEXITCODE"
            exit 1
          }
 
          Write-Host "Installer built successfully!"
          Get-ChildItem "windows\output"
 
      - name: Upload installer artifact
        uses: actions/upload-artifact@v4
        with:
          name: windows-installer
          path: windows/output/*.exe
          retention-days: 30
 
      - name: Create GitHub Release
        if: startsWith(github.ref, 'refs/tags/')
        uses: softprops/action-gh-release@v1
        with:
          files: windows/output/*.exe
          draft: false
          prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 

What changed

2 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.

This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow