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

# The macOS DMG is built and attached by a maintainer rather than by CI, so this
# workflow is deliberately manual too: it never runs on a tag push or on a
# published GitHub release, and it does not attach anything unless explicitly
# asked to.

on:
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to build (for example v0.1.74)"
required: true
type: string
publish:
description: "Attach the built asset to that tag's GitHub release"
required: false
type: boolean
default: false
allow_unsigned_publish:
description: "Allow attaching a clearly labeled UNSIGNED development artifact"
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
package:
name: "Build and (optionally) attach the Windows artifact"
runs-on: windows-2022
permissions:
contents: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: compnerd/gha-setup-swift@397094e75494a93fa8d81db0268dbc8f5d6cf7c6 # v0.4.1
with:
swift-version: swift-6.3.3-release
swift-build: 6.3.3-RELEASE
cache: true

- name: Bootstrap exact Windows dependencies
shell: pwsh
run: ./Tools/windows/bootstrap.ps1 -ToolRoot .ci-tools -ProviderRoot .ci-providers

- name: Stage the Swift daemon and CLI for packaging
shell: pwsh
run: ./Tools/windows/stage-swift-products.ps1

- name: Mask the declared signing thumbprint
shell: pwsh
env:
GRAPHCODE_SIGNING_THUMBPRINT: ${{ secrets.WINDOWS_SIGNING_THUMBPRINT }}
run: |
if ($env:GRAPHCODE_SIGNING_THUMBPRINT) {
Write-Host "::add-mask::$env:GRAPHCODE_SIGNING_THUMBPRINT"
}

# With none of the signing secrets configured this step still succeeds and
# produces graphcode-windows-x86_64-unsigned.zip, which release.ps1 will
# not attach to a release unless allow_unsigned_publish was requested.
- name: Package the Windows release artifact
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_PUBLISH: ${{ inputs.publish }}
RELEASE_ALLOW_UNSIGNED_PUBLISH: ${{ inputs.allow_unsigned_publish }}
GRAPHCODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE }}
GRAPHCODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
GRAPHCODE_SIGNING_THUMBPRINT: ${{ secrets.WINDOWS_SIGNING_THUMBPRINT }}
GRAPHCODE_SIGNING_TIMESTAMP_URL: ${{ secrets.WINDOWS_SIGNING_TIMESTAMP_URL }}
run: |
$arguments = @(
"-Tag", $env:RELEASE_TAG,
"-OutputDirectory", ".build/windows/release-publish")
if ($env:RELEASE_PUBLISH -eq "true") { $arguments += "-Publish" }
if ($env:RELEASE_ALLOW_UNSIGNED_PUBLISH -eq "true") { $arguments += "-AllowUnsignedPublish" }
./Tools/windows/release.ps1 @arguments

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: graphcode-windows-x86_64
path: .build/windows/release-publish/publish/
if-no-files-found: warn
76 changes: 76 additions & 0 deletions Tools/windows/PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,82 @@ production publisher trust. `Packaging.Scheduler.Tests.ps1` exercises an owned
idle task through native stop/delete and verifies the actual missing-task
HRESULT without suppressing account or permission errors.

## Publishing a Windows release

`Tools/windows/release.ps1` is the only supported path from `package.ps1` to a
GitHub release asset, and `.github/workflows/windows-release.yml` is the only
thing that runs it in CI.

Like the macOS DMG — which a maintainer builds locally with `make release-dmg`
and attaches with `gh release create` — Windows publication is deliberately
manual. The workflow has a single `workflow_dispatch` trigger and never fires on
a tag push or on a published release. Its inputs are the existing release `tag`,
`publish` (default `false`), and `allow_unsigned_publish` (default `false`). The
built artifact is always uploaded as a workflow artifact, so a build can be
inspected without touching any release.

```powershell
pwsh -NoProfile -File Tools\windows\release.ps1 -Tag v1.2.3
pwsh -NoProfile -File Tools\windows\release.ps1 -Tag v1.2.3 -Publish
```

The tag supplies the package version (`v1.2.3` and `1.2.3-beta1` are accepted;
`dev` versions and anything that is not a release version are refused before
anything is built). `release.ps1` then builds through `package.ps1`, re-runs
`package.ps1 -Command Verify` — pinned to the expected publisher when signing —
reads the built package's own `metadata.json`, and **refuses to continue if the
package's declared signing state contradicts what the run actually did**.

Locally, `Tools\windows\stage-swift-products.ps1` produces the Swift half of the
release inputs (`graphcoded.exe`, `graphcode.exe`, and the Swift runtime DLLs in
`.build\windows\release`); `package.ps1` builds the versioned shell itself from
the pinned providers.

### Asset names

| Build | Asset | Publishable |
|---|---|---|
| signed | `graphcode-windows-x86_64.zip` + `.sha256` | yes |
| unsigned | `graphcode-windows-x86_64-unsigned.zip` + `.sha256` | only with `allow_unsigned_publish` |

The signed name is versionless, matching `graphcode-macos-arm64.dmg`, so
`releases/latest/download/graphcode-windows-x86_64.zip` resolves. An unsigned
development build can never occupy that name.

### Secrets that enable signing

Set these as repository secrets (**Settings → Secrets and variables → Actions →
New repository secret**). No certificate material is stored in this repository.

| Secret | Format | Required |
|---|---|---|
| `WINDOWS_SIGNING_CERTIFICATE` | Base64 text of a PFX holding the code-signing certificate **and** its private key (`[Convert]::ToBase64String([IO.File]::ReadAllBytes('signing.pfx'))`) | yes |
| `WINDOWS_SIGNING_CERTIFICATE_PASSWORD` | That PFX's password | yes |
| `WINDOWS_SIGNING_THUMBPRINT` | The certificate's SHA-1 thumbprint, exactly 40 hexadecimal characters (`package.ps1` validates `^[0-9a-fA-F]{40}$`) | yes |
| `WINDOWS_SIGNING_TIMESTAMP_URL` | RFC 3161 timestamp service `https://` URL | recommended |

The PFX is required because release jobs run on ephemeral GitHub-hosted runners,
which have no certificate store to pre-provision. The workflow imports it into
`Cert:\CurrentUser\My`, **requires the imported certificate's own thumbprint to
equal `WINDOWS_SIGNING_THUMBPRINT`**, passes only that thumbprint to
`package.ps1 -SignCertificate`, and removes both the imported certificate and the
decoded PFX before the job ends. A mismatched or malformed thumbprint aborts
before anything is built.

With **none** of the signing secrets configured the workflow still succeeds and
produces the unsigned development artifact described above. Supplying only
*some* of them is a hard failure rather than a silent downgrade to unsigned, so a
misconfigured secret can never be mistaken for a signed release.

`Release.Tests.ps1` covers tag resolution, unsigned labeling and its publication
gate, incomplete signing material, thumbprint pinning and certificate cleanup,
the signing-state honesty gate, build/upload failure propagation, and the
workflow's own triggers, action pinning, and input defaults. It runs first under
`validate.ps1 -Task packaging` and needs no certificate, network access, or real
build; its signed path uses an ephemeral self-signed certificate that it removes
again. Publishing plumbing is not production-signing evidence: no certificate
exists yet, and no Windows asset has been published.

## Retained provider sources

Both exact public provider pins have the annotated source-retention tag
Expand Down
Loading
Loading