Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -786,9 +786,12 @@ jobs:
- name: Build the web UI
run: npm run build

# --msix packs the same directory a second time as the Store package
# (GDK-1380) — one compile, two outputs; the Windows job is CI's
# critical path (GDK-1036) and a second build would double it.
- name: Pack the Windows app
shell: pwsh
run: ./desktop/build-windows.ps1
run: ./desktop/build-windows.ps1 --msix

- name: The packed tree carries both binaries
shell: pwsh
Expand Down Expand Up @@ -817,6 +820,60 @@ jobs:
working-directory: desktop
run: go test ./... -run 'Protocol' -count=1

# makeappx validates the manifest schema; only an install proves the
# identity, the full-trust entry point and the protocol declaration
# are accepted. The Store re-signs with its own certificate, so here
# a throwaway self-signed certificate with the same Publisher CN
# stands in — trusted by this runner only, for the length of the step.
# Last in the job on purpose: deploying a package that declares
# windows.protocol registers the scheme in this user's hive, and the
# HKCU assertion above must keep measuring the pack script alone. The
# GUI is not launched.
- name: The msix installs under its declared identity
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$msix = Get-ChildItem -Path desktop/build -File -Filter 'Gadak-*-windows-x64.msix' | Select-Object -First 1
if (-not $msix) { throw "build-windows.ps1 --msix produced no .msix" }
$manifest = Select-Xml -Path desktop/msix/AppxManifest.xml -XPath '/*[local-name()="Package"]/*[local-name()="Identity"]' | Select-Object -First 1
$publisher = $manifest.Node.Publisher
$name = $manifest.Node.Name
$kit = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Directory -Filter '10.*' |
Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1
$signtool = Join-Path $kit.FullName 'x64\signtool.exe'
if (-not (Test-Path -LiteralPath $signtool)) { throw "no signtool.exe under $($kit.FullName)" }
$cert = New-SelfSignedCertificate -Type Custom -Subject $publisher -KeyUsage DigitalSignature `
-FriendlyName 'gadak ci throwaway' -CertStoreLocation 'Cert:\CurrentUser\My' `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}')
$pfx = Join-Path $env:RUNNER_TEMP 'ci-throwaway.pfx'
$pw = ConvertTo-SecureString -String 'ci' -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $pfx -Password $pw | Out-Null
# Sign a copy: the unsigned original is the Store upload artifact.
$test = Join-Path $env:RUNNER_TEMP $msix.Name
Copy-Item -LiteralPath $msix.FullName -Destination $test -Force
& $signtool sign /fd SHA256 /a /f $pfx /p ci $test
if ($LASTEXITCODE -ne 0) { throw "signtool exited $LASTEXITCODE" }
Import-PfxCertificate -FilePath $pfx -CertStoreLocation 'Cert:\LocalMachine\TrustedPeople' -Password $pw | Out-Null
Add-AppxPackage -Path $test
$pkg = Get-AppxPackage -Name $name
if (-not $pkg) { throw "installed, but Get-AppxPackage $name found nothing" }
Write-Host ("installed {0} {1} ({2})" -f $pkg.Name, $pkg.Version, $pkg.Architecture)
if ($pkg.Publisher -ne $publisher) { throw "publisher drifted: $($pkg.Publisher) vs manifest $publisher" }
$installed = Join-Path $pkg.InstallLocation 'gadak-desktop.exe'
if (-not (Test-Path -LiteralPath $installed)) { throw "package has no gadak-desktop.exe at $installed" }
Remove-AppxPackage -Package $pkg.PackageFullName
Write-Host "msix install/uninstall — ok"
exit 0

# The file Partner Center takes. The install check above signed a copy
# in RUNNER_TEMP, so this is the untouched unsigned package.
- name: Upload Store package artifact
uses: actions/upload-artifact@v4
with:
name: gadak-desktop-msix-x64
path: desktop/build/Gadak-*-windows-x64.msix
if-no-files-found: error

# In a child process, not in this step's own runspace: a non-zero
# $LASTEXITCODE left behind at the end of a pwsh step is what the runner
# reads as the step's result, so checking it in place fails the job even
Expand Down
19 changes: 16 additions & 3 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,15 @@ jobs:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if (-not (Test-Path -LiteralPath dist/app)) { throw "dist/app missing" }

