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
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

env:
DOTNET_CLI_TELEMETRY_OPTOUT: "true"
DOTNET_NOLOGO: "true"

jobs:
release:
runs-on: windows-latest
timeout-minutes: 20

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Set up .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Validate release tag
shell: pwsh
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
$tag = $env:RELEASE_TAG
if ($tag -notmatch '^v(?<version>[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?)$') {
throw "Release tags must look like v1.2.3 or v1.2.3-preview.1; got '$tag'."
}
"RELEASE_VERSION=$($Matches.version)" >> $env:GITHUB_ENV

- name: Build, test, and package
shell: pwsh
run: ./scripts/create-local-release.ps1 -Version $env:RELEASE_VERSION

- name: Create GitHub release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
$archive = "releases/ClipDiff-$env:RELEASE_VERSION-win-x64.zip"
gh release create $env:RELEASE_TAG $archive --verify-tag --generate-notes --title "ClipDiff $env:RELEASE_TAG"
26 changes: 3 additions & 23 deletions .github/workflows/windows-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,14 @@ jobs:
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore ClipDiff.Windows.sln

- name: Build and test native Explorer extension
shell: pwsh
run: ./scripts/build-shell-extension.ps1 -Configuration Release -Test

- name: Test
run: dotnet test ClipDiff.Windows.sln --configuration Release --no-restore

- name: Build
run: dotnet build ClipDiff.Windows.sln --configuration Release --no-restore

- name: Publish portable Windows build
- name: Build, test, and package
shell: pwsh
run: |
dotnet publish src/ClipDiff.Windows/ClipDiff.Windows.csproj --configuration Release --runtime win-x64 --self-contained true --output artifacts/portable -p:PublishSingleFile=true -p:PublishTrimmed=false -p:IncludeNativeLibrariesForSelfExtract=true
if ($LASTEXITCODE -ne 0) { throw 'ClipDiff publish failed.' }
foreach ($file in @('ClipDiff.exe', 'ClipDiff.ShellExtension.dll')) {
if (-not (Test-Path "artifacts/portable/$file" -PathType Leaf)) { throw "Missing portable build file: $file" }
}
run: ./scripts/create-local-release.ps1

- name: Upload portable Windows build
uses: actions/upload-artifact@v7
with:
name: ClipDiff-win-x64
path: |
artifacts/portable/ClipDiff.exe
artifacts/portable/ClipDiff.ShellExtension.dll
path: releases/ClipDiff-*-win-x64/
if-no-files-found: error
retention-days: 14
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>1.0</Version>
<Version>1.0.0</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
65 changes: 57 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ ClipDiff does not guess whether text is sensitive from its length, punctuation,

Windows Server Core is not supported. The initial release target is `win-x64`.

## Install or update

Download the `ClipDiff-<version>-win-x64.zip` asset from the GitHub release and
extract the whole archive into its own directory. The complete application is
exactly these two files:

```text
ClipDiff.exe
ClipDiff.ShellExtension.dll
```

They must stay beside one another. Run `ClipDiff.exe`; the application manages
the Explorer integration per user. Do not copy the DLL to a Windows system
directory or register it manually.

ClipDiff is portable and has no installer. The directory may be anywhere the
current user can write, for example
`%LOCALAPPDATA%\Programs\ClipDiff\1.0.0`. To update, quit
ClipDiff, extract the new ZIP to a new directory, and run the new executable.
Using a new directory avoids trying to overwrite an older shell-extension DLL
that Explorer may still have loaded. Restarting Explorer or signing out may be
needed before the old directory can be deleted.

## Build and test

