Skip to content

Commit

Permalink
ci: automate Qt updates in CI (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
Neverous authored Oct 11, 2023
1 parent 3037ce2 commit 1f21d12
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 10 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/asset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ jobs:
fail-fast: false
matrix:
qt-version:
- 5.15.2 # OS LTS
- 6.2.4 # LTS
- 6.5.3 # latest

- 5.15.2 # Supported in Ubuntu Jammy Jellyfish until 2027-04-01
- 6.2.4 # Supported in Ubuntu Jammy Jellyfish until 2027-04-01
- 6.5.3 # Supported until 2024-03-31
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ jobs:
fail-fast: false
matrix:
qt-version:
- 5.15.2 # OS LTS
- 6.2.4 # LTS
- 6.5.3 # latest

- 5.15.2 # Supported in Ubuntu Jammy Jellyfish until 2027-04-01
- 6.2.4 # Supported in Ubuntu Jammy Jellyfish until 2027-04-01
- 6.5.3 # Supported until 2024-03-31
steps:
- name: Checkout source code
uses: actions/checkout@v4
Expand All @@ -45,7 +44,7 @@ jobs:
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: 'cpp'
languages: cpp
queries: security-and-quality
if: inputs.build-config == 'Debug'
continue-on-error: true
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/qt_update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Check for Qt updates

on:
workflow_dispatch:
schedule:
- cron: 0 8 * * 5

concurrency:
group: qt-update-${{ github.ref }}
cancel-in-progress: true

jobs:
check-qt-updates:
name: Check Qt updates
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_ACCESS_TOKEN }}

- name: Install python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: pip

- name: Install dependencies
run: python3 -m pip install -c requirements.txt .
working-directory: misc/qt-updater