- name: Pack Windows portable zips
# --msix adds the unsigned Store package next to each zip (GDK-1380).
# It is a workflow artifact only, never a Release asset: unsigned, it
# is not installable, and the Store re-signs it after certification.
- name: Pack Windows portable zips and Store packages
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
./desktop/build-windows.ps1 --arch x64 --archive
./desktop/build-windows.ps1 --arch arm64 --archive
./desktop/build-windows.ps1 --arch x64 --archive --msix
./desktop/build-windows.ps1 --arch arm64 --archive --msix

# Recurrence gate (same decision as the macOS job, 2026-08-15):
# v0.14.0 apps match gadak-desktop-<os>-<arch>.zip exactly and would
Expand Down Expand Up @@ -329,6 +332,16 @@ jobs:
desktop/build/Gadak-*-windows-*.zip
if-no-files-found: error

# For the maintainer to download and submit in Partner Center. The
# Release step below filters on *.zip and never sees these.
- name: Upload Store package artifact (unsigned msix)
uses: actions/upload-artifact@v4
with:
name: gadak-desktop-msix
path: |
desktop/build/Gadak-*-windows-*.msix
if-no-files-found: error

- name: Attach zip to GitHub Release
if: startsWith(github.ref, 'refs/tags/')
env:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

<sub><a href="CHANGELOG.md">English</a> · 한국어 — 영문이 원본이며, 번역은 영문과 함께 갱신됩니다(마지막 동기화 2026-09-03).</sub>

## Unreleased

- Windows 앱에 Microsoft Store 패키지가 생겼다: `desktop/build-windows.ps1
--msix` 가 zip 이 싣는 것과 같은 `gadak-desktop.exe` 를 무서명 `.msix` 로
묶고, 릴리스 워크플로가 그것을 Partner Center 용 아티팩트로 남긴다. Store 는
인증한 패키지를 자기 인증서로 재서명하는데, 그것이 Smart App Control 을
지나는 유일한 무료 경로다 — zip 은 그대로 무서명이고, 등록이 심사를 통과하기
전까지 "Store 에 있다"는 말은 어디에도 없다 ([GDK-1380]).

## v0.20.0 — 2026-09-03

**셸이 창 안에 있고, 그 셸은 이슈의 셸입니다.** 터미널 도크가 이제 창
Expand Down Expand Up @@ -1028,6 +1037,7 @@ HTTP·sync·에이전트 계약을 담았습니다.
[GDK-208]: https://gadak.dev/backlog/#/?ks=GDK-208
[GDK-209]: https://gadak.dev/backlog/#/?ks=GDK-209
[GDK-211]: https://gadak.dev/backlog/#/?ks=GDK-211
[GDK-1380]: https://gadak.dev/backlog/#/?ks=GDK-1380
[GDK-213]: https://gadak.dev/backlog/#/?ks=GDK-213
[GDK-214]: https://gadak.dev/backlog/#/?ks=GDK-214
[GDK-215]: https://gadak.dev/backlog/#/?ks=GDK-215
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

<sub>English · <a href="CHANGELOG.ko.md">한국어</a></sub>

## Unreleased

- The Windows app has a Microsoft Store package: `desktop/build-windows.ps1
--msix` packs the same `gadak-desktop.exe` the zip ships into an unsigned
`.msix`, and the release workflow keeps it as an artifact for Partner
Center. The Store re-signs what it certifies, which is the one free route
past Smart App Control — the zip stays unsigned and unchanged, and nothing
is "on the Store" until a listing passes ([GDK-1380]).

## v0.20.0 — 2026-09-03

**The shell is in the window, and it is the issue's shell.** The terminal
Expand Down Expand Up @@ -1074,6 +1083,7 @@ and the storage schema plus the HTTP, sync and agent contracts.
[GDK-208]: https://gadak.dev/backlog/#/?ks=GDK-208
[GDK-209]: https://gadak.dev/backlog/#/?ks=GDK-209
[GDK-211]: https://gadak.dev/backlog/#/?ks=GDK-211
[GDK-1380]: https://gadak.dev/backlog/#/?ks=GDK-1380
[GDK-213]: https://gadak.dev/backlog/#/?ks=GDK-213
[GDK-214]: https://gadak.dev/backlog/#/?ks=GDK-214
[GDK-215]: https://gadak.dev/backlog/#/?ks=GDK-215
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ MEDIA_DIR := docs/media
brand:
node tools/brand/render.mjs
bash tools/brand/mobile-icons.sh
bash tools/brand/msix-assets.sh
bash tools/check-brand-icons.sh

