Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 32 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ env:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
env:
QT_QPA_PLATFORM: offscreen
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
python-version: '3.12.14'
- name: Install Linux Qt runtime dependencies
run: |
sudo apt-get update
Expand All @@ -32,35 +32,41 @@ jobs:
libopengl0 \
libxkbcommon-x11-0 \
libxcb-cursor0
- name: Pin pip
run: python -m pip install --disable-pip-version-check "pip==26.2.1"
- name: Install dependencies
run: python -m pip install -r requirements.txt
run: python -m pip install -r requirements.txt -c constraints/ci-linux-py312.txt
- name: Audit
run: python scripts/test/audit.py

windows-test:
runs-on: windows-latest
runs-on: windows-2025
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
python-version: '3.12.10'
architecture: 'x64'
- name: Pin pip
run: python -m pip install --disable-pip-version-check "pip==26.2.1"
- name: Install application dependencies
run: python -m pip install -r requirements.txt
run: python -m pip install -r requirements.txt -c constraints/ci-windows-py312.txt
- name: Full regression audit
run: python scripts/test/audit.py

windows-build:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: windows-latest
runs-on: windows-2025
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
python-version: '3.12.10'
architecture: 'x64'
- name: Pin pip
run: python -m pip install --disable-pip-version-check "pip==26.2.1"
- name: Install application dependencies
run: python -m pip install -r requirements.txt
run: python -m pip install -r requirements.txt -c constraints/ci-windows-py312.txt
- name: Full regression audit
run: python scripts/test/audit.py
- name: Verify workflow version mapping
Expand All @@ -73,7 +79,7 @@ jobs:
- name: Build wheel
shell: pwsh
run: |
python -m pip install --upgrade setuptools wheel
python -m pip install --disable-pip-version-check -r constraints/ci-tools.txt
New-Item -ItemType Directory -Force -Path dist\wheel | Out-Null
python -m pip wheel . --no-deps --no-build-isolation -w dist\wheel
$wheel = (Get-ChildItem dist\wheel\*.whl | Select-Object -First 1).FullName
Expand All @@ -82,9 +88,9 @@ jobs:
shell: pwsh
run: |
python -m venv .p14-wheel-venv
.\.p14-wheel-venv\Scripts\python.exe -m pip install --upgrade pip
.\.p14-wheel-venv\Scripts\python.exe -m pip install --disable-pip-version-check "pip==26.2.1"
$wheel = (Get-ChildItem dist\wheel\*.whl | Select-Object -First 1).FullName
.\.p14-wheel-venv\Scripts\python.exe -m pip install $wheel
.\.p14-wheel-venv\Scripts\python.exe -m pip install -c constraints/ci-windows-py312.txt $wheel
- name: Native PySide6/keyring/resource smoke
shell: pwsh
run: |
Expand Down Expand Up @@ -178,9 +184,9 @@ jobs:
}

- name: Setup .NET for WiX
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '8.0.x'
dotnet-version: '8.0.424'

- name: Install WiX Toolset
shell: pwsh
Expand Down Expand Up @@ -265,7 +271,7 @@ jobs:
python scripts/test/p14_distribution_audit.py --release-dir dist\release --version $env:INVIO_VERSION

