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
30 changes: 13 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
"### Downloads"
""
'- **Installer (recommended):** `ControlFS-Setup-x64.exe` — per-user install, no admin; updates itself automatically.'
'- **Portable:** `ControlFS-Portable-x64.zip` — unzip anywhere and run `ControlFS.exe`; updates are manual.'
'- **Portable:** `ControlFS-Portable-x64.exe` — a single executable; keeps its data in the `ControlFS_Data` folder next to it; updates are manual.'
'- **Integrity:** `SHA256SUMS.txt`; `release-manifest.json` + `.sig` are the signed update manifest the app verifies.'
'- Windows 11 x64. Not code-signed yet: Windows SmartScreen may warn (see the code signing policy).'
""
Expand All @@ -94,22 +94,18 @@ jobs:
shell: pwsh
run: .\build\Publish-ControlFS.ps1 -Runtime x64 -Target All -Version "${{ steps.version.outputs.version }}"

# Instala e desinstala em silêncio numa pasta temporária do runner: prova que o instalador funciona
# sem administrador, grava o marcador de atualização e remove os arquivos ao desinstalar.
- name: Smoke-test installer (silent install and uninstall)
# Abre o app de verdade antes de publicar: o .exe portátil e a versão instalada (instalar, abrir, desinstalar).
- name: Smoke-test portable exe (launch)
shell: pwsh
run: |
$dir = Join-Path $env:RUNNER_TEMP "controlfs-install"
$p = Start-Process -Wait -PassThru -FilePath dist\ControlFS-Setup-x64.exe -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/SP-',"/DIR=$dir"
if ($p.ExitCode -ne 0) { throw "Instalação falhou (código $($p.ExitCode))" }
foreach ($f in "ControlFS.exe", "ControlFS.installed", "SDL3.dll", "SharpCompress.dll", "unins000.exe") {
if (-not (Test-Path -LiteralPath (Join-Path $dir $f))) { throw "Instalação incompleta: falta $f" }
}
$u = Start-Process -Wait -PassThru -FilePath (Join-Path $dir "unins000.exe") -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART'
if ($u.ExitCode -ne 0) { throw "Desinstalação falhou (código $($u.ExitCode))" }
Start-Sleep -Seconds 3
if (Test-Path -LiteralPath (Join-Path $dir "ControlFS.exe")) { throw "Desinstalação deixou o executável" }
Write-Host "Instalação e desinstalação silenciosas OK"
$dir = Join-Path $env:RUNNER_TEMP "portable"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Copy-Item dist\ControlFS-Portable-x64.exe (Join-Path $dir "ControlFS.exe")
.\build\Test-Startup.ps1 -Exe (Join-Path $dir "ControlFS.exe") -OutDir smoke-portable -Seconds 40

- name: Smoke-test installer (install, launch, uninstall)
shell: pwsh
run: .\build\Test-Installed.ps1 -Setup dist\ControlFS-Setup-x64.exe -OutDir smoke-installed

# A chave privada existe só no secret UPDATE_SIGNING_KEY; o script confere a assinatura contra a chave
# pública embutida no app e falha se não bater. Sem manifesto assinado, o app não aceita a atualização.
Expand All @@ -125,7 +121,7 @@ jobs:
name: ControlFS-${{ steps.version.outputs.version }}
path: |
dist/ControlFS-Setup-x64.exe
dist/ControlFS-Portable-x64.zip
dist/ControlFS-Portable-x64.exe
dist/SHA256SUMS.txt
dist/release-manifest.json
dist/release-manifest.json.sig
Expand All @@ -143,7 +139,7 @@ jobs:
body_path: release-notes.md
files: |
dist/ControlFS-Setup-x64.exe
dist/ControlFS-Portable-x64.zip
dist/ControlFS-Portable-x64.exe
dist/SHA256SUMS.txt
dist/release-manifest.json
dist/release-manifest.json.sig
Expand Down
30 changes: 21 additions & 9 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ jobs:
path: smoke-released