- name: Run the updater
id: diff
run: |
python3 -m main ../../.github/workflows/*.yml
if git diff --exit-code; then
echo "changed=false" >> "${GITHUB_OUTPUT}"
else
echo "changed=true" >> "${GITHUB_OUTPUT}"
fi
working-directory: misc/qt-updater

- name: Create pull request
run: |
# Configure git user
git config user.email "EFIBootEditorBot@users.noreply.github.com"
git config user.name "EFIBootEditor (Bot)"
# Create branch with changes
git checkout -b qt-update
git commit -am "ci: update Qt versions in CI"
git push --force --set-upstream origin qt-update
gh pr create \
--title 'ci: update Qt versions in CI' \
--body 'Updates Qt versions in CI to newest ones.' \
--base master \
--head qt-update
env:
GITHUB_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
if: steps.diff.outputs.changed == 'true'
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ jobs:
matrix:
os: [windows-2022]
qt-version:
- 6.5.3 # latest
- 6.5.3 # Supported until 2024-03-31
compiler:
- MSVC
steps:
Expand Down
188 changes: 188 additions & 0 deletions misc/qt-updater/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Fetch latest qt versions and update CI config

import logging
import re
import sys
from dataclasses import dataclass
from datetime import date
from typing import Any

import requests
import ruamel.yaml


@dataclass
class Version:
major: int
minor: int
patch: int | None = None

def nopatch(self) -> "Version":
return Version(self.major, self.minor)

def __hash__(self) -> int:
return hash((self.major, self.minor, self.patch))

def __str__(self) -> str:
return f"{self.major}.{self.minor}" + (f".{self.patch}" if self.patch else "")

def __repr__(self) -> str:
return str(self)

def __lt__(self, obj: "Version") -> bool:
return (self.major, self.minor, self.patch or 0) < (
obj.major,
obj.minor,
obj.patch or 0,
)


@dataclass
class QtVersion:
version: Version
eol: date
source: str = ""

def __hash__(self) -> int:
return hash(self.version)

def comment(self) -> str:
return "Supported" + (" in " + self.source if self.source else "") + f" until {self.eol}"

def __str__(self) -> str:
return f"{self.version} ({self.comment()})"

def __repr__(self) -> str:
return str(self)

def __lt__(self, obj: "QtVersion") -> bool:
return self.version < obj.version


log = logging.getLogger("qt-update")
api = requests.session()


def fetch_eoldate_info(name: str) -> Any:
log.debug("Fetching %s info from endoflife.date", name)
return api.get(f"https://endoflife.date/api/{name}.json").json()


def fetch_ubuntu_package_versions(series_slug: str, name: str) -> set[Version]:
log.debug("Fetching %s packages info from Ubuntu %s", name, series_slug)
packages = api.get(
"https://api.launchpad.net/1.0/ubuntu/+archive/primary",
params={
"ws.op": "getPublishedBinaries",
"binary_name": name,
"exact_match": "true",
"distro_arch_series": f"https://api.launchpad.net/1.0/ubuntu/{series_slug}/amd64",
"pocket": "Release",
"status": "Published",
},
).json()["entries"]
return {Version(*map(int, package["binary_package_version"].split(".")[:2])) for package in packages}


def fetch_installable_qt_versions() -> set[Version]:
log.debug("Fetching installable Qt versions")
listing = api.get("https://download.qt.io/online/qtsdkrepository/linux_x64/desktop/").text
return {
Version(*map(int, filter(None, version))) for version in re.findall(r'href="qt(\d)_\1(\d{0,2})(\d+)/"', listing)
}


def get_supported_qt_versions(supported_date: date) -> tuple[dict[Version, QtVersion], set[Version]]:
log.debug("Getting supported Qt versions")
versions = {}
lts_releases = set()

for cycle in fetch_eoldate_info("qt"):
eol = date.fromisoformat(cycle["eol"])
version = Version(*map(int, cycle["cycle"].split(".")))
if cycle["lts"]:
log.debug("Adding %s to LTS versions", version)
lts_releases.add(version)

if eol < supported_date:
continue

log.info("Adding %s to supported versions (until %s)", version, eol)
versions[version] = QtVersion(version, eol, "")

return versions, lts_releases


def find_used_qt_versions(supported_date: date) -> set[QtVersion]:
log.debug("Finding used Qt versions")

versions, lts_releases = get_supported_qt_versions(supported_date)

# Check for latest versions in supported Ubuntu LTS
for cycle in fetch_eoldate_info("ubuntu"):
eol = date.fromisoformat(cycle["eol"])
if eol < supported_date:
continue

series = cycle["codename"]
log.debug("Checking Qt packages in Ubuntu %s (supported until %s)", series, eol)

series_slug = cycle["codename"].split()[0].lower()
for package in ("qtbase5-dev", "qt6-base-dev"):
for version in fetch_ubuntu_package_versions(series_slug, package):
if version not in lts_releases:
log.debug("Skipping non-LTS version: %s (from Ubuntu %s)", version, series)
continue

log.info("Adding %s to supported versions (from Ubuntu %s)", version, series)
if version not in versions or versions[version].eol < eol:
versions[version] = QtVersion(version, eol, f"Ubuntu {series}")

return set(versions.values())


if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)

log.info("Compiling target Qt versions")
now = date.today()
latest = {}
for version in fetch_installable_qt_versions():
minor = version.nopatch()
if latest.get(minor, Version(0, 0)) < version:
latest[minor] = version

target_versions = sorted(find_used_qt_versions(now))
for qt_version in target_versions:
qt_version.version = latest[qt_version.version]

log.info("Compiled target Qt versions: %s", target_versions)
log.info("Processing input workflow files...")

yaml = ruamel.yaml.YAML()
yaml.width = 1024
yaml.indent(mapping=2, sequence=4, offset=2)
for file in sys.argv[1:]:
log.info("Processing %s file", file)
with open(file, "rb") as fr:
workflow = yaml.load(fr)

for key, job in workflow.get("jobs", {}).items():
if not job.get("strategy", {}).get("matrix", {}).get("qt-version"):
continue

job_versions = job["strategy"]["matrix"]["qt-version"]
# Only set latest version for winget-update job
if key == "winget-update":
target = target_versions[-1]
job_versions[0] = str(target.version)
job_versions.yaml_add_eol_comment(target.comment(), 0)
continue

job_versions.clear()
for t, target in enumerate(target_versions):
job_versions.append(str(target.version))
job_versions.yaml_add_eol_comment(target.comment(), t)

with open(file, "wb") as fw:
yaml.dump(workflow, fw)
50 changes: 50 additions & 0 deletions misc/qt-updater/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[project]
name = "qt-updater"
version = "1.0.0"
description = ""

requires-python = ">=3.11"

dependencies = [
"requests",
"ruamel.yaml",
]

[project.optional-dependencies]

dev = [
"black",
"mypy",
"pip-tools",
"ruff",
"types-requests",
]

[tool.ruff]
select = [
"B",
"C",
"E",
"F",
"I",
"W",
]

line-length = 120

[tool.black]
line-length = 120

[tool.mypy]
strict = true
check_untyped_defs = true
disallow_any_generics = true
strict_optional = true
warn_no_return = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true

plugins = []
Loading

0 comments on commit 1f21d12

Please sign in to comment.