# media-mcp is deliberately not here: it needs vhs and a Claude Code login,
Expand Down
25 changes: 21 additions & 4 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ both so the rest of the tree can point here instead of restating either.
| GOOS | Pack | Shipped on tag | `ApplicationLaunchedWithUrl` |
| --- | --- | --- | --- |
| darwin | `desktop/build-app.sh` → `Gadak-<ver>-arm64.dmg` (signed/notarized) | yes | yes (Apple Event; argv is not applied) |
| windows | `desktop/build-windows.ps1` → `Gadak-<ver>-windows-<x64\|arm64>.zip` (unsigned; [GDK-211]) | yes (from 0.16) | yes when argv is exactly one `://` argument (wails `pkg/application/application_windows.go`); otherwise argv |
| windows | `desktop/build-windows.ps1` → `Gadak-<ver>-windows-<x64\|arm64>.zip` (unsigned; [GDK-211]); `--msix` → `.msix` for the Microsoft Store (unsigned upload, Store re-signs; [GDK-1380]) | yes (from 0.16); the msix is a workflow artifact, not a Release asset | yes when argv is exactly one `://` argument (wails `pkg/application/application_windows.go`); otherwise argv |
| linux | `desktop/build-linux.sh` → AppDir / AppImage | no — from-source only | yes when argv is exactly one `://` argument (wails `pkg/application/application_linux.go`; wailsapp/wails#6000 landed in beta.10); otherwise argv |

The in-app Jira/Confluence browse pane is still darwin-only (`embed_darwin.go`; other GOOS use the stub in `embed_other.go`).
Expand All @@ -40,17 +40,28 @@ desktop/build-linux.sh --appimage # → desktop/build/Gadak-<version>-x86_64.A
Windows (cross-compile with `CGO_ENABLED=0`; Authenticode and a missing-WebView2 check still need a Windows host — see WebView2 below):

```powershell
desktop/build-windows.ps1 # → desktop/build/Gadak-<version>-x64/
desktop/build-windows.ps1 # → desktop/build/Gadak-<version>-x64/
desktop/build-windows.ps1 --msix # + desktop/build/Gadak-<version>-windows-x64.msix (Windows host: makeappx)
```

`--msix` stages that same directory with `desktop/msix/AppxManifest.xml` and
the committed `desktop/msix/Assets/` logos (regenerated by `make brand`) and
runs `makeappx pack`. The manifest version is `(major+1).minor.patch.0`
because Partner Center refuses a 0.x major — see the comment in the script;
that mapping is a one-way door once the Store has accepted a package. The
output is unsigned on purpose: the Store re-signs with a Microsoft
certificate after certification, so the file is for Partner Center, not for
users ([GDK-1380]).

The Windows directory name uses the Windows pack labels (`x64`, `arm64`), not
the macOS dmg labels (`amd64`, `arm64`) or the AppImage / `uname -m` labels
(`x86_64`, `aarch64`). All three scripts stamp the version from the same
`version=` line in `desktop/build-app.sh` (`git describe --tags --always`).

`build-linux.sh` and `build-windows.ps1` exit 64 on usage / unknown arguments
and 69 when a required tool is missing. For Windows that is `go` (and `git`
when Git Bash is not on `PATH` to eval the `version=` line). `build-linux.sh`
when Git Bash is not on `PATH` to eval the `version=` line), plus
`makeappx.exe` from the Windows SDK when `--msix` is passed. `build-linux.sh`
also needs `pkg-config`, `cc`, `magick`/`convert`/`ffmpeg`, the `gtk4` and
`webkitgtk-6.0` pkg-config modules, and `appimagetool` when `--appimage` is
passed.
Expand Down Expand Up @@ -119,7 +130,12 @@ Windows machine in this repository (the authoring runner is darwin).

