build(deps): bump idna from 3.11 to 3.15 #33
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Validate | |
| on: | |
| pull_request: | |
| branches: ["**"] | |
| paths: | |
| - '.github/workflows/release.yml' | |
| - '.github/workflows/release-validate.yml' | |
| - 'app.spec' | |
| - 'requirements*.txt' | |
| - 'requirements*.in' | |
| - 'src/services/update_*.py' | |
| - 'src/utils/install_context.py' | |
| - 'src/main.py' | |
| - 'src/cli.py' | |
| - 'src/resources/icons/app-icon.png' | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| APP_NAME: HistorySync | |
| APP_ID: historysync # lowercase, used for .deb / AppImage / desktop entry | |
| PYTHON_STANDALONE_VERSION: '3.12.12' | |
| PYTHON_STANDALONE_RELEASE_TAG: '20251202' | |
| PYTHON_BUILD_VERSION: '3.12' | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| # Extracts a clean tag (vX.Y.Z) and bare number (X.Y.Z) from the git ref and | |
| # exposes them as outputs for every downstream job. | |
| # Also emits a matrix JSON: PR events run only the default-validation platforms; | |
| # push/dispatch runs the full matrix. | |
| setup: | |
| name: Parse Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version_tag: ${{ steps.ver.outputs.tag }} | |
| version_num: ${{ steps.ver.outputs.num }} | |
| matrix: ${{ steps.matrix.outputs.matrix }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Resolve version tag and bare number | |
| id: ver | |
| shell: bash | |
| run: | | |
| if [[ "$GITHUB_REF" == refs/tags/v* ]]; then | |
| TAG="$GITHUB_REF_NAME" | |
| else | |
| # pull_request / branch push / workflow_dispatch: synthesise a dev version | |
| TAG="v0.0.0-dev.$(git rev-parse --short HEAD)" | |
| fi | |
| NUM="${TAG#v}" | |
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "num=${NUM}" >> "$GITHUB_OUTPUT" | |
| echo "::notice title=Version::Building ${TAG} (${NUM})" | |
| - name: Build platform matrix | |
| id: matrix | |
| shell: bash | |
| run: | | |
| FULL='{"include":[{"platform_name":"windows-x64","os_image":"windows-latest","python_arch":"x86_64-pc-windows-msvc","arch_display":"x64","validate_default":true},{"platform_name":"windows-arm64","os_image":"windows-11-arm","python_arch":"aarch64-pc-windows-msvc","arch_display":"arm64","validate_default":false},{"platform_name":"linux-x64","os_image":"ubuntu-22.04","python_arch":"x86_64-unknown-linux-gnu","arch_display":"x86_64","deb_arch":"amd64","rpm_arch":"x86_64","validate_default":true},{"platform_name":"linux-arm64","os_image":"ubuntu-24.04-arm","python_arch":"aarch64-unknown-linux-gnu","arch_display":"arm64","deb_arch":"arm64","rpm_arch":"aarch64","validate_default":false},{"platform_name":"macos-arm64","os_image":"macos-15","python_arch":"aarch64-apple-darwin","arch_display":"arm64","validate_default":true},{"platform_name":"macos-x64","os_image":"macos-15-intel","python_arch":"x86_64-apple-darwin","arch_display":"x64","validate_default":false}]}' | |
| PR_ONLY='{"include":[{"platform_name":"windows-x64","os_image":"windows-latest","python_arch":"x86_64-pc-windows-msvc","arch_display":"x64","validate_default":true},{"platform_name":"linux-x64","os_image":"ubuntu-22.04","python_arch":"x86_64-unknown-linux-gnu","arch_display":"x86_64","deb_arch":"amd64","rpm_arch":"x86_64","validate_default":true},{"platform_name":"macos-arm64","os_image":"macos-15","python_arch":"aarch64-apple-darwin","arch_display":"arm64","validate_default":true}]}' | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| echo "matrix=${PR_ONLY}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "matrix=${FULL}" >> "$GITHUB_OUTPUT" | |
| fi | |
| # All build jobs depend on this passing; a red test suite blocks the release. | |
| test: | |
| name: Run Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_BUILD_VERSION }} | |
| cache: pip | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements-test.in | |
| # Run with the same flags as test.yml so the release gate has identical | |
| # rigour to the CI gate. Diverging flags risk a test passing here but | |
| # failing (or hiding failures) in the regular workflow. | |
| - name: Run pytest with coverage | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: | | |
| pytest \ | |
| --cov=src \ | |
| --cov-report=term-missing \ | |
| --cov-report=xml:coverage.xml \ | |
| -q --tb=short | |
| # Matrix (PR): windows-x64, linux-x64, macos-arm64 (validate_default platforms only) | |
| # Matrix (push/dispatch): all six platforms | |
| # The setup job emits the correct JSON for each event type. | |
| build: | |
| name: Build · ${{ matrix.platform_name }} | |
| needs: [setup, test] | |
| runs-on: ${{ matrix.os_image }} | |
| env: | |
| VERSION_TAG: ${{ needs.setup.outputs.version_tag }} | |
| VERSION_NUM: ${{ needs.setup.outputs.version_num }} | |
| HAS_WIN_CERT: ${{ secrets.WINDOWS_CERTIFICATE != '' }} | |
| HAS_APPLE_CERT: ${{ secrets.APPLE_CERTIFICATE != '' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.setup.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_BUILD_VERSION }} | |
| cache: pip | |
| - name: Install build dependencies (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install pyinstaller | |
| if (Test-Path requirements.in) { pip install -r requirements.in } | |
| - name: Install build dependencies (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install pyinstaller | |
| if [ -f requirements.in ]; then pip install -r requirements.in; fi | |
| # On Linux / macOS PyInstaller bundles a complete runtime on its own. | |
| # On Windows we replace the bundled vcruntime + Python DLLs with the | |
| # known-good indygreg standalone build to avoid MSVC dependency issues. | |
| - name: Restore standalone Python runtime cache (Windows) | |
| if: runner.os == 'Windows' | |
| id: cache-python-runtime | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: python_minimal | |
| key: python-standalone-${{ env.PYTHON_STANDALONE_VERSION }}-${{ env.PYTHON_STANDALONE_RELEASE_TAG }}-${{ matrix.python_arch }} | |
| - name: Download standalone Python runtime (Windows) | |
| if: runner.os == 'Windows' && steps.cache-python-runtime.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: | | |
| BASE="https://github.com/indygreg/python-build-standalone/releases/download" | |
| TAG="${{ env.PYTHON_STANDALONE_RELEASE_TAG }}" | |
| VER="${{ env.PYTHON_STANDALONE_VERSION }}" | |
| ARCH="${{ matrix.python_arch }}" | |
| URL="${BASE}/${TAG}/cpython-${VER}+${TAG}-${ARCH}-install_only_stripped.tar.gz" | |
| echo "Fetching: ${URL}" | |
| curl -fsSL "$URL" -o runtime.tar.gz | |
| tar -xzf runtime.tar.gz && mv python python_minimal | |
| - name: Save standalone Python runtime cache (Windows) | |
| if: runner.os == 'Windows' && steps.cache-python-runtime.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: python_minimal | |
| key: python-standalone-${{ env.PYTHON_STANDALONE_VERSION }}-${{ env.PYTHON_STANDALONE_RELEASE_TAG }}-${{ matrix.python_arch }} | |
| # app.spec produces two bundles in one pass: | |
| # dist/HistorySync/ -full GUI bundle (for all installers) | |
| # dist/hsync/ -standalone CLI bundle (for headless tar.gz) | |
| # After the build we copy the CLI binary into the GUI bundle so every | |
| # installer ships both HistorySync (GUI) and hsync (CLI). | |
| - name: Build with PyInstaller | |
| shell: bash | |
| run: pyinstaller app.spec --noconfirm | |
| # All downstream packaging steps operate on dist/HistorySync/ only. | |
| # Copying hsync[.exe] there means installers get both binaries for free. | |
| - name: Embed CLI binary into GUI bundle (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| Copy-Item "dist\hsync\hsync.exe" "dist\${{ env.APP_NAME }}\hsync.exe" -Force | |
| Write-Host "Embedded: hsync.exe ->dist\${{ env.APP_NAME }}\hsync.exe" | |
| - name: Embed CLI binary into GUI bundle (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| cp "dist/hsync/hsync" "dist/${{ env.APP_NAME }}/hsync" | |
| chmod +x "dist/${{ env.APP_NAME }}/hsync" | |
| echo "Embedded: hsync ->dist/${{ env.APP_NAME }}/hsync" | |
| - name: Embed CLI binary into GUI bundle (macOS) | |
| if: runner.os == 'macOS' | |
| shell: bash | |
| run: | | |
| cp "dist/hsync/hsync" "dist/${{ env.APP_NAME }}.app/Contents/MacOS/hsync" | |
| chmod +x "dist/${{ env.APP_NAME }}.app/Contents/MacOS/hsync" | |
| echo "Embedded: hsync ->dist/${{ env.APP_NAME }}.app/Contents/MacOS/hsync" | |
| # Covers both the GUI bundle (dist/HistorySync/) and the standalone | |
| # CLI bundle (dist/hsync/) so neither leaks .po files into a release. | |
| - name: Strip .po / .pot files (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| foreach ($dir in @("dist\${{ env.APP_NAME }}", "dist\hsync")) { | |
| if (Test-Path $dir) { | |
| Get-ChildItem -Path $dir -Recurse -Include "*.po","*.pot" | Remove-Item -Force | |
| if (Get-ChildItem -Path $dir -Recurse -Include "*.po","*.pot") { | |
| Write-Error "Error: residual .po/.pot files remain in $dir" | |
| } | |
| } | |
| } | |
| - name: Strip .po / .pot files (Unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| for dir in "dist/${{ env.APP_NAME }}" "dist/hsync"; do | |
| [ -d "$dir" ] || continue | |
| find "$dir" -type f \( -name "*.po" -o -name "*.pot" \) -delete | |
| if find "$dir" -type f \( -name "*.po" -o -name "*.pot" \) | grep -q .; then | |
| echo "Error: residual .po/.pot files remain in $dir" && exit 1 | |
| fi | |
| done | |
| # These steps are no-ops until the corresponding secrets are populated. | |
| # | |
| # Windows: add repository secrets | |
| # WINDOWS_CERTIFICATE -base64-encoded PFX file | |
| # WINDOWS_CERTIFICATE_PASSWORD -PFX passphrase | |
| # | |
| # macOS: add repository secrets | |
| # APPLE_CERTIFICATE -base64-encoded .p12 Developer ID cert | |
| # APPLE_CERTIFICATE_PASSWORD -.p12 passphrase | |
| # APPLE_ID -Apple ID email used for notarization | |
| # APPLE_ID_PASSWORD -app-specific password (not your Apple ID password) | |
| # APPLE_TEAM_ID -10-character Apple Team ID | |
| # IMPORTANT: this step must run BEFORE code signing so that the final | |
| # DLLs (python312.dll, vcruntime140.dll, etc.) are the ones that get | |
| # signed. Signing first and then overwriting the DLLs would invalidate | |
| # the signature on those files. | |
| # Swap out the Python interpreter and vcruntime DLLs that PyInstaller | |
| # pulled from the build machine with the clean standalone build we | |
| # downloaded earlier -ensures a consistent, minimal runtime footprint. | |
| - name: Replace Python runtime (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $src = "python_minimal" | |
| $dlls = @("python.exe","pythonw.exe","python3.dll","python312.dll", | |
| "vcruntime140.dll","vcruntime140_1.dll") | |
| # Replace in both the GUI bundle and the standalone CLI bundle | |
| foreach ($dist in @("dist\${{ env.APP_NAME }}", "dist\hsync")) { | |
| if (-not (Test-Path $dist)) { continue } | |
| foreach ($f in $dlls) { | |
| $s = Join-Path $src $f | |
| if (Test-Path $s) { | |
| Copy-Item $s $dist -Force | |
| Write-Host "Replaced in ${dist}: $f" | |
| } | |
| } | |
| } | |
| - name: Sign executable (Windows) | |
| if: runner.os == 'Windows' && env.HAS_WIN_CERT == 'true' | |
| shell: pwsh | |
| env: | |
| CERT_B64: ${{ secrets.WINDOWS_CERTIFICATE }} | |
| CERT_PWD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} | |
| run: | | |
| $certBytes = [System.Convert]::FromBase64String($env:CERT_B64) | |
| [System.IO.File]::WriteAllBytes("cert.pfx", $certBytes) | |
| $signtoolCandidates = @( | |
| "C:\Program Files (x86)\Windows Kits\10\bin\x64\signtool.exe", | |
| "C:\Program Files (x86)\Windows Kits\10\bin\arm64\signtool.exe", | |
| "C:\Program Files\Windows Kits\10\bin\arm64\signtool.exe", | |
| "C:\Program Files\Windows Kits\10\bin\x64\signtool.exe" | |
| ) | |
| $signtool = $null | |
| foreach ($candidate in $signtoolCandidates) { | |
| if (Test-Path $candidate) { | |
| $signtool = $candidate | |
| break | |
| } | |
| } | |
| if (-not $signtool) { | |
| $signtool = (Get-Command signtool.exe -ErrorAction SilentlyContinue)?.Source | |
| } | |
| if (-not $signtool) { throw "signtool.exe not found. Install Windows SDK." } | |
| # Sign both the GUI launcher and the CLI binary | |
| foreach ($target in @( | |
| "dist\${{ env.APP_NAME }}\${{ env.APP_NAME }}.exe", | |
| "dist\${{ env.APP_NAME }}\hsync.exe" | |
| )) { | |
| & $signtool sign /fd sha256 /td sha256 /tr http://timestamp.digicert.com ` | |
| /f cert.pfx /p $env:CERT_PWD "$target" | |
| if ($LASTEXITCODE -ne 0) { throw "signtool failed for $target" } | |
| Write-Host "::notice::Signed: $target" | |
| } | |
| Remove-Item cert.pfx -Force | |
| - name: Sign and notarize app bundle (macOS) | |
| if: runner.os == 'macOS' && env.HAS_APPLE_CERT == 'true' | |
| shell: bash | |
| env: | |
| APPLE_CERT: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERT_PWD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_ID_PWD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| echo "$APPLE_CERT" | base64 --decode > cert.p12 | |
| security create-keychain -p "" build.keychain | |
| security import cert.p12 -k build.keychain -P "$APPLE_CERT_PWD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain | |
| security list-keychains -d user -s build.keychain | |
| # Derive the signing identity CN from the imported certificate. | |
| # APPLE_TEAM_ID is the 10-character Team ID, not the certificate Common | |
| # Name that codesign --sign expects. Using Team ID directly causes | |
| # codesign to either fail or pick the wrong identity. | |
| SIGNING_IDENTITY=$(security find-identity -v -p codesigning build.keychain \ | |
| | grep "Developer ID Application" \ | |
| | head -1 \ | |
| | sed 's/.*"\(.*\)"/\1/') | |
| if [ -z "$SIGNING_IDENTITY" ]; then | |
| echo "::error::No 'Developer ID Application' certificate found in build.keychain." | |
| exit 1 | |
| fi | |
| echo "::notice::Signing with identity: ${SIGNING_IDENTITY}" | |
| codesign --deep --force --verify --verbose \ | |
| --sign "$SIGNING_IDENTITY" \ | |
| --options runtime \ | |
| "dist/${{ env.APP_NAME }}.app" | |
| ditto -c -k --keepParent "dist/${{ env.APP_NAME }}.app" _notarize.zip | |
| xcrun notarytool submit _notarize.zip \ | |
| --apple-id "$APPLE_ID" --password "$APPLE_ID_PWD" \ | |
| --team-id "$APPLE_TEAM_ID" --wait | |
| xcrun stapler staple "dist/${{ env.APP_NAME }}.app" | |
| rm cert.p12 _notarize.zip | |
| echo "::notice::App bundle signed and notarized." | |
| - name: Package portable ZIP (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| # Drop a marker so the app auto-enables portable mode on launch | |
| New-Item -Path "dist\${{ env.APP_NAME }}\.portable" -ItemType File -Force | Out-Null | |
| $out = "${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-portable.zip" | |
| Push-Location "dist\${{ env.APP_NAME }}" | |
| Compress-Archive -Path * -DestinationPath "..\..\$out" | |
| Pop-Location | |
| # Remove marker so the installer build uses the default (non-portable) paths | |
| Remove-Item "dist\${{ env.APP_NAME }}\.portable" -Force | |
| Write-Host "Created: $out ($('{0:N2}' -f ((Get-Item $out).length / 1MB)) MB)" | |
| - name: Generate Inno Setup script | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $appName = "${{ env.APP_NAME }}" | |
| $ver = "${{ env.VERSION_NUM }}" | |
| $repo = "${{ github.repository }}" | |
| $outName = "${appName}-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-setup" | |
| $archMode = if ("${{ matrix.arch_display }}" -eq "arm64") { "arm64" } else { "x64compatible" } | |
| $archAllowed = if ("${{ matrix.arch_display }}" -eq "arm64") { "arm64" } else { "x64compatible and not arm64" } | |
| @( | |
| "[Setup]", | |
| "AppName=${appName}", | |
| "AppVersion=${ver}", | |
| "AppPublisher=${appName} Project", | |
| "AppPublisherURL=https://github.com/${repo}", | |
| "AppSupportURL=https://github.com/${repo}/issues", | |
| "DefaultDirName={autopf}\${appName}", | |
| "DefaultGroupName=${appName}", | |
| "AllowNoIcons=yes", | |
| "OutputDir=.", | |
| "OutputBaseFilename=${outName}", | |
| "Compression=lzma2/ultra64", | |
| "SolidCompression=yes", | |
| "PrivilegesRequired=lowest", | |
| "ArchitecturesAllowed=${archAllowed}", | |
| "ArchitecturesInstallIn64BitMode=${archMode}", | |
| "WizardStyle=modern", | |
| "DisableWelcomePage=yes", | |
| "LicenseFile=LICENSE", | |
| "UninstallDisplayIcon={app}\${appName}.exe", | |
| "ChangesEnvironment=yes", | |
| "", | |
| "[Languages]", | |
| 'Name: "english"; MessagesFile: "compiler:Default.isl"', | |
| "", | |
| "[Tasks]", | |
| 'Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked', | |
| 'Name: "addtopath"; Description: "Add hsync to the system PATH"; GroupDescription: "Command-line interface:"; Flags: checkedonce', | |
| "", | |
| "[Files]", | |
| "Source: ""dist\${appName}\*""; DestDir: ""{app}""; Flags: ignoreversion recursesubdirs createallsubdirs", | |
| "", | |
| "[Icons]", | |
| "Name: ""{group}\${appName}""; Filename: ""{app}\${appName}.exe""", | |
| "Name: ""{group}\{cm:UninstallProgram,${appName}}""; Filename: ""{uninstallexe}""", | |
| "Name: ""{userdesktop}\${appName}""; Filename: ""{app}\${appName}.exe""; Tasks: desktopicon", | |
| "", | |
| "[Registry]", | |
| "Root: HKCU; Subkey: ""Environment""; ValueType: expandsz; ValueName: ""Path""; ValueData: ""{olddata};{app}""; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('{app}'))", | |
| "", | |
| "[Run]", | |
| "Filename: ""{app}\${appName}.exe""; Description: ""{cm:LaunchProgram,${appName}}""; Flags: nowait postinstall skipifsilent", | |
| "", | |
| "[Code]", | |
| "function NeedsAddPath(Param: string): boolean;", | |
| "var", | |
| " OrigPath: string;", | |
| "begin", | |
| " if not RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) then begin", | |
| " Result := True;", | |
| " exit;", | |
| " end;", | |
| " Result := Pos(Uppercase(Param), Uppercase(OrigPath)) = 0;", | |
| "end;" | |
| ) -join "`n" | Set-Content installer.iss -Encoding UTF8 | |
| - name: Compile Inno Setup installer | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer.iss | |
| if ($LASTEXITCODE -ne 0) { throw "Inno Setup compilation failed." } | |
| $exe = "${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-setup.exe" | |
| Write-Host "Created: $exe ($('{0:N2}' -f ((Get-Item $exe).length / 1MB)) MB)" | |
| - name: Package hsync portable ZIP (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $out = "hsync-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}.zip" | |
| Push-Location "dist\hsync" | |
| Compress-Archive -Path * -DestinationPath "..\..\$out" | |
| Pop-Location | |
| Write-Host "Created: $out ($('{0:N2}' -f ((Get-Item $out).length / 1MB)) MB)" | |
| - name: Generate hsync Inno Setup script (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ver = "${{ env.VERSION_NUM }}" | |
| $repo = "${{ github.repository }}" | |
| $outName = "hsync-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-setup" | |
| $archMode = if ("${{ matrix.arch_display }}" -eq "arm64") { "arm64" } else { "x64compatible" } | |
| $archAllowed = if ("${{ matrix.arch_display }}" -eq "arm64") { "arm64" } else { "x64compatible and not arm64" } | |
| @( | |
| "[Setup]", | |
| "AppName=hsync", | |
| "AppVersion=${ver}", | |
| "AppPublisher=${{ env.APP_NAME }} Project", | |
| "AppPublisherURL=https://github.com/${repo}", | |
| "AppSupportURL=https://github.com/${repo}/issues", | |
| "DefaultDirName={autopf}\hsync", | |
| "DefaultGroupName=hsync", | |
| "AllowNoIcons=yes", | |
| "OutputDir=.", | |
| "OutputBaseFilename=${outName}", | |
| "Compression=lzma2/ultra64", | |
| "SolidCompression=yes", | |
| "PrivilegesRequired=lowest", | |
| "ArchitecturesAllowed=${archAllowed}", | |
| "ArchitecturesInstallIn64BitMode=${archMode}", | |
| "WizardStyle=modern", | |
| "DisableWelcomePage=yes", | |
| "LicenseFile=LICENSE", | |
| "UninstallDisplayIcon={app}\hsync.exe", | |
| "ChangesEnvironment=yes", | |
| "", | |
| "[Languages]", | |
| 'Name: "english"; MessagesFile: "compiler:Default.isl"', | |
| "", | |
| "[Tasks]", | |
| 'Name: "addtopath"; Description: "Add hsync to the system PATH"; GroupDescription: "Command-line interface:"; Flags: checkedonce', | |
| "", | |
| "[Files]", | |
| 'Source: "dist\hsync\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs', | |
| "", | |
| "[Registry]", | |
| 'Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant(''{app}''))', | |
| "", | |
| "[Code]", | |
| "function NeedsAddPath(Param: string): boolean;", | |
| "var", | |
| " OrigPath: string;", | |
| "begin", | |
| " if not RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) then begin", | |
| " Result := True;", | |
| " exit;", | |
| " end;", | |
| " Result := Pos(Uppercase(Param), Uppercase(OrigPath)) = 0;", | |
| "end;" | |
| ) -join "`n" | Set-Content hsync-installer.iss -Encoding UTF8 | |
| - name: Compile hsync Inno Setup installer (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" hsync-installer.iss | |
| if ($LASTEXITCODE -ne 0) { throw "Inno Setup compilation failed for hsync." } | |
| $exe = "hsync-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-setup.exe" | |
| Write-Host "Created: $exe ($('{0:N2}' -f ((Get-Item $exe).length / 1MB)) MB)" | |
| - name: Build standalone CLI tar.gz (macOS) | |
| if: runner.os == 'macOS' | |
| shell: bash | |
| run: | | |
| CLI_OUT="hsync-${{ env.VERSION_TAG }}-macos-${{ matrix.arch_display }}.tar.gz" | |
| tar -czf "$CLI_OUT" -C dist/ hsync | |
| echo "Created: $CLI_OUT ($(du -sh "$CLI_OUT" | cut -f1))" | |
| - name: Package DMG (macOS) | |
| if: runner.os == 'macOS' | |
| shell: bash | |
| run: | | |
| APP_BUNDLE="dist/${{ env.APP_NAME }}.app" | |
| OUT="${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-macos-${{ matrix.arch_display }}.dmg" | |
| # Wait for any open file handles to clear before calling hdiutil | |
| MAX=30; WAITED=0 | |
| while lsof +D "$APP_BUNDLE" >/dev/null 2>&1 && [ $WAITED -lt $MAX ]; do | |
| echo "Waiting for file handles (${WAITED}/${MAX}s)..." && sleep 2 | |
| WAITED=$((WAITED+2)) | |
| done | |
| if [ $WAITED -ge $MAX ]; then | |
| echo "Timed out waiting for file handles:" && lsof +D "$APP_BUNDLE" && exit 1 | |
| fi | |
| hdiutil create \ | |
| -volname "${{ env.APP_NAME }}" \ | |
| -srcfolder "$APP_BUNDLE" \ | |
| -ov -format UDZO \ | |
| "$OUT" | |
| echo "Created: $OUT" | |
| - name: Install Linux packaging tools | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update -qq | |
| if apt-cache show libfuse2t64 >/dev/null 2>&1; then | |
| FUSE_PKG="libfuse2t64" | |
| else | |
| FUSE_PKG="libfuse2" | |
| fi | |
| sudo apt-get install -y "${FUSE_PKG}" fakeroot dpkg-dev rpm | |
| - name: Build AppImage (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| DIST="dist/${{ env.APP_NAME }}" | |
| APPDIR="$RUNNER_TEMP/AppDir" | |
| OUT="${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-linux-${{ matrix.arch_display }}.AppImage" | |
| case "${{ matrix.arch_display }}" in | |
| x86_64) APPIMAGETOOL_ARCH="x86_64" ;; | |
| arm64) APPIMAGETOOL_ARCH="aarch64" ;; | |
| *) echo "::error::Unknown arch: ${{ matrix.arch_display }}" && exit 1 ;; | |
| esac | |
| # Download appimagetool | |
| curl -fsSL -o "$RUNNER_TEMP/appimagetool" \ | |
| "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${APPIMAGETOOL_ARCH}.AppImage" | |
| chmod +x "$RUNNER_TEMP/appimagetool" | |
| # All PyInstaller output lives under usr/lib/historysync; the AppRun | |
| # launcher script at the root delegates to the real binary there. | |
| mkdir -p "$APPDIR/usr/lib/${{ env.APP_ID }}" | |
| cp -r "$DIST"/. "$APPDIR/usr/lib/${{ env.APP_ID }}/" | |
| # AppRun -executes the bundled binary; PyInstaller's bootloader | |
| # handles LD_LIBRARY_PATH internally, so no export needed here. | |
| { | |
| echo '#!/bin/bash' | |
| echo 'SELF="$(readlink -f "$0")"' | |
| echo 'HERE="${SELF%/*}"' | |
| echo 'if [[ -z "${QT_QPA_PLATFORM:-}" ]]; then' | |
| echo ' if [[ -n "${DISPLAY:-}" ]]; then' | |
| echo ' export QT_QPA_PLATFORM=xcb' | |
| echo ' elif [[ -n "${WAYLAND_DISPLAY:-}" ]]; then' | |
| echo ' export QT_QPA_PLATFORM=wayland' | |
| echo ' fi' | |
| echo 'fi' | |
| echo 'exec "${HERE}/usr/lib/historysync/HistorySync" "$@"' | |
| } > "$APPDIR/AppRun" | |
| chmod +x "$APPDIR/AppRun" | |
| # Desktop integration | |
| { | |
| echo '[Desktop Entry]' | |
| echo 'Name=HistorySync' | |
| echo 'Exec=HistorySync' | |
| echo 'Icon=historysync' | |
| echo 'Type=Application' | |
| echo 'Categories=Utility;' | |
| } > "$APPDIR/${{ env.APP_ID }}.desktop" | |
| # Icon (256x256 PNG preferred by appimagetool) | |
| if [ -f "src/resources/icons/app-icon.png" ]; then | |
| cp "src/resources/icons/app-icon.png" "$APPDIR/${{ env.APP_ID }}.png" | |
| else | |
| # Minimal transparent 1x1 PNG fallback | |
| echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" | base64 -d > "$APPDIR/${{ env.APP_ID }}.png" | |
| fi | |
| # APPIMAGE_EXTRACT_AND_RUN=1: run appimagetool without mounting via FUSE | |
| # (belt-and-suspenders alongside the libfuse2 install above) | |
| ARCH="${APPIMAGETOOL_ARCH}" APPIMAGE_EXTRACT_AND_RUN=1 \ | |
| "$RUNNER_TEMP/appimagetool" "$APPDIR" "$OUT" | |
| echo "Created: $OUT" | |
| - name: Build .deb package (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| NUM="${{ env.VERSION_NUM }}" | |
| DIST="dist/${{ env.APP_NAME }}" | |
| case "${{ matrix.arch_display }}" in | |
| x86_64) DEB_ARCH="amd64" ;; | |
| arm64) DEB_ARCH="arm64" ;; | |
| *) echo "::error::Unknown arch: ${{ matrix.arch_display }}" && exit 1 ;; | |
| esac | |
| DEB="${{ env.APP_ID }}_${NUM}_${DEB_ARCH}" # Debian naming convention: lowercase, underscores | |
| # Directory tree expected by dpkg-deb | |
| mkdir -p "$DEB/DEBIAN" | |
| mkdir -p "$DEB/usr/lib/${{ env.APP_ID }}" | |
| mkdir -p "$DEB/usr/bin" | |
| mkdir -p "$DEB/usr/share/applications" | |
| mkdir -p "$DEB/usr/share/icons/hicolor/256x256/apps" | |
| mkdir -p "$DEB/usr/share/doc/${{ env.APP_ID }}" | |
| # Application files | |
| cp -r "$DIST"/. "$DEB/usr/lib/${{ env.APP_ID }}/" | |
| # License (Debian Policy §12.5: must be installed as copyright) | |
| [ -f LICENSE ] && cp LICENSE "$DEB/usr/share/doc/${{ env.APP_ID }}/copyright" | |
| # usr/bin shims: expose both the GUI launcher and the CLI on $PATH | |
| ln -s "/usr/lib/${{ env.APP_ID }}/${{ env.APP_NAME }}" \ | |
| "$DEB/usr/bin/${{ env.APP_ID }}" | |
| ln -s "/usr/lib/${{ env.APP_ID }}/hsync" \ | |
| "$DEB/usr/bin/hsync" | |
| # Desktop entry | |
| { | |
| echo '[Desktop Entry]' | |
| echo 'Name=${{ env.APP_NAME }}' | |
| echo 'Comment=Browser history synchronization tool' | |
| echo 'Exec=/usr/lib/${{ env.APP_ID }}/${{ env.APP_NAME }}' | |
| echo 'Icon=${{ env.APP_ID }}' | |
| echo 'Type=Application' | |
| echo 'Categories=Utility;' | |
| echo 'StartupNotify=true' | |
| } > "$DEB/usr/share/applications/${{ env.APP_ID }}.desktop" | |
| # Icon | |
| [ -f "src/resources/icons/app-icon.png" ] && \ | |
| cp "src/resources/icons/app-icon.png" \ | |
| "$DEB/usr/share/icons/hicolor/256x256/apps/${{ env.APP_ID }}.png" | |
| # DEBIAN/control -Installed-Size is in KiB per policy | |
| INSTALLED_KB=$(du -sk "$DEB/usr" | cut -f1) | |
| { | |
| echo 'Package: ${{ env.APP_ID }}' | |
| echo "Version: ${NUM}" | |
| echo "Architecture: ${DEB_ARCH}" | |
| echo 'Maintainer: ${{ env.APP_NAME }} Project <noreply@github.com>' | |
| echo "Installed-Size: ${INSTALLED_KB}" | |
| echo 'Depends: libgl1, libxcb-cursor0, libxcb-icccm4, libxcb-util1, libxcb-image0, libxcb-keysyms1, libxcb-render-util0, libxcb-shape0, libxcb-xkb1, libxkbcommon-x11-0' | |
| echo 'Homepage: https://github.com/${{ github.repository }}' | |
| echo 'Description: Browser History Sync' | |
| echo ' A cross-platform browser history synchronization and search tool.' | |
| echo ' Includes the hsync CLI for headless / scripted use.' | |
| } > "$DEB/DEBIAN/control" | |
| # maintainer scripts: refresh desktop database on install/remove | |
| printf '#!/bin/sh\nupdate-desktop-database /usr/share/applications || true\n' \ | |
| > "$DEB/DEBIAN/postinst" | |
| printf '#!/bin/sh\nupdate-desktop-database /usr/share/applications || true\n' \ | |
| > "$DEB/DEBIAN/postrm" | |
| chmod 0755 "$DEB/DEBIAN/postinst" "$DEB/DEBIAN/postrm" | |
| fakeroot dpkg-deb --build "$DEB" \ | |
| "${{ env.APP_ID }}_${NUM}_${DEB_ARCH}.deb" | |
| echo "Created: ${{ env.APP_ID }}_${NUM}_${DEB_ARCH}.deb" | |
| - name: Build .rpm package (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| NUM="${{ env.VERSION_NUM }}" | |
| DIST="dist/${{ env.APP_NAME }}" | |
| RPM_VERSION="${NUM}" | |
| RPM_RELEASE="1" | |
| if [[ "${NUM}" =~ ^([0-9]+(\.[0-9]+)*)([-+].+)$ ]]; then | |
| RPM_VERSION="${BASH_REMATCH[1]}~${BASH_REMATCH[3]#[-+]}" | |
| fi | |
| RPM_VERSION="${RPM_VERSION//-/.}" | |
| RPM_VERSION="${RPM_VERSION//[^A-Za-z0-9._~+]/.}" | |
| RPM_RELEASE="${RPM_RELEASE//[^A-Za-z0-9._~+]/.}" | |
| case "${{ matrix.arch_display }}" in | |
| x86_64) RPM_ARCH="x86_64" ;; | |
| arm64) RPM_ARCH="aarch64" ;; | |
| *) echo "::error::Unknown arch: ${{ matrix.arch_display }}" && exit 1 ;; | |
| esac | |
| RPMBUILD="$RUNNER_TEMP/rpmbuild" | |
| BUILDROOT="${RPMBUILD}/BUILDROOT/${{ env.APP_ID }}-${NUM}-1.${RPM_ARCH}" | |
| mkdir -p "${RPMBUILD}"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} | |
| mkdir -p "${BUILDROOT}/usr/lib/${{ env.APP_ID }}" | |
| mkdir -p "${BUILDROOT}/usr/bin" | |
| mkdir -p "${BUILDROOT}/usr/share/applications" | |
| mkdir -p "${BUILDROOT}/usr/share/icons/hicolor/256x256/apps" | |
| cp -r "$DIST"/. "${BUILDROOT}/usr/lib/${{ env.APP_ID }}/" | |
| ln -s "/usr/lib/${{ env.APP_ID }}/${{ env.APP_NAME }}" "${BUILDROOT}/usr/bin/${{ env.APP_ID }}" | |
| ln -s "/usr/lib/${{ env.APP_ID }}/hsync" "${BUILDROOT}/usr/bin/hsync" | |
| { | |
| echo '[Desktop Entry]' | |
| echo 'Name=${{ env.APP_NAME }}' | |
| echo 'Comment=Browser history synchronization tool' | |
| echo 'Exec=/usr/lib/${{ env.APP_ID }}/${{ env.APP_NAME }}' | |
| echo 'Icon=${{ env.APP_ID }}' | |
| echo 'Type=Application' | |
| echo 'Categories=Utility;' | |
| echo 'StartupNotify=true' | |
| } > "${BUILDROOT}/usr/share/applications/${{ env.APP_ID }}.desktop" | |
| if [ -f "src/resources/icons/app-icon.png" ]; then | |
| cp "src/resources/icons/app-icon.png" \ | |
| "${BUILDROOT}/usr/share/icons/hicolor/256x256/apps/${{ env.APP_ID }}.png" | |
| else | |
| echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" \ | |
| | base64 -d > "${BUILDROOT}/usr/share/icons/hicolor/256x256/apps/${{ env.APP_ID }}.png" | |
| fi | |
| { | |
| echo "Name: ${{ env.APP_ID }}" | |
| echo "Version: ${RPM_VERSION}" | |
| echo "Release: ${RPM_RELEASE}%{?dist}" | |
| echo "Summary: Browser History Sync" | |
| echo "License: Apache-2.0" | |
| echo "URL: https://github.com/${{ github.repository }}" | |
| echo "BuildArch: ${RPM_ARCH}" | |
| echo | |
| echo "Requires: mesa-libGL" | |
| echo "Requires: xcb-util-cursor" | |
| echo "Requires: xcb-util-wm" | |
| echo "Requires: xcb-util-image" | |
| echo "Requires: xcb-util-keysyms" | |
| echo "Requires: xcb-util-renderutil" | |
| echo "Requires: libxkbcommon-x11" | |
| echo | |
| echo "%description" | |
| echo "A cross-platform browser history synchronization and search tool." | |
| echo "Includes the hsync CLI for headless / scripted use." | |
| echo | |
| echo "%files" | |
| echo "/usr/lib/${{ env.APP_ID }}/" | |
| echo "/usr/bin/${{ env.APP_ID }}" | |
| echo "/usr/bin/hsync" | |
| echo "/usr/share/applications/${{ env.APP_ID }}.desktop" | |
| echo "/usr/share/icons/hicolor/256x256/apps/${{ env.APP_ID }}.png" | |
| echo | |
| echo "%post" | |
| echo "update-desktop-database /usr/share/applications || true" | |
| echo | |
| echo "%postun" | |
| echo "update-desktop-database /usr/share/applications || true" | |
| } > "${RPMBUILD}/SPECS/${{ env.APP_ID }}.spec" | |
| rpmbuild -bb \ | |
| --define "_topdir ${RPMBUILD}" \ | |
| --define "_buildrootdir ${RPMBUILD}/BUILDROOT" \ | |
| --buildroot "${BUILDROOT}" \ | |
| --target "${RPM_ARCH}" \ | |
| "${RPMBUILD}/SPECS/${{ env.APP_ID }}.spec" | |
| RPM_FILE=$(find "${RPMBUILD}/RPMS" -name "*.rpm" | head -1) | |
| if [ -z "${RPM_FILE}" ]; then | |
| echo "::error::rpmbuild did not produce an RPM file." | |
| exit 1 | |
| fi | |
| OUT_RPM="${{ env.APP_ID }}_${NUM}_${RPM_ARCH}.rpm" | |
| cp "$RPM_FILE" "$OUT_RPM" | |
| echo "Created: $OUT_RPM" | |
| - name: Build tar.gz (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| OUT="${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-linux-${{ matrix.arch_display }}.tar.gz" | |
| tar -czf "$OUT" -C dist/ "${{ env.APP_NAME }}" | |
| echo "Created: $OUT" | |
| - name: Build standalone CLI tar.gz (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| CLI_OUT="hsync-${{ env.VERSION_TAG }}-linux-${{ matrix.arch_display }}.tar.gz" | |
| tar -czf "$CLI_OUT" -C dist/ hsync | |
| CLI_SIZE=$(du -sh "$CLI_OUT" | cut -f1) | |
| echo "Created: $CLI_OUT (${CLI_SIZE})" | |
| - name: Upload artifacts (Windows) | |
| if: runner.os == 'Windows' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pkg-${{ matrix.platform_name }} | |
| path: | | |
| ${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-setup.exe | |
| ${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-portable.zip | |
| hsync-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}-setup.exe | |
| hsync-${{ env.VERSION_TAG }}-windows-${{ matrix.arch_display }}.zip | |
| - name: Upload artifacts (macOS) | |
| if: runner.os == 'macOS' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pkg-${{ matrix.platform_name }} | |
| path: | | |
| ${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-macos-${{ matrix.arch_display }}.dmg | |
| hsync-${{ env.VERSION_TAG }}-macos-${{ matrix.arch_display }}.tar.gz | |
| - name: Upload artifacts (Linux) | |
| if: runner.os == 'Linux' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pkg-${{ matrix.platform_name }} | |
| path: | | |
| ${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-linux-${{ matrix.arch_display }}.AppImage | |
| ${{ env.APP_ID }}_${{ env.VERSION_NUM }}_${{ matrix.deb_arch }}.deb | |
| ${{ env.APP_ID }}_${{ env.VERSION_NUM }}_${{ matrix.rpm_arch }}.rpm | |
| ${{ env.APP_NAME }}-${{ env.VERSION_TAG }}-linux-${{ matrix.arch_display }}.tar.gz | |
| hsync-${{ env.VERSION_TAG }}-linux-${{ matrix.arch_display }}.tar.gz |