# Сборка и публикация «Переводчика экрана». # # Идёт на self-hosted Windows-раннере (метка `windows`, host mode) — иначе никак: # распознавание держится на Windows.Media.Ocr, а сборка — на Roslyn из VS Build Tools # и на Windows.winmd из Windows SDK. В контейнере ничего этого нет. # # * push в master -> сборка + прогон проверок + перезапись релиза `latest` # * push тега v* -> то же, но отдельный постоянный релиз с этой версией # # Требований к раннеру всего два, и оба уже выполнены (тот же хост собирает GusVoice): # Visual Studio Build Tools (нужен Roslyn csc) и Windows SDK (Windows.winmd). # Если Roslyn вдруг отсутствует, build.ps1 сам откатывается на компилятор из состава # Windows — код специально написан без возможностей C# 6 и выше, чтобы это работало. name: build on: push: branches: [master] tags: ['v*'] workflow_dispatch: permissions: contents: write env: GITEA_HOST: gitea.gusik.tech jobs: build: runs-on: windows steps: # Обычный actions/checkout тянется с github.com, а до него из этой сети TLS не доходит # (`net/http: TLS handshake timeout`). Репозиторий публичный и лежит рядом, поэтому # проще и надёжнее склонировать его напрямую, без внешних действий. - name: Checkout shell: pwsh env: REPO: ${{ github.repository }} SHA: ${{ github.sha }} run: | $ErrorActionPreference = 'Stop' git init -q . git remote add origin "https://$($env:GITEA_HOST)/$($env:REPO).git" git -c protocol.version=2 fetch -q --depth 1 origin $env:SHA git checkout -q FETCH_HEAD git log --oneline -1 - name: Сборка exe и диагностической утилиты shell: pwsh run: .\build.ps1 -Test # Модель правдоподобия английского — единственная часть, которую можно проверить # без экрана и без языковых пакетов. Проверяет, что таблица биграмм на месте и # всё ещё отделяет настоящие надписи от каши распознавания. # ВНИМАНИЕ: внутри run: только латиница. Раннер сохраняет шаг во временный .ps1 # в кодировке, которая портит кириллицу, и условие с русским текстом не сработает # (проверено: сравнение превращалось в мусор и валило сборку). - name: Проверка биграммной модели shell: pwsh run: | $out = & .\dist\RenderTest.exe --selftest 2>&1 | Out-String Write-Host $out if ($LASTEXITCODE -ne 0) { throw "selftest exit code $LASTEXITCODE" } if ($out -notmatch 'RESULT worst_good=(-?[\d.]+) best_bad=(-?[\d.]+)') { throw "selftest produced no summary" } $good = [double]$Matches[1]; $bad = [double]$Matches[2] Write-Host "worst good = $good, best bad = $bad" # Настоящие надписи не должны опускаться до уровня каши, иначе фильтр начнёт # выбрасывать нормальный текст. if ($good -lt -3.0) { throw "real phrases scored below the garbage threshold" } # Гейт качества на эталонном кадре. Работает, только если на раннере установлен # английский пакет распознавания; если его нет — не валим сборку, а предупреждаем: # это среда, а не регрессия кода. - name: Проверка распознавания на эталоне shell: pwsh run: | .\tools\make-sample.ps1 $out = & .\dist\RenderTest.exe --in .\dist\sample.png --out .\dist\ci --no-translate 2>&1 | Out-String Write-Host $out # Латиница в шаблонах намеренно — см. примечание о кодировке выше. if ($out -match 'OCR\s+\S*\s*=?\s*$' -or $out -match 'exit code 2') { Write-Host '::warning::no OCR language pack on the runner - frame check skipped' exit 0 } if ($out -notmatch '->\s*(\d+)') { Write-Host '::warning::no block count in output - frame check skipped' exit 0 } $blocks = [int]$Matches[1] Write-Host "blocks detected: $blocks" # На этом кадре стабильно выходит 27-29 блоков. Резкое расхождение означает, что # правки в Blocks.cs поехали — либо всё слиплось, либо рассыпалось. if ($blocks -lt 18 -or $blocks -gt 40) { throw "unexpected block count ($blocks) - segmentation broke" } - name: Сборка пакета shell: pwsh run: | $ErrorActionPreference = 'Stop' $rel = 'release' New-Item -ItemType Directory -Force -Path $rel | Out-Null Copy-Item .\dist\ScreenTranslator.exe $rel -Force # Инструкция названа по-русски, поэтому берём её по маске, а не по имени: # писать кириллицу в этот скрипт нельзя (кодировка раннера). Get-ChildItem -Path . -Filter '*.txt' -File | Copy-Item -Destination $rel -Force # config.json программа делает сама из вшитого умолчания & .\dist\ScreenTranslator.exe --write-config Start-Sleep -Seconds 2 if (Test-Path .\dist\config.json) { Copy-Item .\dist\config.json $rel -Force } else { Write-Host '::warning::config.json was not generated' } Get-ChildItem $rel | Select-Object Name, Length | Format-Table -AutoSize Compress-Archive -Path "$rel\*" -DestinationPath 'ScreenTranslator.zip' -Force # Тег v* -> постоянный релиз. Любой push в master -> перезаписываемый `latest`, # чтобы у тех, кому программу отдали, всегда была ссылка на свежую сборку. - name: Публикация релиза shell: pwsh env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} REPO: ${{ github.repository }} REF_NAME: ${{ github.ref_name }} REF: ${{ github.ref }} SHA: ${{ github.sha }} run: | $ErrorActionPreference = 'Stop' $host_ = $env:GITEA_HOST $base = "https://${host_}/api/v1/repos/$($env:REPO)" $hdr = @{ Authorization = "token $($env:GITEA_TOKEN)" } $isTag = $env:REF -like 'refs/tags/*' $tag = if ($isTag) { $env:REF_NAME } else { 'latest' } if (-not $isTag) { # Скользящий релиз: сносим прошлый вместе с тегом, иначе он останется # висеть на старом коммите и «свежая сборка» перестанет быть свежей. try { $old = Invoke-RestMethod -Uri "$base/releases/tags/$tag" -Headers $hdr if ($old.id) { Invoke-RestMethod -Method Delete -Uri "$base/releases/$($old.id)" -Headers $hdr | Out-Null Write-Host "previous $tag release removed" } } catch { } try { Invoke-RestMethod -Method Delete -Uri "$base/tags/$tag" -Headers $hdr | Out-Null } catch { } } $short = $env:SHA.Substring(0, 8) $stamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm') + ' UTC' # Русский текст описания лежит в файле и читается как UTF-8 — в самом скрипте # кириллицу писать нельзя (её ломает кодировка временного .ps1 у раннера). $notes = (Get-Content '.gitea/release-body.md' -Raw -Encoding UTF8). Replace('{COMMIT}', $short).Replace('{DATE}', $stamp) $body = @{ tag_name = $tag target_commitish = $env:SHA name = if ($isTag) { $tag } else { "latest ($short)" } body = $notes draft = $false prerelease = (-not $isTag) } | ConvertTo-Json -Compress try { $rel = Invoke-RestMethod -Method Post -Uri "$base/releases" -Headers $hdr -ContentType 'application/json' -Body $body } catch { # Релиз на этот тег уже есть (перезапуск задания) — берём его и обновляем описание $rel = Invoke-RestMethod -Uri "$base/releases/tags/$tag" -Headers $hdr $patch = @{ body = $notes } | ConvertTo-Json -Compress Invoke-RestMethod -Method Patch -Uri "$base/releases/$($rel.id)" -Headers $hdr -ContentType 'application/json' -Body $patch | Out-Null } if (-not $rel.id) { throw "could not resolve release for tag $tag" } # Старые файлы с тем же именем API не заменяет, поэтому сначала чистим try { foreach ($a in (Invoke-RestMethod -Uri "$base/releases/$($rel.id)/assets" -Headers $hdr)) { Invoke-RestMethod -Method Delete -Uri "$base/releases/$($rel.id)/assets/$($a.id)" -Headers $hdr | Out-Null } } catch { } foreach ($f in @('ScreenTranslator.zip', '.\dist\ScreenTranslator.exe')) { $item = Get-Item $f Write-Host "uploading $($item.Name) ..." & curl.exe -sf -X POST "$base/releases/$($rel.id)/assets?name=$($item.Name)" ` -H "Authorization: token $($env:GITEA_TOKEN)" -F "attachment=@$($item.FullName)" if ($LASTEXITCODE -ne 0) { throw "upload failed for $($item.Name)" } } Write-Host "release $tag published: https://${host_}/$($env:REPO)/releases"