source:
name: Portable from source
name: Launch portable exe and installed app
runs-on: windows-latest
timeout-minutes: 30
timeout-minutes: 40
env:
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_NOLOGO: "1"
Expand All @@ -52,16 +52,28 @@ jobs:
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Build portable
- name: Install Inno Setup (if missing)
shell: pwsh
run: .\build\Publish-ControlFS.ps1 -Runtime x64 -Target Portable -Version 0.0.0-smoke
- name: Launch and collect evidence
run: |
if (-not (Test-Path "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe")) { choco install innosetup -y --no-progress }
- name: Build portable exe and installer
shell: pwsh
run: .\build\Publish-ControlFS.ps1 -Runtime x64 -Target All -Version 0.0.0-smoke
- name: Launch portable exe
shell: pwsh
run: |
Expand-Archive dist\ControlFS-Portable-x64.zip -DestinationPath (Join-Path $env:RUNNER_TEMP portable)
.\build\Test-Startup.ps1 -Exe (Join-Path $env:RUNNER_TEMP "portable\ControlFS.exe") -OutDir smoke-source
$dir = Join-Path $env:RUNNER_TEMP "portable"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Copy-Item dist\ControlFS-Portable-x64.exe (Join-Path $dir "ControlFS.exe")
.\build\Test-Startup.ps1 -Exe (Join-Path $dir "ControlFS.exe") -OutDir smoke-portable -Seconds 40
- name: Install, launch and uninstall
if: always()
shell: pwsh
run: .\build\Test-Installed.ps1 -Setup dist\ControlFS-Setup-x64.exe -OutDir smoke-installed
- uses: actions/upload-artifact@v4
if: always()
with:
name: smoke-source
path: smoke-source
name: smoke-evidence
path: |
smoke-portable
smoke-installed
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ release-notes.md
Thumbs.db
Desktop.ini
*.suo

# Dados da versão portátil (preferências e logs ao lado do .exe)
ControlFS_Data/
13 changes: 13 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor

## [Unreleased]

## [0.1.0-alpha.3]
### Fixes
- The app closed right after opening (installed and portable) in 0.1.0-alpha.1 and 0.1.0-alpha.2: the app resource file (`ControlFS.pri`) was missing and WinUI couldn't find its styles. Fixed.

