Skip to content
Open
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
6 changes: 5 additions & 1 deletion .github/BRANCH_PROTECTION_RULESETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This repository uses two GitHub branch protection rulesets.
1. **Default branch (main)**
- Target: branch name `main`.
- Require a pull request, stale review dismissal, and resolved review threads (self-approval allowed; no mandatory external approval).
- Require status checks: **Analyze (python)**, **Unit tests (3.11)**, **Unit tests (3.12)**, **Compile + help smoke (macos-latest, 3.11)**, **Compile + help smoke (windows-latest, 3.11)**, **No build artifacts tracked**.
- Require status checks: **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**.
- Require linear history.
- Block force pushes and branch deletion.
- Bypass: none configured in rulesets.
Expand All @@ -34,6 +34,8 @@ This repository uses two GitHub branch protection rulesets.
- **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**

## Optional: apply via API
Expand All @@ -48,6 +50,8 @@ CONTEXTS='[
{"context":"Unit tests (3.12)"},
{"context":"Compile + help smoke (macos-latest, 3.11)"},
{"context":"Compile + help smoke (windows-latest, 3.11)"},
{"context":"Windows unit tests"},
{"context":"Windows MSI smoke"},
{"context":"No build artifacts tracked"}
]'

Expand Down
2 changes: 2 additions & 0 deletions .github/ruleset-main.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
{ "context": "Unit tests (3.12)" },
{ "context": "Compile + help smoke (macos-latest, 3.11)" },
{ "context": "Compile + help smoke (windows-latest, 3.11)" },
{ "context": "Windows unit tests" },
{ "context": "Windows MSI smoke" },
{ "context": "No build artifacts tracked" }
]
}
Expand Down
2 changes: 2 additions & 0 deletions .github/ruleset-release.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
{ "context": "Unit tests (3.12)" },
{ "context": "Compile + help smoke (macos-latest, 3.11)" },
{ "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 @@ -80,17 +86,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
94 changes: 94 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,101 @@
- name: Help smoke
run: python -m dataplicity_cli --help

windows-unit-tests:
name: Windows unit tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[test]"
- name: Run unit tests
run: pytest -q --maxfail=1

windows-package-smoke:
Comment on lines +52 to +66
name: Windows MSI smoke
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- 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: |
$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:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: No build artifacts tracked
runs-on: ubuntu-latest
steps:
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 @@ -34,7 +34,7 @@ jobs:
fi

- 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.
9 changes: 5 additions & 4 deletions build/windows/DataplicityCLI.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,24 @@
<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
Loading
Loading