Required: Go, and `dist/app` from `npm run build` at the repo root. Git is
required only when `bash` is missing, so the script can run the same
`git describe --tags --always` formula as `desktop/build-app.sh`.
`git describe --tags --always` formula as `desktop/build-app.sh`. `--msix`
additionally needs `makeappx.exe` (Windows SDK, found under
`Windows Kits\10\bin\<newest>\x64` when not on `PATH`); the CI Windows job
also signs the result with a throwaway self-signed certificate and installs
it, which is the only check that the identity and full-trust declaration are
accepted — `makeappx` alone validates the schema.

### WebView2

Expand Down Expand Up @@ -270,4 +286,5 @@ would try to self-swap.
gone — wails v3 declares that framework itself.)

[GDK-211]: https://midagedev.github.io/gadak/backlog/#/?ks=GDK-211
[GDK-1380]: https://gadak.dev/backlog/#/?ks=GDK-1380
[GDK-293]: https://midagedev.github.io/gadak/backlog/#/?ks=GDK-293
106 changes: 102 additions & 4 deletions desktop/build-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@
# What still needs Windows is Authenticode signing (GDK-211) and any
# WebView2 runtime/bootstrap installer work; this script does neither.
#
# Usage: desktop/build-windows.ps1 [--arch x64|arm64] [--archive]
# Usage: desktop/build-windows.ps1 [--arch x64|arm64] [--archive] [--msix]
#
# Exit 64 = usage / unknown argument
# 69 = a required tool is missing
# 69 = a required tool is missing (go; makeappx.exe for --msix)
# 1 = dist/app missing or a build step failed
# 0 = portable directory written (and the zip, if --archive)
# 0 = portable directory written (and the zip / msix, if asked)
#
# --msix (GDK-1380) additionally packs the same directory into an UNSIGNED
# Gadak-<ver>-windows-<x64|arm64>.msix for Microsoft Store submission, from
# desktop/msix/AppxManifest.xml and the committed desktop/msix/Assets/. It
# needs makeappx.exe (Windows SDK), so it is a Windows-host output — the
# portable directory and zip still cross-compile from anywhere. The Store
# re-signs the package with a Microsoft certificate after certification;
# the file this script writes is not installable as-is and is never a
# GitHub Release asset.
#
# This script must not emit gadak-desktop-<os>-<arch>.zip — v0.14.0 apps
# match that name exactly and would self-swap (same rule as
Expand Down Expand Up @@ -44,7 +53,7 @@
$ErrorActionPreference = 'Stop'

function Usage {
[Console]::Error.WriteLine('usage: desktop/build-windows.ps1 [--arch x64|arm64] [--archive]')
[Console]::Error.WriteLine('usage: desktop/build-windows.ps1 [--arch x64|arm64] [--archive] [--msix]')
exit 64
}

Expand All @@ -57,12 +66,14 @@ function Need {
}

