Skip to content

Automated Windows .exe Compilation with GitHub Actions CI/CD #36

@AliiiBenn

Description

@AliiiBenn

Automated Windows .exe Compilation with GitHub Actions

Problem Description

Currently, the GUI requires users to:

  1. Install Python 3.10+
  2. Install dependencies (customtkinter, pillow, etc.)
  3. Run wareflow-gui command

This creates a barrier to adoption for non-technical users who simply want to download and run an executable.

Proposed Solution

Implement automated compilation of Windows .exe using GitHub Actions that:

  1. Compiles on every push to main/feature branches
  2. Stores executable as artifact for download
  3. Creates GitHub releases with .exe attachment on version tags
  4. Generates checksums for integrity verification
  5. Optimizes file size through UPX compression
  6. Tests the executable in CI environment before release

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                     GitHub Repository                        │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  Push / Pull Request / Tag                                   │
│       │                                                       │
│       ▼                                                       │
│  ┌──────────────────────────────────────────────┐           │
│  │  .github/workflows/build-exe.yml            │           │
│  │  - Triggered on push to main/*              │           │
│  │  - Triggered on pull requests               │           │
│  │  - Triggered on version tags (v*)           │           │
│  └──────────────────────────────────────────────┘           │
│       │                                                       │
│       ▼                                                       │
│  ┌──────────────────────────────────────────────┐           │
│  │  Windows Runner (ubuntu-latest or windows)  │           │
│  │  1. Setup Python 3.10+                      │           │
│  │  2. Install dependencies                    │           │
│  │  3. Run PyInstaller                         │           │
│  │  4. Test executable                          │           │
│  │  5. Generate checksums                      │           │
│  │  6. Upload artifacts                        │           │
│  └──────────────────────────────────────────────┘           │
│       │                                                       │
│       ├─────────────────┬─────────────────┐                  │
│       ▼                 ▼                 ▼                  │
│  ┌──────────┐     ┌──────────┐     ┌──────────┐             │
│  │ Artifact │     │ Release  │     │  Check   │             │
│  │ (PR/Push)│     │ (Tags)   │     │  Sums    │             │
│  └──────────┘     └──────────┘     └──────────┘             │
│                                                               │
└─────────────────────────────────────────────────────────────┘

Implementation Plan

Phase 1: PyInstaller Configuration (1 day)

Tasks:

  1. Create build/gui.spec file

    • One-file executable configuration
    • Include all data files and assets
    • Configure hidden imports
    • Set up UPX compression
    • Disable console (windowed mode)
  2. Create build/__init__.py for build utilities

  3. Create build/icon.ico application icon

    • Use a warehouse/box icon
    • Multiple sizes for Windows
  4. Create build/ directory structure

Deliverables:

  • Complete PyInstaller spec file
  • Application icon
  • Build documentation

Phase 2: GitHub Actions Workflow (1 day)

Tasks:

  1. Create .github/workflows/build-exe.yml workflow

  2. Job: Build Windows Executable

    • Trigger: Push to main, feature/*, develop
    • OS: windows-latest or ubuntu-latest (with Wine)
    • Steps:
      - Checkout code
      - Setup Python 3.10
      - Install dependencies
      - Install PyInstaller
      - Build .exe with PyInstaller
      - Run smoke tests on .exe
      - Generate SHA256 checksums
      - Upload artifact (wareflow-gui-win.exe)
  3. Job: Create Release (on tags only)

    • Trigger: Version tags (v*..)
    • Steps:
      - Download artifacts
      - Create GitHub release
      - Upload .exe to release
      - Upload checksums
      - Generate release notes

Deliverables:

  • Complete GitHub Actions workflow
  • Automated artifact generation
  • Automated release creation

Phase 3: Build Testing (0.5 day)

Tasks:

  1. Create smoke tests for compiled .exe

    • Test application launches
    • Test basic navigation
    • Test project loading
    • Test database connection
  2. Add tests to CI workflow

    • Run .exe with --help flag
    • Run .exe with test project
    • Validate no crashes on startup
  3. Create tests/integration/test_exeSmokeTests.py

Deliverables:

  • Smoke test suite
  • CI integration
  • Test documentation

Phase 4: Build Optimization (0.5 day)

Tasks:

  1. Optimize PyInstaller spec

    • Exclude unnecessary dependencies
    • Use UPX compression
    • Optimize imports
  2. Reduce file size

    • Target: < 100 MB compressed
    • Test different PyInstaller options
  3. Improve startup time

    • Lazy load modules
    • Optimize imports
    • Profile and benchmark

Deliverables:

  • Optimized build configuration
  • Size/performance benchmarks

Phase 5: Documentation (0.5 day)

Tasks:

  1. Update README with download instructions
  2. Create docs/BUILD.md for manual builds
  3. Create docs/INSTALL.md for end users
  4. Add GitHub releases template
  5. Document artifact locations

Deliverables:

  • Complete user documentation
  • Developer build documentation
  • Release process documentation

Detailed GitHub Actions Workflow

File Structure

.github/
└── workflows/
    ├── build-exe.yml        # Main build workflow
    ├── release.yml          # Release creation workflow
    └── test-exe.yml         # Smoke test workflow

build/
├── gui.spec                 # PyInstaller configuration
├── icon.ico                 # Application icon
└── metadata/                # Version info, etc.

Workflow: build-exe.yml

name: Build Windows Executable

on:
  push:
    branches: [ main, develop, 'feature/**' ]
  pull_request:
    branches: [ main, develop ]
  workflow_dispatch:

jobs:
  build:
    runs-on: windows-latest

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

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pyinstaller
          pip install -e .

      - name: Build executable
        run: |
          pyinstaller build/gui.spec --clean --noconfirm

      - name: Run smoke tests
        run: |
          $exePath = "dist\Wareflow-GUI.exe"
          Start-Process -FilePath $exePath -ArgumentList "--help" -Wait

      - name: Generate checksums
        run: |
          Get-FileHash dist\Wareflow-GUI.exe -Algorithm SHA256 | Select-Object -ExpandProperty Hash > dist\Wareflow-GUI.exe.sha256

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: wareflow-gui-windows
          path: |
            dist/Warehouse-GUI.exe
            dist/Warehouse-GUI.exe.sha256
          retention-days: 30

      - name: Upload build info
        uses: actions/upload-artifact@v4
        with:
          name: build-info
          path: |
            dist/*.txt
          retention-days: 30

Workflow: release.yml (on tags)

name: Create Release

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  release:
    runs-on: ubuntu-latest

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

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: wareflow-gui-windows
          path: ./artifacts

      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            artifacts/Warehouse-GUI.exe
            artifacts/Warehouse-GUI.exe.sha256
          generate_release_notes: true
          draft: false
          prerelease: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

PyInstaller Configuration

gui.spec

# -*- mode: python ; coding: utf-8 -*-

import sys
from PyInstaller.utils.hooks import collect_data_files, collect_submodules

block_cipher = None

a = Analysis(
    ['src/wareflow_analysis/gui/__main__.py'],
    pathex=[],
    binaries=[],
    datas=[
        # Source code
        ('src/wareflow_analysis', 'wareflow_analysis'),

        # Templates
        ('src/wareflow_analysis/templates', 'wareflow_analysis/templates'),
    ],
    hiddenimports=[
        'customtkinter',
        'tkinter',
        '_tkinter',
        'pandas',
        'pandas._libs.tslibs.base',
        'pandas._libs.tslibs.dtypes',
        'pandas._libs.tslibs.np_datetime',
        'pandas._libs.tslibs.nattype',
        'openpyxl',
        'openpyxl.cell._writer',
        'yaml',
        'sqlite3',
        'PIL',
        'PIL._tkinter_finder',
        'darkdetect',
        'excel_to_sql',
    ],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[
        # Exclude test modules
        'pytest',
        'tests',
        'unittest',

        # Exclude development tools
        'ruff',
        'black',
        'mypy',

        # Exclude unnecessary stdlib
        'email',
        'html',
        'http',
        'urllib3',
    ],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

# Remove duplicate files
for src in a.datas:
    if '__pycache__' in src[0]:
        a.datas.remove(src)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='Warehouse-GUI',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,  # Windowed mode (no console)
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='build/icon.ico',
    version_file='build/version_info.txt',
)

version_info.txt

VSVersionInfo(
  ffi=FixedFileInfo(
    filevers=(0, 6, 0, 0),
    prodvers=(0, 6, 0, 0),
    mask=0x3f,
    flags=0x0,
    OS=0x40004,
    fileType=0x1,
    subtype=0x0,
    date=(0, 0)
  ),
  kids=[
    StringFileInfo(
      [
        StringTable(
          u'040904B0',
          [
            StringStruct(u'CompanyName', u'Wareflow'),
            StringStruct(u'FileDescription', u'Warehouse Data Analysis GUI'),
            StringStruct(u'FileVersion', u'0.6.0.0'),
            StringStruct(u'InternalName', u'Warehouse-GUI'),
            StringStruct(u'LegalCopyright', u'MIT License'),
            StringStruct(u'OriginalFilename', u'Warehouse-GUI.exe'),
            StringStruct(u'ProductName', u'Wareflow Analysis'),
            StringStruct(u'ProductVersion', u'0.6.0.0')
          ]
        )
      ]
    ),
    VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
  ]
)

Smoke Tests

tests/integration/test_exeSmokeTests.py

"""Smoke tests for compiled executable."""

import subprocess
import time
from pathlib import Path


class TestExeSmokeTests:
    """Basic smoke tests for the compiled .exe."""

    @staticmethod
    def get_exe_path() -> Path:
        """Get path to compiled executable."""
        # Check dist directory
        exe_paths = [
            Path("dist/Warehouse-GUI.exe"),
            Path("artifacts/Warehouse-GUI.exe"),
        ]

        for path in exe_paths:
            if path.exists():
                return path

        raise FileNotFoundError("Executable not found")

    def test_exe_exists(self):
        """Test that executable was built."""
        exe_path = self.get_exe_path()
        assert exe_path.exists()
        assert exe_path.stat().st_size > 1_000_000  # At least 1 MB

    def test_exe_runs(self):
        """Test that executable launches without crashing."""
        exe_path = self.get_exe_path()

        # Run with timeout
        result = subprocess.run(
            [str(exe_path)],
            timeout=5,
            capture_output=True
        )

        # Should either run successfully or show window
        # Exit code 0 or 1 is acceptable (user closed window)
        assert result.returncode in [0, 1]

    def test_exe_version(self):
        """Test executable version info."""
        exe_path = self.get_exe_path()

        # Try to read version info
        result = subprocess.run(
            ["powershell", "(Get-Item '').VersionInfo"],
            capture_output=True
        )

        # Should have version info
        assert result.returncode == 0

    def test_checksum_exists(self):
        """Test that checksum file exists and is valid."""
        exe_path = self.get_exe_path()
        checksum_path = exe_path.with_suffix('.exe.sha256')

        assert checksum_path.exists()

        content = checksum_path.read_text()
        assert len(content) == 64  # SHA256 hash length

    def test_exe_imports_work(self):
        """Test that all imports work in frozen environment."""
        # This would be run inside the exe with special test mode
        pass

Build Optimization

Exclusion Strategy

excludes=[
    # Testing
    'pytest', 'unittest', 'mock', 'coverage',

    # Development
    'ruff', 'black', 'mypy', 'flake8', 'pylint',

    # Unused stdlib
    'email', 'smtplib', 'email.mime',
    'html', 'html.parser',
    'http', 'http.server', 'urllib3', 'requests',
    'xml', 'xmlrpc',

    # Database servers
    'psycopg2', 'pymysql', 'cx_oracle',
    'redis', 'pymongo',
]

Optimization Goals

Metric Target Strategy
File Size < 100 MB UPX compression, excludes
Startup Time < 3 sec Lazy imports, optimization
Memory Usage < 200 MB idle Cleanup, proper garbage collection
First Render < 2 sec Optimized imports

Release Process

Automated Release Flow

1. Developer merges feature to main
   ↓
2. Version bump in pyproject.toml (0.6.0 → 0.7.0)
   ↓
3. Create git tag: git tag v0.7.0
   ↓
4. Push tag: git push origin v0.7.0
   ↓
5. GitHub Actions triggers
   ↓
6. Builds .exe (10-15 minutes)
   ↓
7. Runs smoke tests
   ↓
8. Creates GitHub Release
   ↓
9. Uploads artifacts:
   - Warehouse-GUI.exe
   - Warehouse-GUI.exe.sha256
   - Release notes
   ↓
10. Users download from Releases page

Version Bumping Strategy

# Update version in pyproject.toml
version = "0.7.0"

# Commit version bump
git add pyproject.toml
git commit -m "chore: bump version to 0.7.0"

# Create and push tag
git tag v0.7.0
git push origin main --tags

Success Criteria

Functionality

  • .exe launches without Python installed
  • All GUI features work in compiled version
  • No console window appears
  • Application icon displays correctly
  • Version info shows in file properties

Performance

  • File size < 100 MB
  • Startup time < 3 seconds
  • Memory usage < 200 MB idle
  • No memory leaks detected

CI/CD

  • Build completes successfully on every push
  • Artifacts uploaded and downloadable
  • Releases created automatically on tags
  • Checksums generated correctly
  • Smoke tests pass in CI

Documentation

  • User can download and run .exe without issues
  • Documentation updated with download instructions
  • Developer build documentation complete
  • Release process documented

Risks & Mitigation

Risk Impact Mitigation
Large file size Medium UPX compression, exclude unnecessary deps
Build fails on CI High Test builds locally first, use stable Python version
False positive antivirus Medium Code signing certificate (future), reputation building
Missing dependencies High Comprehensive hidden imports list, thorough testing
Windows-only Low Document clearly, offer Mac/Linux builds later
Slow builds Low Cache dependencies, parallel jobs if needed

Future Enhancements

  1. Code Signing - Purchase certificate to sign .exe
  2. MacOS Build - Create .dmg for macOS
  3. Linux Build - Create .AppImage for Linux
  4. Auto-update - Implement in-app update checking
  5. Installer - Create NSIS installer with shortcuts
  6. Portable Version - ZIP version without installation
  7. Smaller Size - Try PyInstaller alternatives (Nuitka, cx_Freeze)
  8. Faster Builds - Use caching, build matrix

Timeline Estimation

Total Effort: 3.5 days

  • Phase 1: PyInstaller Config (1 day)
  • Phase 2: GitHub Actions (1 day)
  • Phase 3: Build Testing (0.5 day)
  • Phase 4: Optimization (0.5 day)
  • Phase 5: Documentation (0.5 day)

Dependencies:

  • PyInstaller expertise
  • GitHub Actions knowledge
  • Windows machine for testing
  • Icon design assets

Priority: Medium-High

  • User value: High (easier adoption)
  • Technical risk: Low (proven tools)
  • Effort: Moderate

Issue ID: 003
Created: 2025-01-26
Last Updated: 2025-01-26
Status: Open - Planning
Assignee: TBD
Labels: ci-cd, build, windows, enhancement
Milestone: v0.7.0 or v1.0.0

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions