Skip to content

Commit 5628328

Browse files
committed
ci: automatizar build+release Windows (PyInstaller+Inno)
1 parent 597a7cb commit 5628328

8 files changed

Lines changed: 316 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI (Windows)
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
test:
13+
runs-on: windows-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Setup Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.13"
21+
22+
- name: Install deps
23+
shell: pwsh
24+
run: |
25+
python -m pip install -U pip wheel setuptools
26+
python -m pip install -r requirements.txt
27+
28+
- name: Pytest
29+
shell: pwsh
30+
run: |
31+
python -m pytest -q
32+

.github/workflows/release.yml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
name: Release (Windows)
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: "Version X.Y.Z (sin 'v')"
11+
required: true
12+
13+
permissions:
14+
contents: write
15+
16+
jobs:
17+
build_release:
18+
runs-on: windows-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.13"
26+
27+
- name: Resolve version
28+
id: ver
29+
shell: pwsh
30+
run: |
31+
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
32+
$v = "${{ inputs.version }}".Trim()
33+
} else {
34+
$v = "${{ github.ref_name }}".Trim()
35+
if ($v.StartsWith("v")) { $v = $v.Substring(1) }
36+
}
37+
echo "version=$v" >> $env:GITHUB_OUTPUT
38+
Write-Host "Version=$v"
39+
40+
- name: Apply version to files (CI-only)
41+
shell: pwsh
42+
run: |
43+
python scripts/ci/apply_version.py "${{ steps.ver.outputs.version }}"
44+
45+
- name: Install deps
46+
shell: pwsh
47+
run: |
48+
python -m pip install -U pip wheel setuptools
49+
python -m pip install -r requirements.txt
50+
51+
- name: Pytest
52+
shell: pwsh
53+
run: |
54+
python -m pytest -q
55+
56+
- name: Build (PyInstaller)
57+
shell: pwsh
58+
run: |
59+
python -m PyInstaller --clean --noconfirm Yagua.spec
60+
61+
- name: Install Inno Setup
62+
shell: pwsh
63+
run: |
64+
choco install innosetup -y
65+
66+
- name: Build installer (Inno Setup)
67+
shell: pwsh
68+
run: |
69+
iscc Yagua.iss
70+
71+
- name: Package portable zip + hashes
72+
shell: pwsh
73+
run: |
74+
$ver = "${{ steps.ver.outputs.version }}"
75+
$setup = "installer\\Yagua_Setup_$ver.exe"
76+
if (-not (Test-Path $setup)) { throw "No se encontro instalador: $setup" }
77+
78+
$portable = "installer\\Yagua_Portable_$ver.zip"
79+
if (Test-Path $portable) { Remove-Item -Force $portable }
80+
Compress-Archive -Path "dist\\Yagua\\*" -DestinationPath $portable
81+
82+
$h1 = (Get-FileHash $setup -Algorithm SHA256).Hash.ToLower()
83+
$h2 = (Get-FileHash $portable -Algorithm SHA256).Hash.ToLower()
84+
85+
"$h1 $(Split-Path -Leaf $setup)" | Out-File -Encoding ascii "$setup.sha256"
86+
"$h2 $(Split-Path -Leaf $portable)" | Out-File -Encoding ascii "$portable.sha256"
87+
88+
python scripts/ci/make_latest_json.py $ver (Split-Path -Leaf $setup) $h1 (Split-Path -Leaf $portable) $h2 | Out-File -Encoding utf8 "installer\\latest.json"
89+
90+
- name: Release notes
91+
shell: pwsh
92+
run: |
93+
python scripts/ci/extract_release_notes.py "${{ steps.ver.outputs.version }}" | Out-File -Encoding utf8 "release_notes.md"
94+
95+
- name: Create GitHub Release
96+
uses: softprops/action-gh-release@v2
97+
with:
98+
body_path: release_notes.md
99+
files: |
100+
installer/Yagua_Setup_${{ steps.ver.outputs.version }}.exe
101+
installer/Yagua_Setup_${{ steps.ver.outputs.version }}.exe.sha256
102+
installer/Yagua_Portable_${{ steps.ver.outputs.version }}.zip
103+
installer/Yagua_Portable_${{ steps.ver.outputs.version }}.zip.sha256
104+
installer/latest.json
105+

app/ui/frames/settings/tabs/updates_tab.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from app.ui import colors, fonts
66
from app.translations import t
7+
from app.version import __version__
78

89

910
class UpdatesTab(ctk.CTkFrame):
@@ -35,7 +36,7 @@ def _build(self):
3536