$wantArchive = $false
$wantMsix = $false
$archArg = $null
for ($i = 0; $i -lt $args.Count; $i++) {
$arg = [string]$args[$i]
switch ($arg) {
{ $_ -in @('-h', '--help') } { Usage }
{ $_ -in @('--archive', '-Archive', '-archive') } { $wantArchive = $true }
{ $_ -in @('--msix', '-Msix', '-msix') } { $wantMsix = $true }
{ $_ -in @('--arch', '-Arch', '-arch') } {
if ($i + 1 -ge $args.Count) {
[Console]::Error.WriteLine('build-windows: --arch needs x64 or arm64')
Expand Down Expand Up @@ -233,6 +244,93 @@ finally {

Write-Host "built $bundle ($version, $fileArch)"

if ($wantMsix) {
# Store package version: four numeric parts, the fourth reserved (0) and
# the first non-zero — Partner Center refuses a 0.x major. gadak is 0.x,
# so the manifest carries (major+1).minor.patch.0: 0.20.0 → 1.20.0.0,
# and a future 1.0.0 → 2.0.0.0 stays monotonic, which matters because
# the Store rejects any upload lower than one it has accepted. A
# git-describe suffix (0.20.0-33-gabc) is dropped. A stamp with no
# semver at all — 0.0.0-dev, or the bare hash a shallow CI checkout
# without tags produces — yields 1.0.0.0 so the pack can still be built
# and install-tested; the Store never sees one of those, the release
# job checks out with tags.
# ponytail: the +1 is a one-way door once the first package is accepted;
# never "fix" it back to the semver major.
function Get-MsixVersion {
param([string]$Stamp)
if ($Stamp -notmatch '^(\d+)\.(\d+)\.(\d+)') {
[Console]::Error.WriteLine("build-windows: no semver in stamp '$Stamp'; manifest version 1.0.0.0 (untagged build, not for the Store)")
return '1.0.0.0'
}
return ('{0}.{1}.{2}.0' -f ([int]$Matches[1] + 1), [int]$Matches[2], [int]$Matches[3])
}

# makeappx.exe ships in the Windows SDK, not on PATH; take the newest kit.
$makeappx = Get-Command makeappx.exe -ErrorAction SilentlyContinue
if ($null -eq $makeappx) {
$kits = ${env:ProgramFiles(x86)}
if ([string]::IsNullOrEmpty($kits)) { $kits = $env:ProgramFiles }
$candidates = @()
if (-not [string]::IsNullOrEmpty($kits)) {
$candidates = @(Get-ChildItem -Path (Join-Path $kits 'Windows Kits\10\bin') -Directory -Filter '10.*' -ErrorAction SilentlyContinue |
Sort-Object { [version]$_.Name } -Descending |
ForEach-Object { Join-Path $_.FullName 'x64\makeappx.exe' } |
Where-Object { Test-Path -LiteralPath $_ })
}
if ($candidates.Count -eq 0) {
[Console]::Error.WriteLine('build-windows: missing makeappx.exe (Windows SDK) — --msix needs a Windows host')
exit 69
}
$makeappx = Get-Command $candidates[0]
}

$msixDir = Join-Path $here 'msix'
$manifestSrc = Join-Path $msixDir 'AppxManifest.xml'
$assetsSrc = Join-Path $msixDir 'Assets'
foreach ($p in @($manifestSrc, $assetsSrc)) {
if (-not (Test-Path -LiteralPath $p)) {
[Console]::Error.WriteLine("build-windows: missing $p")
exit 1
}
}

# Stage = the portable directory + Assets + the substituted manifest.
# makeappx wants the manifest named exactly AppxManifest.xml at the root.
$stage = Join-Path $out ("msix-stage-{0}" -f $fileArch)
if (Test-Path -LiteralPath $stage) {
Remove-Item -LiteralPath $stage -Recurse -Force
}
New-Item -ItemType Directory -Path $stage | Out-Null
Copy-Item -Path (Join-Path $bundle '*') -Destination $stage -Recurse
Copy-Item -Path $assetsSrc -Destination (Join-Path $stage 'Assets') -Recurse
Remove-Item -LiteralPath (Join-Path $stage 'Assets\SOURCE.sha256') -Force -ErrorAction SilentlyContinue
$msixVersion = Get-MsixVersion -Stamp $verStamp
$manifest = Get-Content -LiteralPath $manifestSrc -Raw
$manifest = $manifest.Replace('@VERSION@', $msixVersion).Replace('@ARCH@', $fileArch)
if ($manifest -match '@[A-Z]+@') {
[Console]::Error.WriteLine("build-windows: unsubstituted placeholder in AppxManifest.xml: $($Matches[0])")
exit 1
}
# UTF-8 without BOM: makeappx reads the declaration, not a BOM.
[System.IO.File]::WriteAllText((Join-Path $stage 'AppxManifest.xml'), $manifest, (New-Object System.Text.UTF8Encoding $false))

$msixPath = Join-Path $out ('Gadak-{0}-windows-{1}.msix' -f $verStamp, $fileArch)
if (Test-Path -LiteralPath $msixPath) {
Remove-Item -LiteralPath $msixPath -Force
}
& $makeappx.Source pack /d $stage /p $msixPath /o
if ($LASTEXITCODE -ne 0) {
[Console]::Error.WriteLine("build-windows: makeappx pack exited $LASTEXITCODE")
exit 1
}
if (-not (Test-Path -LiteralPath $msixPath) -or ((Get-Item -LiteralPath $msixPath).Length -le 0)) {
[Console]::Error.WriteLine("build-windows: msix was not written: $msixPath")
exit 1
}
Write-Host "packed $msixPath (manifest version $msixVersion, unsigned — Store re-signs)"
}

if (-not $wantArchive) {
exit 0
}
Expand Down
Loading
Loading