Windows build #107
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: Windows build | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| env: | |
| PYTHON_VERSION: '3.14' | |
| jobs: | |
| build: | |
| permissions: | |
| contents: write | |
| strategy: | |
| matrix: | |
| include: | |
| - arch: x64 | |
| runner: windows-latest | |
| python_arch: x64 | |
| artifact_suffix: x64 | |
| unsigned_exe_name: unsigned-exes-x64 | |
| unsigned_msi_name: unsigned-msi-x64 | |
| unsigned_dll_name: unsigned-dll-x64 | |
| dll_file: YASBTrayHook.dll | |
| - arch: arm64 | |
| runner: windows-11-arm | |
| python_arch: arm64 | |
| artifact_suffix: aarch64 | |
| unsigned_exe_name: unsigned-exes-arm64 | |
| unsigned_msi_name: unsigned-msi-arm64 | |
| unsigned_dll_name: unsigned-dll-arm64 | |
| dll_file: YASBTrayHook_arm64.dll | |
| runs-on: ${{ matrix.runner }} | |
| name: Build (${{ matrix.arch }}) | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| architecture: ${{ matrix.python_arch }} | |
| - name: Create virtual environment | |
| run: | | |
| python -m venv venv | |
| shell: pwsh | |
| - name: Upload unsigned DLL artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.unsigned_dll_name }} | |
| path: src/core/widgets/services/systray/hook/${{ matrix.dll_file }} | |
| - name: Get unsigned-dll artifact id | |
| id: get_dll_artifact | |
| uses: actions/github-script@v7 | |
| env: | |
| ARTIFACT_NAME: ${{ matrix.unsigned_dll_name }} | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const run_id = context.runId; | |
| const expectedName = process.env.ARTIFACT_NAME; | |
| const res = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, run_id }); | |
| const artifact = res.data.artifacts.find(a => a.name === expectedName); | |
| if (!artifact) throw new Error(`${expectedName} artifact not found`); | |
| return artifact.id; | |
| - name: Submit DLL signing request | |
| id: sign_dll | |
| uses: signpath/github-action-submit-signing-request@v1 | |
| with: | |
| api-token: '${{ secrets.SIGN_TOKEN }}' | |
| organization-id: '9efb6764-d1fc-46c5-b050-5ef07bb67a8c' | |
| project-slug: 'yasb' | |
| signing-policy-slug: '${{ secrets.SIGN_POLICY_SLUG }}' | |
| artifact-configuration-slug: 'signing_dll' | |
| github-artifact-id: '${{ steps.get_dll_artifact.outputs.result }}' | |
| wait-for-completion: 'true' | |
| output-artifact-directory: 'src/signed' | |
| - name: Replace unsigned DLL with signed | |
| if: steps.sign_dll.outcome == 'success' | |
| run: | | |
| $signedDir = 'src/signed' | |
| if (-not (Test-Path $signedDir)) { | |
| Write-Host "Signed artifacts directory $signedDir not found. Ensure SignPath places signed files there."; | |
| exit 1 | |
| } | |
| # Copy signed DLL back to source location fail if none found | |
| $signedDlls = Get-ChildItem -Path $signedDir -File -Filter '*.dll' -Recurse -ErrorAction SilentlyContinue | |
| if ($null -eq $signedDlls -or $signedDlls.Count -eq 0) { | |
| Write-Error "No signed .dll files found in $signedDir after extraction. Failing the job." | |
| exit 1 | |
| } | |
| Write-Host "Copying signed DLL from $signedDir to src/core/widgets/services/systray/hook" | |
| foreach ($f in $signedDlls) { | |
| Copy-Item -Path $f.FullName -Destination (Join-Path 'src/core/widgets/services/systray/hook' $f.Name) -Force | |
| } | |
| # Clean up signed directory after successful copy | |
| try { | |
| Get-ChildItem -Path $signedDir -Recurse -Force | Remove-Item -Force -Recurse | |
| Write-Host "Cleaned up $signedDir" | |
| } catch { | |
| Write-Warning "Failed to clean up ${signedDir}: $($_.Exception.Message)" | |
| } | |
| shell: pwsh | |
| - name: Activate virtual environment and install dependencies | |
| run: | | |
| .\venv\Scripts\Activate | |
| python -m pip install --upgrade pip | |
| pip install --force --no-cache .[packaging] | |
| shell: pwsh | |
| - name: Build EXE | |
| run: | | |
| .\venv\Scripts\Activate | |
| cd src | |
| python build.py build | |
| shell: pwsh | |
| - name: Upload unsigned EXE artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.unsigned_exe_name }} | |
| path: | | |
| src/dist/yasb.exe | |
| src/dist/yasbc.exe | |
| src/dist/yasb_themes.exe | |
| - name: Get unsigned-exes artifact id | |
| id: get_exes_artifact | |
| uses: actions/github-script@v7 | |
| env: | |
| ARTIFACT_NAME: ${{ matrix.unsigned_exe_name }} | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const run_id = context.runId; | |
| const expectedName = process.env.ARTIFACT_NAME; | |
| const res = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, run_id }); | |
| const artifact = res.data.artifacts.find(a => a.name === expectedName); | |
| if (!artifact) throw new Error(`${expectedName} artifact not found`); | |
| return artifact.id; | |
| - name: Submit EXE signing request | |
| id: sign_exes | |
| uses: signpath/github-action-submit-signing-request@v1 | |
| with: | |
| api-token: '${{ secrets.SIGN_TOKEN }}' | |
| organization-id: '9efb6764-d1fc-46c5-b050-5ef07bb67a8c' | |
| project-slug: 'yasb' | |
| signing-policy-slug: '${{ secrets.SIGN_POLICY_SLUG }}' | |
| artifact-configuration-slug: 'signing_executable' | |
| github-artifact-id: '${{ steps.get_exes_artifact.outputs.result }}' | |
| wait-for-completion: 'true' | |
| output-artifact-directory: 'src/signed' | |
| - name: Replace unsigned EXE with signed | |
| if: steps.sign_exes.outcome == 'success' | |
| run: | | |
| .\venv\Scripts\Activate | |
| $signedDir = 'src/signed' | |
| if (-not (Test-Path $signedDir)) { | |
| Write-Host "Signed artifacts directory $signedDir not found. Ensure SignPath places signed files there."; | |
| exit 1 | |
| } | |
| # Copy signed EXEs into the distribution folder; fail if none found | |
| $signedExes = Get-ChildItem -Path $signedDir -File -Filter '*.exe' -Recurse -ErrorAction SilentlyContinue | |
| if ($null -eq $signedExes -or $signedExes.Count -eq 0) { | |
| Write-Error "No signed .exe files found in $signedDir after extraction. Failing the job." | |
| exit 1 | |
| } | |
| Write-Host "Copying signed EXEs from $signedDir to src/dist" | |
| foreach ($f in $signedExes) { | |
| Copy-Item -Path $f.FullName -Destination (Join-Path 'src/dist' $f.Name) -Force | |
| } | |
| # Clean up signed directory after successful copy | |
| try { | |
| Get-ChildItem -Path $signedDir -Recurse -Force | Remove-Item -Force -Recurse | |
| Write-Host "Cleaned up $signedDir" | |
| } catch { | |
| Write-Warning "Failed to clean up ${signedDir}: $($_.Exception.Message)" | |
| } | |
| shell: pwsh | |
| - name: Build MSI | |
| run: | | |
| .\venv\Scripts\Activate | |
| cd src | |
| python build.py bdist_msi | |
| shell: pwsh | |
| - name: Upload unsigned MSI | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.unsigned_msi_name }} | |
| path: src/dist/out/*.msi | |
| - name: Get unsigned-msi artifact id | |
| id: get_msi_artifact | |
| uses: actions/github-script@v7 | |
| env: | |
| ARTIFACT_NAME: ${{ matrix.unsigned_msi_name }} | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const run_id = context.runId; | |
| const expectedName = process.env.ARTIFACT_NAME; | |
| const res = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, run_id }); | |
| const artifact = res.data.artifacts.find(a => a.name === expectedName); | |
| if (!artifact) throw new Error(`${expectedName} artifact not found`); | |
| return artifact.id; | |
| - name: Submit MSI signing request | |
| id: sign_msi | |
| uses: signpath/github-action-submit-signing-request@v1 | |
| with: | |
| api-token: '${{ secrets.SIGN_TOKEN }}' | |
| organization-id: '9efb6764-d1fc-46c5-b050-5ef07bb67a8c' | |
| project-slug: 'yasb' | |
| signing-policy-slug: '${{ secrets.SIGN_POLICY_SLUG }}' | |
| artifact-configuration-slug: 'signing_installer' | |
| github-artifact-id: '${{ steps.get_msi_artifact.outputs.result }}' | |
| wait-for-completion: 'true' | |
| output-artifact-directory: 'src/signed' | |
| - name: Replace unsigned MSI with signed | |
| if: steps.sign_msi.outcome == 'success' | |
| run: | | |
| .\venv\Scripts\Activate | |
| $signedDir = 'src/signed' | |
| if (-not (Test-Path $signedDir)) { | |
| Write-Host "Signed artifacts directory $signedDir not found. Ensure SignPath places signed files there."; | |
| exit 1 | |
| } | |
| # Copy signed MSIs into the output folder; fail if none found | |
| $signedMsis = Get-ChildItem -Path $signedDir -File -Filter '*.msi' -Recurse -ErrorAction SilentlyContinue | |
| if ($null -eq $signedMsis -or $signedMsis.Count -eq 0) { | |
| Write-Error "No signed .msi files found in $signedDir after extraction. Failing the job." | |
| exit 1 | |
| } | |
| Write-Host "Copying signed MSIs from $signedDir to src/dist/out" | |
| foreach ($f in $signedMsis) { | |
| Copy-Item -Path $f.FullName -Destination (Join-Path 'src/dist/out' $f.Name) -Force | |
| } | |
| # Clean up signed directory after successful copy | |
| try { | |
| Get-ChildItem -Path $signedDir -Recurse -Force | Remove-Item -Force -Recurse | |
| Write-Host "Cleaned up $signedDir" | |
| } catch { | |
| Write-Warning "Failed to clean up ${signedDir}: $($_.Exception.Message)" | |
| } | |
| shell: pwsh | |
| - name: Upload Built MSI | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: built-msi-${{ matrix.artifact_suffix }} | |
| path: src/dist/out/*.msi | |
| - name: Delete Artifacts | |
| if: github.event_name == 'workflow_dispatch' | |
| uses: geekyeggo/delete-artifact@v5 | |
| with: | |
| name: | | |
| unsigned-* | |
| signed-* | |
| release: | |
| needs: build | |
| runs-on: windows-latest | |
| permissions: | |
| contents: write | |
| models: read | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Create virtual environment | |
| run: | | |
| python -m venv venv | |
| shell: pwsh | |
| - name: Download all MSI artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: built-msi-* | |
| path: dist-msis | |
| merge-multiple: true | |
| - name: Get App Info | |
| id: get_version | |
| run: | | |
| .\venv\Scripts\Activate | |
| $version = (Get-Content src/settings.py | Select-String -Pattern 'BUILD_VERSION\s*=\s*"([^"]+)"').Matches.Groups[1].Value | |
| echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| shell: pwsh | |
| - name: Create Tag | |
| id: create_tag | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| const version = `v${process.env.VERSION}`; | |
| const { owner, repo } = context.repo; | |
| const sha = context.sha; | |
| try { | |
| const { data: tags } = await github.rest.repos.listTags({ | |
| owner, | |
| repo, | |
| }); | |
| const tagExists = tags.some(tag => tag.name === version); | |
| if (!tagExists) { | |
| console.log(`Creating tag ${version}`); | |
| await github.rest.git.createRef({ | |
| owner, | |
| repo, | |
| ref: `refs/tags/${version}`, | |
| sha, | |
| }); | |
| } else { | |
| console.log(`Tag ${version} already exists, skipping tag creation`); | |
| } | |
| } catch (error) { | |
| console.error(`Error fetching tags: ${error.message}`); | |
| throw error; | |
| } | |
| - name: Install dependencies for schema generation | |
| run: | | |
| .\venv\Scripts\Activate | |
| pip install --force --no-cache . | |
| shell: pwsh | |
| - name: Generate schema.json | |
| run: | | |
| .\venv\Scripts\Activate | |
| python src/core/validation/export_schema.py | |
| shell: pwsh | |
| - name: Commit schema.json | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add schema.json | |
| git diff --cached --quiet || git commit -m "chore(schema): update schema.json for v${{ steps.get_version.outputs.VERSION }}" | |
| git push | |
| shell: pwsh | |
| - name: Fetch all tags | |
| run: git fetch --tags | |
| shell: pwsh | |
| - name: Create Changelog | |
| id: changelog | |
| uses: loopwerk/tag-changelog@v1.3.0 | |
| with: | |
| token: ${{ secrets.PAT }} | |
| config_file: .github/changelog/changelog.js | |
| - name: Compute Commit Range | |
| id: commit_range | |
| shell: pwsh | |
| run: | | |
| $currentTag = "v${{ steps.get_version.outputs.VERSION }}" | |
| $prevTag = (git tag --sort=-version:refname | Where-Object { $_ -ne $currentTag } | Select-Object -First 1) | |
| if ([string]::IsNullOrWhiteSpace($prevTag)) { | |
| $prevTag = (git describe --tags --abbrev=0 2>$null) | |
| } | |
| if ([string]::IsNullOrWhiteSpace($prevTag)) { | |
| $range = "HEAD" | |
| $commits = git log -n 100 --pretty=format:"- %s (%h)%n%b%n----" | |
| } else { | |
| $range = "$prevTag..$currentTag" | |
| $commits = git log $range -n 100 --pretty=format:"- %s (%h)%n%b%n----" | |
| } | |
| "range=$range" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "commits<<EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| $commits | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - name: Summarize Changelog | |
| id: ai_summary | |
| uses: actions/github-script@v8 | |
| env: | |
| AI_SYSTEM_PROMPT: ${{ secrets.AI_SYSTEM_PROMPT }} | |
| AI_USER_PROMPT_TEMPLATE: ${{ secrets.AI_USER_PROMPT }} | |
| AI_MODEL: ${{ secrets.AI_MODEL }} | |
| AI_MAX_TOKENS: ${{ secrets.AI_MAX_TOKENS }} | |
| COMMIT_RANGE: ${{ steps.commit_range.outputs.range }} | |
| COMMITS: ${{ steps.commit_range.outputs.commits }} | |
| CHANGELOG: ${{ steps.changelog.outputs.changes }} | |
| with: | |
| github-token: ${{ secrets.AI_TOKEN }} | |
| script: | | |
| const systemPrompt = process.env.AI_SYSTEM_PROMPT; | |
| const promptTemplate = process.env.AI_USER_PROMPT_TEMPLATE; | |
| const model = process.env.AI_MODEL; | |
| const maxTokens = parseInt(process.env.AI_MAX_TOKENS, 300); | |
| if (!systemPrompt || !promptTemplate) { | |
| console.log('Prompts not configured. Skipping summary.'); | |
| core.setOutput('summary', ''); | |
| return; | |
| } | |
| const userPrompt = promptTemplate | |
| .replace('{commit_range}', process.env.COMMIT_RANGE || '') | |
| .replace('{commits}', process.env.COMMITS || '') | |
| .replace('{changelog}', process.env.CHANGELOG || ''); | |
| try { | |
| const response = await fetch('https://models.github.ai/inference/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${{ secrets.AI_TOKEN }}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| model: model, | |
| messages: [ | |
| { role: 'system', content: systemPrompt }, | |
| { role: 'user', content: userPrompt } | |
| ], | |
| max_tokens: maxTokens | |
| }) | |
| }); | |
| if (!response.ok) { | |
| console.log(`Request failed: ${response.status}`); | |
| core.setOutput('summary', ''); | |
| return; | |
| } | |
| const data = await response.json(); | |
| const summary = data.choices?.[0]?.message?.content?.trim() || ''; | |
| core.setOutput('summary', summary); | |
| } catch (error) { | |
| console.log(`Error: ${error.message}`); | |
| core.setOutput('summary', ''); | |
| } | |
| - name: Build Release Body | |
| id: release_body | |
| uses: actions/github-script@v8 | |
| env: | |
| AI_SUMMARY: ${{ steps.ai_summary.outputs.summary }} | |
| CHANGELOG: ${{ steps.changelog.outputs.changes }} | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| const summary = (process.env.AI_SUMMARY || '').trim(); | |
| const changelog = (process.env.CHANGELOG || '').trim(); | |
| let body = ''; | |
| if (summary && summary.length > 50) { | |
| body = `${summary}\n\n\n\n${changelog}`; | |
| } else { | |
| body = changelog; | |
| } | |
| core.setOutput('body', body); | |
| - name: Generate Checksum | |
| run: | | |
| .\venv\Scripts\Activate | |
| $checksums = Get-FileHash dist-msis/*.msi -Algorithm SHA256 | |
| $output = @() | |
| foreach ($checksum in $checksums) { | |
| $filename = [System.IO.Path]::GetFileName($checksum.Path) | |
| $output += "$($checksum.Hash) $filename" | |
| } | |
| $output -join "`n" > dist-msis/checksums.txt | |
| shell: pwsh | |
| - name: Create and Upload Release | |
| id: create_release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.get_version.outputs.VERSION }} | |
| name: v${{ steps.get_version.outputs.VERSION }} | |
| body: | | |
| ${{ steps.release_body.outputs.body }} | |
| append_body: true | |
| files: | | |
| dist-msis/*.msi | |
| dist-msis/checksums.txt | |
| prerelease: false | |
| generate_release_notes: true | |
| draft: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.PAT }} |