feat(ci): conditional SignPath signing step for the Windows binary#42
feat(ci): conditional SignPath signing step for the Windows binary#42omarsy wants to merge 1 commit into
Conversation
Implements workstream 2 of the greffer-windows-support epic (``docs/features/greffer-windows-support/hld.md``). The Windows binary has been publishing unsigned since workstream 1 landed (PR #39) — operators downloading via ``iwr | iex`` hit Defender SmartScreen on first run, with no path to fix it short of the right-click → Properties → Unblock dance. This adds three steps between the existing "Rename binary with target suffix" and "Upload binary artifact (Windows)" steps: 1. **Pre-signing upload** — uploads the unsigned ``.exe`` as a short-lived input artifact (``greffer-${target}-unsigned-input``, 1-day retention) so SignPath has an artifact-id to download. 2. **Submit signing request to SignPath** — ``signpath/github-action-submit-signing-request@v1.2``. Signs under the chosen policy, writes the signed binary back to ``cli/dist/``. The HLD picks SignPath because it's free for AGPL/OSS projects, API-driven (no HSM management), and issues an OV cert under the Greffon org name. 3. **Verify signed binary** — ``Get-AuthenticodeSignature`` check that fails the job loudly if the signed file is missing or has a non-Valid signature status. Without this, a silent SignPath failure would publish an unsigned binary indistinguishably. All three steps are gated on ``env.SIGNPATH_API_TOKEN != ''``. Reasoning: - During the gap between this landing and the SignPath account being live, every step skips cleanly. The pre-existing unsigned- binary upload still runs, so behaviour is unchanged from today. - The moment ``SIGNPATH_API_TOKEN`` and ``SIGNPATH_ORG_ID`` are added to repo secrets, the next ``cli-v*`` tag automatically produces a signed binary. No code change needed at go-live. GitHub Actions disallows ``secrets.X`` in ``if:`` expressions for security; the env-indirection ``env: SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}`` then ``if: env.SIGNPATH_API_TOKEN != ''`` is the workaround. Next steps (operator-side, not engineering): 1. Register the ``greffer-cli`` project on SignPath OSS community plan (free for AGPL). 2. Configure the ``release-signing`` policy and the ``greffer-exe`` artifact configuration as referenced in the step's ``with:`` block. 3. Add ``SIGNPATH_API_TOKEN`` + ``SIGNPATH_ORG_ID`` to repo secrets (Settings → Secrets and variables → Actions). 4. Cut the next ``cli-v*`` tag. The install.ps1 banner already mentions a SmartScreen "warm-up period" — that text is currently misleading because there is no signing at all yet. After SignPath is live and the first signed binary ships, the banner becomes accurate. Operator-facing copy fix is a separate small PR on landing-page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4cd0cf4fe2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| env: | ||
| SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} | ||
| run: | | ||
| $exe = "cli/dist/greffer-${{ matrix.target }}.exe" |
There was a problem hiding this comment.
Fix signed-binary path in PowerShell verification step
The build job sets defaults.run.working-directory: cli, so this run step executes from .../cli, but it checks for cli/dist/..., which resolves to .../cli/cli/dist/.... When SignPath is enabled, the file lookup will fail even if signing succeeded, causing the Windows build to error and blocking release publication.
Useful? React with 👍 / 👎.
| SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: greffer-${{ matrix.target }}-unsigned-input |
There was a problem hiding this comment.
Exclude unsigned input artifact from release download pattern
This artifact name now matches the publish job’s pattern: greffer-*, so the pre-sign unsigned artifact is downloaded alongside the final Windows artifact. Because actions/download-artifact@v4 is used with merge-multiple: true, identical filenames collide and one wins last-write, which can let the unsigned greffer-windows-x86_64.exe overwrite the signed one before checksums and release upload.
Useful? React with 👍 / 👎.
Deferred per product framework (greffon-product 6-question test, Q6 fail)Postponing this PR per a formal pass of the The code is reviewed and ready. Converting to draft so it doesn't sit in the merge queue, but holds its place for the moment a revival trigger fires. Revival triggers (any one):
Companion banner-honesty fix at greffon/landing-page#14 ships independently — it's required by the marketing-honesty framework regardless of when signing lands. Decision logged in greffon/greffon docs/product/decision-log.md (separate PR). |
Summary
Implements workstream 2 of the `greffer-windows-support` epic. Adds three new CI steps between the rename and upload of the Windows binary:
All three are gated on `env.SIGNPATH_API_TOKEN != ''`. The workflow no-ops cleanly today; the moment SignPath secrets are added to repo settings, the next `cli-v*` tag produces a signed binary automatically.
Why now
The Windows binary has been publishing unsigned since PR #39 (workstream 1) landed — `iwr | iex` operators hit Defender SmartScreen on first run with no remediation short of the right-click → Properties → Unblock dance. Workstream 3 (`install.ps1` + landing-page route) is also already live, so today every Windows operator who follows the documented install path hits the warning. This PR closes that gap without requiring the SignPath account to exist yet.
Operator-side steps (your-move, not engineering)
Before this can do anything, the following one-time setup is needed:
Notes
Test plan
🤖 Generated with Claude Code