### What's new
- Single-file portable version, `ControlFS-Portable-x64.exe`: keeps settings and logs in the `ControlFS_Data` folder next to it (if that folder isn't writable, it uses `%LOCALAPPDATA%\ControlFS` and tells you).
- Local startup and crash log in `logs` inside the data folder, to diagnose problems (no passwords or file contents).
- Every release is now published only after CI actually opens the portable and the installed app on Windows and confirms the window shows up.

### Updating
- From 0.1.0-alpha.1 or 0.1.0-alpha.2: those versions didn't open, so they can't update themselves. Download and run `ControlFS-Setup-x64.exe` once.


## [0.1.0-alpha.2]
### What's new
- Windows installer (`ControlFS-Setup-x64.exe`): per-user install, no admin, Start menu shortcut and an optional desktop shortcut.
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH

## [Unreleased]

## [0.1.0-alpha.3]
### Correções
- O app fechava logo ao abrir (instalado e portátil) nas versões 0.1.0-alpha.1 e 0.1.0-alpha.2: faltava o arquivo de recursos do app (`ControlFS.pri`) e o WinUI não encontrava seus estilos. Corrigido.

### Novidades
- Versão portátil em um único arquivo, `ControlFS-Portable-x64.exe`: guarda preferências e logs na pasta `ControlFS_Data` ao lado dele (se a pasta não aceitar gravação, usa `%LOCALAPPDATA%\ControlFS` e avisa).
- Log local de inicialização e falhas em `logs` dentro da pasta de dados, para diagnosticar problemas (sem senhas nem conteúdo de arquivos).
- Cada release agora só é publicada depois que a CI abre de verdade o portátil e a versão instalada num Windows e confirma que a janela aparece.

### Atualizando
- Da 0.1.0-alpha.1 ou 0.1.0-alpha.2: essas versões não abriam, então não conseguem se atualizar sozinhas. Baixe e rode o `ControlFS-Setup-x64.exe` uma vez.


## [0.1.0-alpha.2]
### Novidades
- Instalador para Windows (`ControlFS-Setup-x64.exe`): instala por usuário, sem administrador, com atalho no menu Iniciar e opção de atalho na Área de Trabalho.
Expand Down
12 changes: 12 additions & 0 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ não foi aberto numa sessão Windows interativa**. Nenhum controle físico foi t
- **Configuração:** JSON versionado com gravação atômica e recuperação de arquivo corrompido.
- **Documentação e CI:** todos os documentos exigidos; ADRs 0001–0004; workflow de CI (não executado).

## Correção de inicialização (0.1.0-alpha.3)

- **As releases 0.1.0-alpha.1 e 0.1.0-alpha.2 não abriam** (instalado e portátil): o processo terminava em ~1 s com
`0xC000027B` em `Microsoft.UI.Xaml.dll`. Reproduzido pelo novo workflow `smoke.yml` num Windows do GitHub; o log
novo mostrou `Cannot locate resource from 'ms-appx:///Microsoft.UI.Xaml/Themes/themeresources.xaml'`. Causa: app sem
pacote MSIX precisa de `EnableMsixTooling=true` para gerar `ControlFS.pri`. Os testes anteriores cobriam a lógica,
mas nunca abriam o app — lacuna fechada: CI e release agora abrem o portátil e o instalado antes de publicar.
- Evidência pós-correção (runner `windows-latest`, Windows Server 2025 26100): portátil `.exe` único e app instalado
abrem, mantêm a janela "ControlFS" (40 s e 25 s), log até "Janela ativada", SDL 3.5.0 carregado; print mostra a
interface desenhada e o aviso de nova versão vindo do GitHub real. Instalar → abrir → desinstalar: OK.
- Portátil agora é `ControlFS-Portable-x64.exe` (arquivo único) com dados em `ControlFS_Data` ao lado.

## Instalador e atualizações (0.1.0-alpha.2)

- Instalador Inno Setup por usuário, sem administrador (`build/ControlFS.iss`), com marcador que habilita atualização.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ A **file manager for Windows made for the controller**, with a **built-in extrac

## Download

Get **[0.1.0-alpha.2](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.2)** (pre-release):
Get **[0.1.0-alpha.3](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.3)** (pre-release):

- **`ControlFS-Setup-x64.exe`** (recommended): per-user install, no admin, **updates itself automatically** (verified, signed updates).
- **`ControlFS-Portable-x64.zip`**: unzip anywhere and run `ControlFS.exe`; it tells you about new versions, replacing it is manual.
- **`ControlFS-Portable-x64.exe`**: a single executable that keeps its data in the `ControlFS_Data` folder next to it; it tells you about new versions, replacing it is manual.

Windows 11 x64. Not code-signed yet, so SmartScreen may warn ([policy](docs/CODE_SIGNING.md)). On 0.1.0-alpha.1? Run the installer once; updates are automatic from then on.
Windows 11 x64. Not code-signed yet, so SmartScreen may warn ([policy](docs/CODE_SIGNING.md)). On 0.1.0-alpha.1 or alpha.2? Those didn't open: run the installer once; updates are automatic from then on.

## How it works

Expand Down
6 changes: 3 additions & 3 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ Um **gerenciador de arquivos para Windows feito para o controle**, com **extrato

## Download

Baixe a **[0.1.0-alpha.2](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.2)** (pré-lançamento):
Baixe a **[0.1.0-alpha.3](https://github.com/nextestudios/ControlFS/releases/tag/v0.1.0-alpha.3)** (pré-lançamento):

- **`ControlFS-Setup-x64.exe`** (recomendado): instala por usuário, sem admin, e **se atualiza sozinho** (atualizações assinadas e verificadas).
- **`ControlFS-Portable-x64.zip`**: descompacte em qualquer pasta e rode o `ControlFS.exe`; ele avisa de novas versões, a troca é manual.
- **`ControlFS-Portable-x64.exe`**: um único executável que guarda os dados na pasta `ControlFS_Data` ao lado dele; avisa de novas versões, a troca é manual.

Windows 11 x64. Ainda sem assinatura de código, então o SmartScreen pode avisar ([política](docs/CODE_SIGNING.md)). Está na 0.1.0-alpha.1? Rode o instalador uma vez; daí em diante as atualizações são automáticas.
Windows 11 x64. Ainda sem assinatura de código, então o SmartScreen pode avisar ([política](docs/CODE_SIGNING.md)). Está na 0.1.0-alpha.1 ou alpha.2? Elas não abriam: rode o instalador uma vez; daí em diante as atualizações são automáticas.

## Como funciona

Expand Down
2 changes: 1 addition & 1 deletion build/New-ReleaseManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ $manifest = [ordered]@{
version = $Version
releasedAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
installer = Get-Asset "ControlFS-Setup-x64.exe"
portable = Get-Asset "ControlFS-Portable-x64.zip"
portable = Get-Asset "ControlFS-Portable-x64.exe"
}
$json = $manifest | ConvertTo-Json -Compress -Depth 4
$bytes = [Text.UTF8Encoding]::new($false).GetBytes($json)
Expand Down
59 changes: 37 additions & 22 deletions build/Publish-ControlFS.ps1
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<#
.SYNOPSIS
Publica o ControlFS (win-x64, self-contained) e gera em dist/:
ControlFS-Setup-x64.exe (instalador por usuário), ControlFS-Portable-x64.zip e SHA256SUMS.txt.
Publica o ControlFS (WinUI 3, sem pacote MSIX, self-contained) e gera em dist/:
ControlFS-Portable-x64.exe um único .exe portátil (dados em ControlFS_Data ao lado dele)
ControlFS-Setup-x64.exe instalador por usuário (Inno Setup; dados em %LOCALAPPDATA%\ControlFS)
SHA256SUMS.txt
.EXAMPLE
.\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.2
.\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.2 -Target Portable
.\build\Publish-ControlFS.ps1 -Version 0.1.0-alpha.3
#>
param(
[ValidateSet("x64")] [string] $Runtime = "x64",
Expand All @@ -15,39 +16,53 @@ $ErrorActionPreference = "Stop"
if ($Version -notmatch '^\d+\.\d+\.\d+(-[0-9A-Za-z.\-]+)?$') { throw "Versão inválida: $Version" }

$root = Split-Path -Parent $PSScriptRoot
$out = Join-Path $root "artifacts/publish-$Runtime"
$project = Join-Path $root "src/ControlFS.App/ControlFS.App.csproj"
$singleDir = Join-Path $root "artifacts/publish-$Runtime-single"
$appDir = Join-Path $root "artifacts/publish-$Runtime-app"
$dist = Join-Path $root "dist"
Remove-Item -LiteralPath $out, $dist -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $singleDir, $appDir, $dist -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $dist | Out-Null

# O RID vem do próprio ControlFS.App.csproj: passar -r aqui propagaria o RID às bibliotecas e quebraria o restore travado.
# Versão numérica para o assembly (sem sufixo de pré-release).
$numeric = ($Version -split '-')[0]
dotnet publish (Join-Path $root "src/ControlFS.App/ControlFS.App.csproj") -c Release --self-contained true `
-p:Platform=$Runtime -p:Version=$Version -p:AssemblyVersion="$numeric.0" -p:FileVersion="$numeric.0" `
-p:DebugType=none -p:DebugSymbols=false -o $out --nologo
if ($LASTEXITCODE -ne 0) { throw "dotnet publish falhou" }

foreach ($required in "ControlFS.exe", "SDL3.dll", "SharpCompress.dll") {
if (-not (Test-Path -LiteralPath (Join-Path $out $required))) { throw "Pacote incompleto: falta $required" }
function Invoke-Publish([string]$OutDir, [bool]$SingleFile) {
# O RID vem do próprio ControlFS.App.csproj: passar -r aqui propagaria o RID às bibliotecas e quebraria o restore travado.
# PublishSingleFile acrescenta implicitamente Microsoft.NET.ILLink.Tasks (do próprio SDK), que os lock files não
# listam. Só nesta publicação (feita por último) o restore não é travado; as versões continuam fixadas em
# Directory.Packages.props, o instalador é publicado antes em modo travado e o build da CI segue travado.
$pubArgs = [System.Collections.Generic.List[string]]@(
"publish", $project, "-c", "Release", "--self-contained", "true",
"-p:Platform=$Runtime", "-p:PublishSingleFile=$($SingleFile.ToString().ToLowerInvariant())",
"-p:Version=$Version", "-p:AssemblyVersion=$numeric.0", "-p:FileVersion=$numeric.0",
"-o", $OutDir, "--nologo")
if ($SingleFile) { $pubArgs.Add("-p:RestoreLockedMode=false") }
& dotnet @pubArgs
if ($LASTEXITCODE -ne 0) { throw "dotnet publish falhou ($OutDir)" }
if (-not (Test-Path -LiteralPath (Join-Path $OutDir "ControlFS.exe"))) { throw "Publish não gerou ControlFS.exe em $OutDir" }
}
Copy-Item -LiteralPath (Join-Path $root "LICENSE"), (Join-Path $root "THIRD_PARTY_NOTICES.md") -Destination $out

$files = @()
if ($Target -in "All", "Portable") {
$zip = Join-Path $dist "ControlFS-Portable-$Runtime.zip"
Compress-Archive -Path (Join-Path $out "*") -DestinationPath $zip
$files += $zip
}
if ($Target -in "All", "Installer") {
Write-Host "Publicando versão para o instalador (pasta)..."
Invoke-Publish $appDir $false
foreach ($required in "ControlFS.exe", "ControlFS.pri", "SDL3.dll", "SharpCompress.dll") {
if (-not (Test-Path -LiteralPath (Join-Path $appDir $required))) { throw "Pacote incompleto: falta $required" }
}
Copy-Item -LiteralPath (Join-Path $root "LICENSE"), (Join-Path $root "THIRD_PARTY_NOTICES.md") -Destination $appDir
$iscc = @("${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", "$env:ProgramFiles\Inno Setup 6\ISCC.exe", "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe") |
Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
if (-not $iscc) { throw "Inno Setup 6 (ISCC.exe) não encontrado." }
& $iscc /Q "/DAppVersion=$Version" "/DNumericVersion=$numeric.0" "/DSourceDir=$out" "/DOutputDir=$dist" (Join-Path $PSScriptRoot "ControlFS.iss")
& $iscc /Q "/DAppVersion=$Version" "/DNumericVersion=$numeric.0" "/DSourceDir=$appDir" "/DOutputDir=$dist" (Join-Path $PSScriptRoot "ControlFS.iss")
if ($LASTEXITCODE -ne 0) { throw "ISCC falhou" }
$files += Join-Path $dist "ControlFS-Setup-$Runtime.exe"
}

if ($Target -in "All", "Portable") {
Write-Host "Publicando versão portátil (um único .exe)..."
Invoke-Publish $singleDir $true
$portable = Join-Path $dist "ControlFS-Portable-$Runtime.exe"
Copy-Item -LiteralPath (Join-Path $singleDir "ControlFS.exe") -Destination $portable
$files += $portable
}
# LF explícito: "sha256sum -c" / "shasum -c" falham com CRLF no nome do arquivo.
$lines = foreach ($f in $files) { "$((Get-FileHash -LiteralPath $f -Algorithm SHA256).Hash.ToLowerInvariant()) $(Split-Path -Leaf $f)" }
[IO.File]::WriteAllText((Join-Path $dist "SHA256SUMS.txt"), (($lines -join "`n") + "`n"), [Text.Encoding]::ASCII)
Expand Down
24 changes: 24 additions & 0 deletions build/Test-Installed.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<#
.SYNOPSIS
Instala o ControlFS em silêncio numa pasta temporária, confere os arquivos, abre o app instalado
(Test-Startup.ps1) e desinstala. Falha se qualquer etapa falhar. Usado pela CI.
#>
param(
[Parameter(Mandatory = $true)] [string] $Setup,
[Parameter(Mandatory = $true)] [string] $OutDir
)
$ErrorActionPreference = "Stop"
$dir = Join-Path $env:RUNNER_TEMP "controlfs-install"
$p = Start-Process -Wait -PassThru -FilePath $Setup -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-', "/DIR=$dir"
if ($p.ExitCode -ne 0) { throw "Instalação falhou (código $($p.ExitCode))" }
foreach ($f in "ControlFS.exe", "ControlFS.pri", "ControlFS.installed", "SDL3.dll", "SharpCompress.dll", "unins000.exe") {
if (-not (Test-Path -LiteralPath (Join-Path $dir $f))) { throw "Instalação incompleta: falta $f" }
}
& (Join-Path $PSScriptRoot "Test-Startup.ps1") -Exe (Join-Path $dir "ControlFS.exe") -OutDir $OutDir
$startupOk = $LASTEXITCODE -eq 0
$u = Start-Process -Wait -PassThru -FilePath (Join-Path $dir "unins000.exe") -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART'
if ($u.ExitCode -ne 0) { throw "Desinstalação falhou (código $($u.ExitCode))" }
Start-Sleep -Seconds 3
if (Test-Path -LiteralPath (Join-Path $dir "ControlFS.exe")) { throw "Desinstalação deixou o executável" }
if (-not $startupOk) { throw "O app instalado não abriu" }
Write-Host "Instalar, abrir e desinstalar: OK"
Loading
Loading