3637
self._lbl_version = ctk.CTkLabel(
3738
panel,
38-
text=f"{t('current_version')}: v2.0.0",
39+
text=f"{t('current_version')}: v{__version__}",
3940
font=fonts.FUENTE_CHICA,
4041
text_color=colors.TEXT_GRAY,
4142
anchor='w'

app/version.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Single source of truth for the app version.
2+
3+
CI updates this file from the git tag (e.g. v2.0.1) during release builds.
4+
"""
5+
6+
__version__ = "2.0.0"

scripts/build_windows.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
param(
2+
[string]$Version = ""
3+
)
4+
5+
$ErrorActionPreference = "Stop"
6+
7+
if (-not (Test-Path ".\\.venv\\Scripts\\python.exe")) {
8+
throw "No se encontro el venv. Crea uno con: python -m venv .venv"
9+
}
10+
11+
if ($Version -ne "") {
12+
.\\.venv\\Scripts\\python.exe scripts\\ci\\apply_version.py $Version
13+
}
14+
15+
.\\.venv\\Scripts\\python.exe -m pip install -U pip wheel setuptools
16+
.\\.venv\\Scripts\\python.exe -m pip install -r requirements.txt
17+
18+
.\\.venv\\Scripts\\python.exe -m pytest -q
19+
.\\.venv\\Scripts\\python.exe -m PyInstaller --clean --noconfirm Yagua.spec
20+
21+
Write-Host "PyInstaller OK. Ahora corre Inno Setup manualmente si lo tenes instalado:"
22+
Write-Host " iscc Yagua.iss"
23+

scripts/ci/apply_version.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import sys
5+
from pathlib import Path
6+
7+
8+
_VER_RE = re.compile(r"^\d+\.\d+\.\d+$")
9+
10+
11+
def _replace_line(text: str, pattern: re.Pattern[str], replacement: str) -> str:
12+
new_text, n = pattern.subn(replacement, text, count=1)
13+
if n != 1:
14+
raise RuntimeError(f"No se pudo actualizar: patron={pattern.pattern!r}")
15+
return new_text
16+
17+
18+
def _apply_app_version_py(repo_root: Path, version: str) -> None:
19+
path = repo_root / "app" / "version.py"
20+
text = path.read_text(encoding="utf-8")
21+
text = _replace_line(
22+
text,
23+
re.compile(r'^__version__\s*=\s*".*?"\s*$', re.MULTILINE),
24+
f'__version__ = "{version}"',
25+
)
26+
path.write_text(text, encoding="utf-8")
27+
28+
29+
def _apply_inno_iss(repo_root: Path, version: str) -> None:
30+
path = repo_root / "Yagua.iss"
31+
text = path.read_text(encoding="utf-8")
32+
text = _replace_line(
33+
text,
34+
re.compile(r"^AppVersion=.*$", re.MULTILINE),
35+
f"AppVersion={version}",
36+
)
37+
text = _replace_line(
38+
text,
39+
re.compile(r"^AppVerName=.*$", re.MULTILINE),
40+
f"AppVerName=Yagua {version}",
41+
)
42+
text = _replace_line(
43+
text,
44+
re.compile(r"^OutputBaseFilename=.*$", re.MULTILINE),
45+
f"OutputBaseFilename=Yagua_Setup_{version}",
46+
)
47+
path.write_text(text, encoding="utf-8")
48+
49+
50+
def main(argv: list[str]) -> int:
51+
if len(argv) != 2:
52+
print("Uso: python scripts/ci/apply_version.py <X.Y.Z>", file=sys.stderr)
53+
return 2
54+
55+
version = argv[1].strip()
56+
if not _VER_RE.match(version):
57+
print(f"Version invalida: {version!r} (esperado X.Y.Z)", file=sys.stderr)
58+
return 2
59+
60+
repo_root = Path(__file__).resolve().parents[2]
61+
_apply_app_version_py(repo_root, version)
62+
_apply_inno_iss(repo_root, version)
63+
print(f"OK: version aplicada = {version}")
64+
return 0
65+
66+
67+
if __name__ == "__main__":
68+
raise SystemExit(main(sys.argv))
69+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import sys
5+
from pathlib import Path
6+
7+
8+
def _extract_section(md: str, header: str) -> str | None:
9+
# header is like: [Unreleased] or [2.0.0]
10+
# Accept: ## [X] or ## [X] - DATE
11+
pattern = re.compile(
12+
rf"^##\s+\[{re.escape(header)}\].*$\n(?P<body>.*?)(?=^##\s+\[|\Z)",
13+
re.MULTILINE | re.DOTALL,
14+
)
15+
m = pattern.search(md)
16+
if not m:
17+
return None
18+
return m.group("body").strip()
19+
20+
21+
def main(argv: list[str]) -> int:
22+
if len(argv) != 2:
23+
print("Uso: python scripts/ci/extract_release_notes.py <X.Y.Z>", file=sys.stderr)
24+
return 2
25+
26+
version = argv[1].strip()
27+
repo_root = Path(__file__).resolve().parents[2]
28+
changelog = (repo_root / "CHANGELOG.md").read_text(encoding="utf-8")
29+
30+
body = _extract_section(changelog, version)
31+
source = version
32+
if not body:
33+
body = _extract_section(changelog, "Unreleased") or ""
34+
source = "Unreleased"
35+
36+
out = []
37+
out.append(f"## Yagua v{version}")
38+
out.append("")
39+
if body:
40+
out.append(body)
41+
out.append("")
42+
out.append(f"_Notas generadas desde CHANGELOG.md ({source})._")
43+
sys.stdout.write("\n".join(out).strip() + "\n")
44+
return 0
45+
46+
47+
if __name__ == "__main__":
48+
raise SystemExit(main(sys.argv))
49+

scripts/ci/make_latest_json.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import sys
5+
from pathlib import Path
6+
7+
8+
def main(argv: list[str]) -> int:
9+
if len(argv) != 6:
10+
print(
11+
"Uso: python scripts/ci/make_latest_json.py <version> <setup_name> <setup_sha256> <portable_name> <portable_sha256>",
12+
file=sys.stderr,
13+
)
14+
return 2
15+
16+
version, setup_name, setup_sha, portable_name, portable_sha = argv[1:]
17+
payload = {
18+
"version": version,
19+
"assets": [
20+
{"name": setup_name, "sha256": setup_sha},
21+
{"name": portable_name, "sha256": portable_sha},
22+
],
23+
}
24+
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
25+
return 0
26+
27+
28+
if __name__ == "__main__":
29+
raise SystemExit(main(sys.argv))
30+

0 commit comments

Comments
 (0)