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
12 changes: 12 additions & 0 deletions .github/BRANCH_PROTECTION_RULESETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ This repository uses GitHub repository rulesets. Public contributors may open pu

`.github/ruleset-tags.json` protects `v*` tags from deletion and force-updates.

## Required status check names

- Use check names exactly as they appear on pull requests. In this repo, required checks are:
- **Analyze (python)**
- **Unit tests (3.11)**
- **Unit tests (3.12)**
- **Compile + help smoke (macos-latest, 3.11)**
- **Compile + help smoke (windows-latest, 3.11)**
- **Windows unit tests**
- **Windows MSI smoke**
- **No build artifacts tracked**

## Apply or refresh via API

```bash
Expand Down
6 changes: 6 additions & 0 deletions .github/ruleset-main.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
{
"context": "Compile + help smoke (windows-latest, 3.11)"
},
{
"context": "Windows unit tests"
},
{
"context": "Windows MSI smoke"
},
{
"context": "No build artifacts tracked"
}
Expand Down
6 changes: 6 additions & 0 deletions .github/ruleset-release.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
{
"context": "Compile + help smoke (windows-latest, 3.11)"
},
{
"context": "Windows unit tests"
},
{
"context": "Windows MSI smoke"
},
{
"context": "No build artifacts tracked"
}
Expand Down
54 changes: 51 additions & 3 deletions .github/workflows/bootstrap-winget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name: Bootstrap WinGet package

"on":
workflow_dispatch:
inputs:
release_tag:
description: "Published release tag to submit (for example v0.1.7)"
required: true
default: "v0.1.7"
type: string

permissions:
contents: read
Expand Down Expand Up @@ -84,17 +90,59 @@ jobs:
$ErrorActionPreference = "Stop"
Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe -UseBasicParsing

- name: Prepare manifest from published release
id: prepare
shell: pwsh
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
$ErrorActionPreference = "Stop"
if ($env:RELEASE_TAG -notmatch '^v(\d+\.\d+\.\d+(?:\.\d+)?)$') {
throw "Release tag must use vX.Y.Z or vX.Y.Z.W."
}
$version = $Matches[1]
$headers = @{
Authorization = "Bearer $env:WINGET_TOKEN"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}
$release = Invoke-RestMethod `
-Headers $headers `
-Uri "https://api.github.com/repos/wildfoundry/dataplicity-cli/releases/tags/$env:RELEASE_TAG"
$assetName = "dataplicity-cli-$version-windows-x64.msi"
$asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
if (-not $asset) {
throw "Release $env:RELEASE_TAG does not contain $assetName."
}
$downloadPath = Join-Path $env:RUNNER_TEMP $assetName
Invoke-WebRequest $asset.browser_download_url -OutFile $downloadPath -UseBasicParsing
$sha256 = (Get-FileHash $downloadPath -Algorithm SHA256).Hash
$releaseDate = ([DateTime]$release.published_at).ToString("yyyy-MM-dd")
$outputRoot = Join-Path $env:RUNNER_TEMP "winget-manifests"
$manifestOutput = python build/winget/prepare_manifest.py `
--source-dir "build/winget/manifests/w/Wildfoundry/DataplicityCLI/0.1.6" `
--output-root $outputRoot `
--version $version `
--installer-url $asset.browser_download_url `
--installer-sha256 $sha256 `
--release-date $releaseDate
$manifestPath = ($manifestOutput | Select-Object -Last 1).Trim()
"manifest_path=$manifestPath" >> $env:GITHUB_OUTPUT
"package_version=$version" >> $env:GITHUB_OUTPUT

- name: Submit initial manifest
shell: pwsh
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
MANIFEST_PATH: ${{ steps.prepare.outputs.manifest_path }}
PACKAGE_VERSION: ${{ steps.prepare.outputs.package_version }}
run: |
$ErrorActionPreference = "Stop"
$manifestPath = "build/winget/manifests/w/Wildfoundry/DataplicityCLI/0.1.6"
$prTitle = "New package: Wildfoundry.DataplicityCLI version 0.1.6"
$prTitle = "New package: Wildfoundry.DataplicityCLI version $env:PACKAGE_VERSION"

./wingetcreate.exe submit `
--token $env:WINGET_TOKEN `
--prtitle $prTitle `
--no-open `
$manifestPath
$env:MANIFEST_PATH
79 changes: 72 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,86 @@ jobs:
python -m pip install --upgrade pip
pip install -e ".[test]"
- name: Run unit tests
# remote_access imports termios, which is not available on Windows.
run: pytest -q --maxfail=1 --ignore=tests/test_remote_access_helpers.py --ignore=tests/test_remote_access_single_command.py
run: pytest -q --maxfail=1

windows-msi-smoke:
name: Windows MSI smoke
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Validate WiX source and MSI build script
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install . pyinstaller
choco install wixtoolset -y
- name: Build executable and MSI
shell: pwsh
run: |
$env:VERSION = python build/get_version.py
pyinstaller --noconfirm --clean --onefile --name dataplicity `
--workpath pyinstaller-build --distpath pyinstaller-dist `
dataplicity_cli/__main__.py
./pyinstaller-dist/dataplicity.exe --version
./pyinstaller-dist/dataplicity.exe --help
./build/windows/build_msi.ps1 -Version $env:VERSION
"MSI_PATH=$((Resolve-Path "dist/dataplicity-cli-$env:VERSION-windows-x64.msi").Path)" >> $env:GITHUB_ENV
- name: Install, verify, and uninstall MSI
shell: pwsh
run: |
if (!(Test-Path "build/windows/DataplicityCLI.wxs")) { throw "missing wxs" }
if (!(Test-Path "build/windows/build_msi.ps1")) { throw "missing build_msi.ps1" }
$null = [xml](Get-Content "build/windows/DataplicityCLI.wxs")
Write-Host "OK: WiX source parses"
$ErrorActionPreference = "Stop"
$installDir = Join-Path $env:ProgramFiles "Dataplicity\Dataplicity CLI"
$installedExe = Join-Path $installDir "dataplicity.exe"
$installed = $false
try {
$install = Start-Process msiexec.exe `
-ArgumentList @("/i", "`"$env:MSI_PATH`"", "/qn", "/norestart") `
-Wait -PassThru
if ($install.ExitCode -ne 0) {
throw "MSI install failed with exit code $($install.ExitCode)."
}
$installed = $true
if (!(Test-Path $installedExe)) {
throw "Installed executable not found at $installedExe."
}
& $installedExe --version
if ($LASTEXITCODE -ne 0) {
throw "Installed executable --version failed."
}
& $installedExe --help
if ($LASTEXITCODE -ne 0) {
throw "Installed executable --help failed."
}
$machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
$expectedPathEntry = $installDir -replace '[\\/]+$', ''
$machinePathEntries = $machinePath -split ";" | ForEach-Object {
$_.Trim().Trim('"') -replace '[\\/]+$', ''
}
if ($machinePathEntries -notcontains $expectedPathEntry) {
throw "Installer did not add $installDir to the machine PATH. Machine PATH: $machinePath"
}
} finally {
if ($installed) {
$uninstall = Start-Process msiexec.exe `
-ArgumentList @("/x", "`"$env:MSI_PATH`"", "/qn", "/norestart") `
-Wait -PassThru
if ($uninstall.ExitCode -ne 0) {
throw "MSI uninstall failed with exit code $($uninstall.ExitCode)."
}
}
}
if (Test-Path $installedExe) {
throw "Installed executable remains after uninstall."
}
$machinePathAfterUninstall = [Environment]::GetEnvironmentVariable("Path", "Machine")
$pathEntriesAfterUninstall = $machinePathAfterUninstall -split ";" | ForEach-Object {
$_.Trim().Trim('"') -replace '[\\/]+$', ''
}
if ($pathEntriesAfterUninstall -contains $expectedPathEntry) {
throw "Installer PATH entry remains after uninstall: $installDir"
}