### Prerequisites on Windows
Expand Down Expand Up @@ -137,11 +160,11 @@ dotnet test ClipDiff.Windows.sln --configuration Release --no-restore
dotnet build ClipDiff.Windows.sln --configuration Release --no-restore
```

The framework-dependent executable is then at
The framework-dependent developer executable is then at
`src\ClipDiff.Windows\bin\Release\net10.0-windows10.0.17763.0\ClipDiff.exe`.
It requires the .NET 10 Windows Desktop Runtime on the computer where it runs.
For a portable executable that includes its runtime, use the local release script
below instead.
It is not the distributable package. For the self-contained, validated two-file
package, use the local release script below instead.

The pure `ClipDiff.Core` project and both policy test assemblies target ordinary `net10.0`, so they can run on macOS:

Expand All @@ -165,7 +188,11 @@ An older test launcher could print `elevated=0, elevationType=2` after dropping

If the native test run fails, include the full output from `Native build:` through the final `[FAIL]` line and script error. The output identifies the test case, expected visibility, direct-handler and Windows-aggregate results, HRESULT/Win32 errors, selection counts and Shell attributes, and effective COM/menu registration. A visibility mismatch also prints menu IDs, states, separator labels, and a read-only snapshot of Shell restriction settings and ClipDiff's Approved/Blocked entries. These probes do not change policy or read clipboard contents or selected file paths. Passing the direct-handler check but failing the Windows-aggregate check narrows the problem to Shell discovery/aggregation; it does not by itself establish that a work-machine policy is responsible. Microsoft's [Shell extension approval policy documentation](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-windowsexplorer#enforceshellextensionsecurity) explains one possible restriction.

GitHub Actions also builds and tests the native DLL, then tests and builds the .NET Release solution on `windows-latest` for every push and pull request. Each successful run provides a `ClipDiff-win-x64` artifact containing the executable and native DLL; extract both into the same directory. It can be run manually from the repository's **Actions** tab as well. Actual Explorer desktop interaction remains a separate manual check.
GitHub Actions runs the same release script on `windows-latest` for every push
and pull request. Each successful run provides a `ClipDiff-win-x64` artifact
containing only the executable and native DLL; extract both into the same
directory. It can be run manually from the repository's **Actions** tab as well.
Actual Explorer desktop interaction remains a separate manual check.

## Local release

Expand All @@ -175,7 +202,22 @@ On Windows PowerShell, from the repository root:
.\scripts\create-local-release.ps1
```

The script builds/tests the native extension and publishes a self-contained, untrimmed `win-x64` application to the gitignored `releases/win-x64` directory. Distribute **both `ClipDiff.exe` and `ClipDiff.ShellExtension.dll`** together; the .NET portion is bundled into the executable. Pass `-Launch` to start it after publishing. The personal build is unsigned, so Windows SmartScreen may warn before first launch.
The script builds and tests the native extension, runs the .NET tests, and
publishes a self-contained, untrimmed `win-x64` application. It stages the SDK
output in a clean private directory, then creates both of these gitignored
outputs:

```text
releases\ClipDiff-<version>-win-x64\
releases\ClipDiff-<version>-win-x64.zip
```

The directory and ZIP are validated to contain exactly `ClipDiff.exe` and
`ClipDiff.ShellExtension.dll`. Those are the complete release; keep them
together. PDB, `deps.json`, `runtimeconfig.json`, `bin`, `obj`, and the rest of
the raw publish directory are not release payload. Pass `-Launch` to start the
packaged executable after publishing. The personal build is unsigned, so
Windows SmartScreen may warn before first launch.

To skip the native Explorer integration tests for a local release, pass `-SkipNativeTests`:

Expand All @@ -185,7 +227,11 @@ To skip the native Explorer integration tests for a local release, pass `-SkipNa

This still builds and packages the native DLL and runs the .NET tests. Native tests run by default and remain enabled in CI. You can combine this switch with `-Launch`.

Quit ClipDiff before upgrading. Explorer may keep the DLL loaded; use a new release directory if Windows refuses to replace it, and restart Explorer or sign out if an old handler remains loaded. Starting the new version removes the obsolete two-file static menu registration automatically.
Quit ClipDiff before upgrading. The versioned package directory deliberately
makes side-by-side extraction straightforward because Explorer may keep the old
DLL loaded. Restart Explorer or sign out if the old handler remains loaded.
Starting the new version removes the obsolete two-file static menu registration
automatically.

If PowerShell reports that script execution is disabled, allow the checked-out
script for this process only, then run it again:
Expand All @@ -195,11 +241,14 @@ Set-ExecutionPolicy -Scope Process Bypass
.\scripts\create-local-release.ps1
```

The resulting portable executable is `releases\win-x64\ClipDiff.exe`; copy that
file to the work PC and run it without installing a separate .NET runtime.
Copy the resulting ZIP to the target PC and extract both files together. No
separate .NET runtime is required.

No installer, automatic updater, or code signing is included in the initial release.