- name: Upload Windows distribution artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: invio-windows-distribution
path: dist/release/*
Expand All @@ -274,18 +280,18 @@ jobs:
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [test, windows-test, windows-build]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
python-version: '3.12.14'
- name: Verify release tag matches Invio version
run: python scripts/build/version_info.py --expect-tag "${GITHUB_REF_NAME}"
- name: Download Windows distribution artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: invio-windows-distribution
path: release
Expand Down
19 changes: 19 additions & 0 deletions constraints/ci-linux-py312.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Invio CI lock: Ubuntu 24.04 / CPython 3.12.14
# Frozen from successful main CI environment on 2026-08-27.
PySide6==6.11.2
PySide6_Addons==6.11.2
PySide6_Essentials==6.11.2
shiboken6==6.11.2
openpyxl==3.1.5
keyring==25.7.0
truststore==0.10.4
et-xmlfile==2.0.0
SecretStorage==3.5.0
jeepney==0.9.0
cryptography==50.0.1
cffi==2.1.1
pycparser==3.0
jaraco.classes==3.4.0
jaraco.context==6.1.2
jaraco.functools==4.6.0
more-itertools==11.1.0
6 changes: 6 additions & 0 deletions constraints/ci-tools.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Invio tag-build Python tooling lock.
# Versions are taken from the successful published v1.0.0.1.50.1 release build.
pip==26.2.1
setuptools==84.0.0
wheel==0.48.0
packaging==26.3
15 changes: 15 additions & 0 deletions constraints/ci-windows-py312.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Invio CI lock: Windows Server 2025 / CPython 3.12.10 x64
# Frozen from successful main CI environment on 2026-08-27.
PySide6==6.11.2
PySide6_Addons==6.11.2
PySide6_Essentials==6.11.2
shiboken6==6.11.2
openpyxl==3.1.5
keyring==25.7.0
truststore==0.10.4
pywin32-ctypes==0.2.3
et-xmlfile==2.0.0
jaraco.classes==3.4.0
jaraco.context==6.1.2
jaraco.functools==4.6.0
more-itertools==11.1.0
152 changes: 152 additions & 0 deletions tests/test_ci_reproducibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from __future__ import annotations

import re
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
WORKFLOW = ROOT / ".github" / "workflows" / "ci.yml"

EXPECTED_ACTION_PINS = {
"actions/checkout": "fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09",
"actions/setup-python": "ece7cb06caefa5fff74198d8649806c4678c61a1",
"actions/setup-dotnet": "26b0ec14cb23fa6904739307f278c14f94c95bf1",
"actions/upload-artifact": "b7c566a772e6b6bfb58ed0dc250532a479d7789f",
"actions/download-artifact": "37930b1c2abaa49bbe596cd826c3c89aef350131",
"Nuitka/Nuitka-Action": "99c9d3ab258c7008c0604617d925574101327e5d",
}

EXPECTED_LINUX_CONSTRAINTS = {
"PySide6==6.11.2",
"PySide6_Addons==6.11.2",
"PySide6_Essentials==6.11.2",
"shiboken6==6.11.2",
"openpyxl==3.1.5",
"keyring==25.7.0",
"truststore==0.10.4",
"et-xmlfile==2.0.0",
"SecretStorage==3.5.0",
"jeepney==0.9.0",
"cryptography==50.0.1",
"cffi==2.1.1",
"pycparser==3.0",
"jaraco.classes==3.4.0",
"jaraco.context==6.1.2",
"jaraco.functools==4.6.0",
"more-itertools==11.1.0",
}

EXPECTED_WINDOWS_CONSTRAINTS = {
"PySide6==6.11.2",
"PySide6_Addons==6.11.2",
"PySide6_Essentials==6.11.2",
"shiboken6==6.11.2",
"openpyxl==3.1.5",
"keyring==25.7.0",
"truststore==0.10.4",
"pywin32-ctypes==0.2.3",
"et-xmlfile==2.0.0",
"jaraco.classes==3.4.0",
"jaraco.context==6.1.2",
"jaraco.functools==4.6.0",
"more-itertools==11.1.0",
}

EXPECTED_TOOL_LOCK = {
"pip==26.2.1",
"setuptools==84.0.0",
"wheel==0.48.0",
"packaging==26.3",
}


def _requirements(path: Path) -> set[str]:
return {
line.strip()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}


class CiReproducibilityTests(unittest.TestCase):
def test_all_external_actions_are_immutable_sha_pinned(self):
workflow = WORKFLOW.read_text(encoding="utf-8")
uses_lines = [line.strip() for line in workflow.splitlines() if line.strip().startswith("- uses:")]
self.assertTrue(uses_lines)
pattern = re.compile(r"^- uses: ([^@\s]+)@([0-9a-f]{40})(?:\s+#\s+\S+)?$")
for line in uses_lines:
with self.subTest(line=line):
self.assertIsNotNone(pattern.fullmatch(line))
self.assertNotRegex(workflow, r"uses:\s+[^\s]+@(v\d+|main|master|latest)(?:\s|$)")

def test_expected_node24_action_revisions_are_pinned(self):
workflow = WORKFLOW.read_text(encoding="utf-8")
seen: dict[str, set[str]] = {}
for owner_repo, sha in re.findall(r"uses:\s+([^@\s]+)@([0-9a-f]{40})", workflow):
seen.setdefault(owner_repo, set()).add(sha)
self.assertEqual(set(seen), set(EXPECTED_ACTION_PINS))
for action, expected_sha in EXPECTED_ACTION_PINS.items():
with self.subTest(action=action):
self.assertEqual(seen[action], {expected_sha})

def test_runner_python_and_dotnet_versions_are_explicit(self):
workflow = WORKFLOW.read_text(encoding="utf-8")
self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 2)
self.assertEqual(workflow.count("runs-on: windows-2025"), 2)
self.assertEqual(workflow.count("python-version: '3.12.14'"), 2)
self.assertEqual(workflow.count("python-version: '3.12.10'"), 2)
self.assertIn("dotnet-version: '8.0.424'", workflow)
for floating in ("ubuntu-latest", "windows-latest", "python-version: '3.12'", "dotnet-version: '8.0.x'"):
with self.subTest(floating=floating):
self.assertNotIn(floating, workflow)

def test_platform_dependency_constraints_match_frozen_ci_environment(self):
self.assertEqual(_requirements(ROOT / "constraints" / "ci-linux-py312.txt"), EXPECTED_LINUX_CONSTRAINTS)
self.assertEqual(_requirements(ROOT / "constraints" / "ci-windows-py312.txt"), EXPECTED_WINDOWS_CONSTRAINTS)
self.assertEqual(_requirements(ROOT / "constraints" / "ci-tools.txt"), EXPECTED_TOOL_LOCK)

def test_workflow_uses_constraints_and_exact_build_tools_without_floating_upgrades(self):
workflow = WORKFLOW.read_text(encoding="utf-8")
self.assertIn("python -m pip install -r requirements.txt -c constraints/ci-linux-py312.txt", workflow)
self.assertEqual(
workflow.count("python -m pip install -r requirements.txt -c constraints/ci-windows-py312.txt"),
2,
)
self.assertIn("python -m pip install --disable-pip-version-check -r constraints/ci-tools.txt", workflow)
self.assertEqual(workflow.count('python -m pip install --disable-pip-version-check "pip==26.2.1"'), 3)
self.assertIn(
".\\.p14-wheel-venv\\Scripts\\python.exe -m pip install -c constraints/ci-windows-py312.txt $wheel",
workflow,
)
self.assertNotIn("pip install --upgrade setuptools wheel", workflow)
self.assertNotIn("pip install --upgrade pip", workflow)

def test_update_fix_jobs_remain_source_only_and_release_jobs_remain_tag_gated(self):
workflow = WORKFLOW.read_text(encoding="utf-8")
linux_test = workflow.split("\n test:\n", 1)[1].split("\n windows-test:\n", 1)[0]
windows_test = workflow.split("\n windows-test:\n", 1)[1].split("\n windows-build:\n", 1)[0]
windows_build = workflow.split("\n windows-build:\n", 1)[1].split("\n release:\n", 1)[0]
release = workflow.split("\n release:\n", 1)[1]
forbidden = (
"pip wheel",
"Nuitka",
"wix build",
"msiexec",
"prepare_windows_distribution.py",
"finalize_release_checksums.py",
"actions/upload-artifact",
"gh release",
)
for job_name, block in (("test", linux_test), ("windows-test", windows_test)):
with self.subTest(job=job_name):
self.assertIn("python scripts/test/audit.py", block)
for fragment in forbidden:
self.assertNotIn(fragment, block)
self.assertIn("if: startsWith(github.ref, 'refs/tags/v')", windows_build)
self.assertIn("if: startsWith(github.ref, 'refs/tags/v')", release)
self.assertIn("needs: [test, windows-test, windows-build]", release)


if __name__ == "__main__":
unittest.main()
6 changes: 3 additions & 3 deletions tests/test_p14_distribution_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ def test_github_workflow_builds_wheel_nuitka_onedir_wix_msi_and_tag_release(self
"$wixVersion = (wix --version).Trim()",
"$wixCoreVersion = ($wixVersion -split '\\+', 2)[0]",
"if ($wixCoreVersion -ne $env:WIX_VERSION)",
"actions/setup-dotnet@v4",
"dotnet-version: '8.0.x'",
"actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1",
"dotnet-version: '8.0.424'",
"Nuitka/Nuitka-Action@99c9d3ab258c7008c0604617d925574101327e5d",
"mode: standalone",
"enable-plugins: pyside6",
Expand All @@ -173,7 +173,7 @@ def test_github_workflow_builds_wheel_nuitka_onedir_wix_msi_and_tag_release(self
"INVIO_P14_COMPILED_TLS_SMOKE",
"wix build build\\Invio.wxs -arch x64 -pdbtype none",
"p14_distribution_audit.py",
"actions/upload-artifact@v4",
"actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f",
"startsWith(github.ref, 'refs/tags/v')",
"gh release create",
"gh release upload",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_repository_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def test_p14_candidate_records_are_truthful_and_packaging_contract_is_present(se
self.assertIn('"src.core.settings"', pyproject)
self.assertIn('"providers.packages.stripe"', pyproject)
self.assertIn('"assets.icons"', pyproject)
self.assertIn("windows-latest", workflow)
self.assertIn("runs-on: windows-2025", workflow)
self.assertIn("p14_windows_smoke.py", workflow)
self.assertIn("P14 CERTIFICATION PENDING", roadmap)
self.assertIn("Production-ready: NO", release)
Expand Down