no-artifacts-tracked:
name: No build artifacts tracked
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ jobs:
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Verify Windows executable signature
if: runner.os == 'Windows'
shell: pwsh
run: |
$signature = Get-AuthenticodeSignature "pyinstaller-dist/dataplicity.exe"
if ($signature.Status -ne "Valid") {
throw "Executable signature is $($signature.Status): $($signature.StatusMessage)"
}
Write-Host "Executable signed by $($signature.SignerCertificate.Subject)"

- name: Build macOS tarball (for Homebrew)
if: runner.os == 'macOS'
shell: bash
Expand Down Expand Up @@ -136,6 +146,20 @@ jobs:
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Verify Windows MSI signature
if: runner.os == 'Windows'
shell: pwsh
run: |
$msi = Get-ChildItem "dist/*.msi" | Select-Object -First 1
if (-not $msi) {
throw "No MSI found to verify."
}
$signature = Get-AuthenticodeSignature $msi.FullName
if ($signature.Status -ne "Valid") {
throw "MSI signature is $($signature.Status): $($signature.StatusMessage)"
}
Write-Host "MSI signed by $($signature.SignerCertificate.Subject)"

- name: Stage Windows release artifacts
if: runner.os == 'Windows'
shell: pwsh
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-winget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
scripts/verify-release-bot-token.sh WINGET_TOKEN "${EXPECTED_BOT}"