Maintainer instructions for publishing a tagged GitHub release are in
[`RELEASING.md`](RELEASING.md).

## Windows verification checklist

On a Windows desktop, verify:
Expand Down
75 changes: 75 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Releasing ClipDiff

The release contract is the ZIP, not a `bin` or raw `dotnet publish` directory.
Every ClipDiff Windows x64 release contains exactly:

```text
ClipDiff.exe
ClipDiff.ShellExtension.dll
```

Both files go in the same directory. The executable is the self-contained .NET
application; the DLL is the native two-file Explorer context-menu handler. Do
not ship either file on its own.

## Build a release locally

On Windows, from a normal non-administrator PowerShell in the repository root:

```powershell
.\scripts\create-local-release.ps1
```

The script requires the .NET 10 SDK and Visual Studio C++ build tools with a
Windows SDK. It builds and tests the native extension, runs the .NET tests,
publishes the self-contained application in a clean staging directory, and
validates the final folder and ZIP. Output is:

```text
releases\ClipDiff-<version>-win-x64\
ClipDiff.exe
ClipDiff.ShellExtension.dll
releases\ClipDiff-<version>-win-x64.zip
```

The version defaults to the `Version` property in `Directory.Build.props`. A
specific version can be supplied when reproducing a tagged build:

```powershell
.\scripts\create-local-release.ps1 -Version 1.2.3
```

`-SkipNativeTests` skips only the native Shell integration tests; it still
builds and packages the DLL and runs the .NET tests. Do not use that switch for
a published release.

## Publish on GitHub

1. Merge the release changes into `main` and confirm **Windows build** succeeds.
2. Choose a semantic version such as `1.2.3`.
3. Create and push an annotated tag for that exact `main` commit:

```powershell
git tag -a v1.2.3 -m "ClipDiff v1.2.3"
git push origin v1.2.3
```

The **Release** workflow validates the tag, runs the complete release script,
and creates a GitHub release with generated notes and
`ClipDiff-1.2.3-win-x64.zip` attached. It will not publish if any build, test, or
payload validation fails.

## Install or update the portable build

Extract the whole ZIP into a directory owned by the current user and run
`ClipDiff.exe`. Do not move the DLL elsewhere or run `regsvr32`; ClipDiff owns
its per-user Explorer registration.

For an update, quit ClipDiff and extract the new version into a new directory.
This avoids overwriting a DLL that Explorer may still have loaded. Update any
startup shortcut to the new executable. The old directory can be removed after
Explorer releases its DLL, which may require restarting Explorer or signing
out.

Release binaries are currently unsigned, so Windows SmartScreen may warn on the
first launch.
7 changes: 5 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1317,9 +1317,10 @@ It should:

1. Build and test the native Explorer DLL with Visual Studio C++ build tools and the Windows SDK.
1. Run core tests.
1. Publish a Release build.
1. Place output in a gitignored `releases/` directory.
1. Publish a Release build through a clean private staging directory.
1. Place a versioned directory and ZIP in the gitignored `releases/` directory.
1. Place `ClipDiff.ShellExtension.dll` alongside `ClipDiff.exe`; keep the DLL outside the .NET single-file bundle.
1. Validate that the versioned directory and ZIP contain exactly `ClipDiff.exe` and `ClipDiff.ShellExtension.dll`, with no SDK by-products or stale files.
1. Optionally launch the resulting executable.

Suggested publish properties:
Expand All @@ -1336,6 +1337,8 @@ Do not enable trimming for the initial WPF build.

The native projects are built separately with `scripts/build-shell-extension.ps1 -Test`, so macOS can still compile and test the .NET solution. The release script runs this native build first. Distribute the executable and DLL together. Explorer can keep a loaded DLL locked after ClipDiff quits; updating into a new directory avoids overwriting a loaded DLL, and restarting Explorer or signing out may be needed to unload an older handler. Do not restart Explorer automatically.

Continuous integration must call the same release script rather than duplicate the publish commands. A semantic-version tag in the form `v1.2.3` should run the complete build and test process, then create a GitHub release containing the validated versioned ZIP.

For local releases, `create-local-release.ps1 -SkipNativeTests` may explicitly skip the native Explorer integration tests. It must still build and package the DLL and run the .NET tests. Native tests remain enabled by default and in CI.

A typical command is:
Expand Down
Loading