- name: Publish to WinGet
uses: vedantmgoyal9/winget-releaser@main
uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5
with:
identifier: Wildfoundry.DataplicityCLI
release-tag: ${{ github.event.release.tag_name || inputs.release_tag }}
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,26 @@ dataplicity --help

### Windows (no Python required)

Download the latest `.msi` from [GitHub Releases](https://github.com/wildfoundry/dataplicity-cli/releases) and install it. It installs `dataplicity.exe` and adds it to `PATH`.
Install the signed x64 MSI from WinGet in PowerShell or Windows Terminal:

```
```powershell
winget install --id Wildfoundry.DataplicityCLI --exact
dataplicity --help
```

WinGet handles future upgrades and uninstall:

```powershell
winget upgrade --id Wildfoundry.DataplicityCLI --exact
winget uninstall --id Wildfoundry.DataplicityCLI --exact
```

The installer is machine-wide and may request administrator approval. If
WinGet is unavailable, download the latest signed `.msi` from
[GitHub Releases](https://github.com/wildfoundry/dataplicity-cli/releases).
Both install paths add `dataplicity.exe` to `PATH`; open a new terminal after
installation.

### Python (developer install)

If you do have Python available and prefer `pipx`:
Expand Down Expand Up @@ -211,3 +225,4 @@ dataplicity --install-completion zsh
- The `Update WinGet package` workflow publishes new `.msi` releases to WinGet using `Wildfoundry.DataplicityCLI`.
- Configure a repository secret named `WINGET_TOKEN` (classic PAT with `public_repo`) and ensure your account has a fork of `microsoft/winget-pkgs`.
- WinGet automation updates existing manifests; if this package is not yet in WinGet, submit the first manifest for the current release, then subsequent releases are automated.
- Follow [`docs/windows-release.md`](docs/windows-release.md) before tagging a Windows release or submitting its first WinGet manifest.
11 changes: 6 additions & 5 deletions build/windows/DataplicityCLI.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,32 @@
<Product
Id="*"
Name="Dataplicity CLI"
Manufacturer="Dataplicity"
Manufacturer="Wildfoundry Ltd"
Version="$(var.ProductVersion)"
Language="1033"
UpgradeCode="B3C5A6C2-3D42-4E6B-9E2E-4F7B3D3C2A10">

<Package
InstallerVersion="500"
Compressed="yes"
InstallScope="perMachine" />
InstallScope="perMachine"
Platform="x64" />

<MajorUpgrade DowngradeErrorMessage="A newer version of Dataplicity CLI is already installed." />
<MediaTemplate />
<MediaTemplate EmbedCab="yes" />

<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />

<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="ProgramFiles64Folder">
<Directory Id="ManufacturerFolder" Name="Dataplicity">
<Directory Id="INSTALLFOLDER" Name="Dataplicity CLI" />
</Directory>
</Directory>
</Directory>

<DirectoryRef Id="INSTALLFOLDER">
<Component Id="MainExecutable" Guid="B2C7E7C3-9A4C-4C7E-A2E6-8DDB0F3A1E1B">
<Component Id="MainExecutable" Guid="B2C7E7C3-9A4C-4C7E-A2E6-8DDB0F3A1E1B" Win64="yes">
<File Id="DataplicityExe" Source="dist\dataplicity.exe" KeyPath="yes" />
<Environment
Id="PathEntry"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Installers:
InstallerSha256: 399CB0A4415F01AC22252F3F533199ACFF2287EFBC8E7C197C65F0A54EF0365B
AppsAndFeaturesEntries:
- DisplayName: Dataplicity CLI
Publisher: Dataplicity
Publisher: Wildfoundry Ltd
UpgradeCode: '{B3C5A6C2-3D42-4E6B-9E2E-4F7B3D3C2A10}'
ManifestType: installer
ManifestVersion: 1.9.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
PackageIdentifier: Wildfoundry.DataplicityCLI
PackageVersion: 0.1.6
PackageLocale: en-US
Publisher: Dataplicity
Publisher: Wildfoundry Ltd
PublisherUrl: https://github.com/wildfoundry
PublisherSupportUrl: https://github.com/wildfoundry/dataplicity-cli/issues
PackageName: Dataplicity CLI
Expand Down
Loading
Loading