feat(releases): ship MSI and reuse exact-source CI proof #40

Merged
KofTwentyTwo merged 2 commits from feat/msi-installers into main 2026-10-13 14:04:33 +00:00
11 changed files with 154 additions and 69 deletions

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
// Copyright (c) 2026 James Maes (KofTwentyTwo)
// SPDX-License-Identifier: MIT
"use strict";
/** Requires genuine passing CI on the exact complete tree merged for this release. */
async function run({ github, context, core })
{
const repository = context.repo;
const { data: source } = await github.rest.repos.getCommit({ ...repository, ref: context.sha });
if(!source.commit.verification.verified)
{
throw new Error("The release commit signature is not verified.");
}
const pulls = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit,
{ ...repository, commit_sha: context.sha, per_page: 100 });
const merged = pulls.filter(pull => pull.merged_at && pull.state === "closed"
&& pull.base.ref === "main" && pull.merge_commit_sha === context.sha);
if(merged.length !== 1)
{
throw new Error("Expected one merged main PR for the tagged release commit.");
}
const pull = merged[0];
const { data: tested } = await github.rest.repos.getCommit({ ...repository, ref: pull.head.sha });
if(tested.commit.tree.sha !== source.commit.tree.sha)
{
throw new Error("The release tree differs from the tested PR tree.");
}
const { data: rules } = await github.request("GET /repos/{owner}/{repo}/rules/branches/{branch}",
{ ...repository, branch: "main" });
// Personal acknowledgement is enforced at merge; technical validation is reused here.
const required = rules.filter(rule => rule.type === "required_status_checks")
.flatMap(rule => rule.parameters.required_status_checks)
.filter(check => check.context !== "review / automated");
if(required.length === 0)
{
throw new Error("No required technical checks were found.");
}
const checks = await github.paginate(github.rest.checks.listForRef,
{ ...repository, ref: pull.head.sha, per_page: 100 });
for(const requirement of required)
{
const records = checks.filter(check => check.name === requirement.context
&& check.app?.id === (requirement.integration_id ?? 15368));
records.sort((left, right) => right.id - left.id);
if(records[0]?.status !== "completed" || records[0]?.conclusion !== "success")
{
throw new Error(`Required tested-source check is not successful: ${requirement.context}`);
}
}
core.info(`Reused ${required.length} genuine passing checks from PR #${pull.number} at ${pull.head.sha}; exact release tree ${source.commit.tree.sha}.`);
return { head: pull.head.sha, tree: source.commit.tree.sha, checks: required.length };
}
module.exports = { run };
@@ -0,0 +1,43 @@
// Copyright (c) 2026 James Maes (KofTwentyTwo)
// SPDX-License-Identifier: MIT
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { run } = require("./assert-release-source.cjs");
/** Exercises release-tree, check-provider and latest-result boundaries. */
test("reuses only verified identical-source passing CI", async () =>
{
for(const failure of [null, "signature", "tree", "merge", "provider", "pending", "failed", "missing", "latest", "rules"])
{
const check = { id: 1, name: "ci / ui-tests", app: { id: failure === "provider" ? 7 : 15368 },
status: failure === "pending" ? "in_progress" : "completed",
conclusion: failure === "failed" ? "failure" : "success" };
const github = {
rest: {
repos: { getCommit: async ({ ref }) => ({ data: { commit: {
verification: { verified: failure !== "signature" },
tree: { sha: failure === "tree" && ref === "head" ? "other" : "tree" },
} } }), listPullRequestsAssociatedWithCommit: "pulls" },
checks: { listForRef: "checks" },
},
request: async () => ({ data: failure === "rules" ? [] : [{ type: "required_status_checks",
parameters: { required_status_checks: [{ context: "ci / ui-tests", integration_id: 15368 }, { context: "review / automated" }] } }] }),
paginate: async method => method === "pulls" ? (failure === "merge" ? [] : [{ number: 1, state: "closed",
merged_at: "today", base: { ref: "main" }, merge_commit_sha: "release", head: { sha: "head" } }])
: failure === "missing" ? [] : failure === "latest" ? [check, { ...check, id: 2, conclusion: "failure" }] : [check],
};
const operation = run({ github, context: { repo: { owner: "owner", repo: "repo" }, sha: "release" }, core: { info: () =>
{} } });
if(failure)
{
await assert.rejects(operation);
}
else
{
assert.deepEqual(await operation, { head: "head", tree: "tree", checks: 1 });
}
}
});
+11 -33
View File
@@ -38,6 +38,8 @@ jobs:
environment: release
permissions:
contents: read
checks: read
pull-requests: read
steps:
- name: Checkout main and tag history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -61,41 +63,16 @@ jobs:
ARTIFACT_SIGNING_SUBJECT: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
run: ./build/Assert-ReleaseSigning.ps1
gates:
needs: verify
uses: ./.github/workflows/ci.yml
permissions:
contents: read
security:
needs: verify
uses: ./.github/workflows/security.yml
permissions:
contents: read
security-events: write
codeql:
needs: verify
uses: ./.github/workflows/codeql.yml
permissions:
contents: read
security-events: write
scripts:
needs: verify
uses: ./.github/workflows/scripts.yml
permissions:
contents: read
fuzzing:
needs: verify
uses: ./.github/workflows/fuzz.yml
permissions:
contents: read
- name: Require passing CI for the exact merged release tree
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { run } = require('./.github/scripts/assert-release-source.cjs');
await run({ github, context, core });
release:
needs: [verify, gates, security, codeql, scripts, fuzzing]
uses: KofTwentyTwo/standards/.github/workflows/release-velopack.yml@083d90326a8f3b750d840b6180e00870ee1342f5 # v1.1.2
needs: verify
uses: KofTwentyTwo/standards/.github/workflows/release-velopack.yml@70b555bd5658273090769991bfc4c9b6681dde18 # v1.2.1
permissions:
contents: write # create and publish the GitHub release
id-token: write # sign attestations; Azure OIDC when signing
@@ -113,6 +90,7 @@ jobs:
test-projects: |
tests/MarkdownViewr.Windows.Core.Tests
sign: true
msi: true
signing-files: |
app/MarkdownViewr.Windows.exe
app/MarkdownViewr.Windows.dll
+1 -1
View File
@@ -54,7 +54,7 @@
commit as "+<sha>" on AssemblyInformationalVersion, which the About dialog and the
CLI's version output show as a short hash. -->
<PropertyGroup>
<VersionPrefix>1.0.3</VersionPrefix>
<VersionPrefix>1.0.4</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
</PropertyGroup>
+2 -2
View File
@@ -31,8 +31,8 @@ Windows packages support **Windows 10 version 1809 or later and Windows 11, x64*
| Edition | Choose it for | Package |
| --- | --- | --- |
| Stable | Everyday reading | [Download EXE installer](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/latest/download/markdownviewr-windows-stable-Setup.exe) |
| Development preview | Trying upcoming changes | [Download dev EXE installer](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/download/v1.0.2-beta.1/markdownviewr-windows-dev-dev-Setup.exe) |
| Stable | Everyday reading | [Download EXE installer](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/latest/download/markdownviewr-windows-stable-Setup.exe) · [MSI installer](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/latest/download/markdownviewr-windows-stable.msi) |
| Development preview | Trying upcoming changes | [Download dev EXE installer](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/download/v1.0.3-beta.1/markdownviewr-windows-dev-dev-Setup.exe) · [MSI installer](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/download/v1.0.3-beta.1/markdownviewr-windows-dev-dev.msi) |
| Portable | Running without installation | [Download portable ZIP](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/latest/download/markdownviewr-windows-stable-Portable.zip) |
Download a package from [Releases](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases). Run Setup for a per-user installation without administrator rights, or extract the portable ZIP before launching the app.
+21 -22
View File
@@ -4,31 +4,30 @@ Updated: 2026-10-13
## Release source and verification
Application behavior remains the owner-reviewed PR37 source at main4c0ccc7.
Actual focused30, native111, published111 and installed stable/dev110 reports
have zero failures/skips. Unchanged-source main validation38325540268 attempt2
also passes published111 and installed stable/dev110 each. Update/restart,
channel coexistence, registration and preservation of defaults are verified.
Stable1.0.2 and dev1.0.2-beta.1 EXE/ZIP releases are public and immutable at
source18d8ae950cab445672d216d387a956b964d2c6c1, builder083d903. Actual downloaded
signatures/publisher/trust/timestamps, package payloads, checksums, CycloneDX
SBOM attestations, source/builder provenance and updater feeds verify.
Production runs38392743207 and38392743312 succeeded. Failed older refs remain.
The signing caller pins standards1.1.2 at
083d90326a8f3b750d840b6180e00870ee1342f5. Personally approved PR45 corrects the
GitHub pwsh bootstrap wrapper; all hosted signing checks pass. Caller NuGet
mapping, Azure trust scopes and strict publisher/signature gates are unchanged.
No new Azure administration is required.
The maintainer renewed the MSI request on2026-10-13; earlier MSI deferral is
superseded. Prepare signed per-user MSI alongside EXE/ZIP for stable/dev1.0.3.
MSI payload verification and focused native install/launch/uninstall must pass
before publication. Source behaviors, dependencies and Azure trust are unchanged.
Preserve the failed1.0.0 and1.0.1 stable/dev tags. New candidates are
1.0.2/stable and1.0.2-beta.1/dev. Production status and exact shipped-artifact
proof belong in [release checklist](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/issues/33)
and [GitHub Releases](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases).
Require successful signing and verification of exact downloaded bytes before
claiming a signed public release.
Prevent duplicate release test passes by requiring the exact complete Git tree
of a genuinely merged main PR whose latest required technical checks all passed
under their required provider identities. Reject unverified source signatures,
different trees, missing/failed/pending checks and API failures. Reuse real CI
proof; do not post fabricated results or weaken branch protection. Fresh release
build/core coverage, signing and installer/distribution checks remain required.
The maintainer authorized release without further human-review requests on
2026-10-13. Record this as an explicit release-specific waiver, never as a claim
that an unseen change was personally read. Automated review and technical CI
remain required. The next development prefix is1.0.3. EXE/ZIP publication comes
first; MSI is tracked for the following release in issue36. OpenSSF15417 remains
70%, not passing. Preserve original owner previews/profiles and unrelated work.
The owner explicitly authorized finishing releases without further human-review
requests. Record the release-specific waiver truthfully; restore the original
manual-review requirement immediately after each authorized merge. The next
development prefix is1.0.4. OpenSSF15417 remains70%, not passing. Preserve owner
previews/profiles and unrelated worktrees. Track actual publication/proof in
[release checklist](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/issues/33).
## Historical release snapshots
+6 -10
View File
@@ -2,16 +2,12 @@
## Release and follow-up work
- [x] Merge reviewed PR37 at4c0ccc7; focused30/native111/published111/stable110/dev110 pass with no failures/skips.
- [x] Merge personally approved signing-wrapper PR45 and metadata-only standards1.1.2 proposal; pin083d903 without changing NuGet mapping or Azure trust scopes.
- [x] Preserve failed1.0.0 and1.0.1 stable/dev tags and prepare new1.0.2 candidates.
- [x] Prepare direct download links, portable-update guidance and developer verification links.
- [x] Set next development VersionPrefix to1.0.3.
- [ ] Verify published standards1.1.2 bundle hashes/provenance and exact builder bytes.
- [ ] Pass final exact-head CI, record automated review and the maintainer's explicit release-specific review waiver, and merge normally.
- [ ] Publish signed stable/dev EXE installers and portable ZIPs; verify exact shipped signatures, publisher, trust, timestamps, checksums, SBOMs and provenance (#33).
- Deliver signed, installed-acceptance-tested MSI installers next release (#36).
- Reduce repeated validation while preserving trust and complete acceptance (#38).
- [x] Publish/verify signed stable1.0.2 and dev1.0.2-beta.1 EXE/ZIP releases (#33).
- [x] Prepare exact-tree/provider-bound reuse of genuine source CI; boundary tests and real17-check source proof pass (#38).
- [ ] Publish the native MSI builder, pin it and enable signed MSI in the caller (#36).
- [ ] Pass exact-head CI, record the existing release waiver, merge and restore all original branch checks.
- [ ] Publish1.0.3 stable/dev MSI+EXE+ZIP; verify actual MSI install/launch/uninstall and all final payload signatures, hashes, SBOMs and provenance.
- [x] Set next development prefix to1.0.4 and prepare user download links.
- Complete remaining OpenSSF evidence; saved70% is not a passing badge (#31).
## Prior release checklist
+4
View File
@@ -426,3 +426,7 @@ actual parsers. Independent properties detect unsafe acceptance as well as crash
Synthetic inputs never execute scripts, start native processes or touch real profiles.
The record in [assurance.json](assurance.json) does not claim coverage-guided
instrumentation, Best Practices enrollment or verified production signatures.
## Release validation reuse
Release tags must be on main and have a verified commit signature. Reused source CI must belong to the genuinely merged main PR, match the entire tagged Git tree and have successful latest required technical checks from the configured provider identities. Missing, stale, failed or pending proof fails publication. This avoids repeating identical UI tests; it does not trust arbitrary PR artifacts or remove branch checks. Fresh build/coverage, Azure signing, MSI installation and exact distributed-payload verification remain release gates.
+4
View File
@@ -75,3 +75,7 @@ Documents are rendered locally. Authored scripts and remote images are blocked;
- **The app does not start:** confirm Microsoft Edge WebView2 Runtime is installed and inspect the activity logs if available.
For help, use [Discussions](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/discussions). Report reproducible problems through [Issues](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/issues/new/choose), including **Help → About** version, Windows version, and relevant sanitized logs. Report vulnerabilities privately using [SECURITY.md](../SECURITY.md).
## MSI installation
The MSI installs for the current Windows user, alongside the EXE and portable download options. Stable and dev have separate package identities. Use the same installer format for an existing edition; install the other channel separately to try previews. Uninstall an MSI installation through Windows Settings → Apps → Installed apps. The installer registers supported Markdown types and preserves your existing default choices.
+1 -1
View File
@@ -7,7 +7,7 @@
},
"scripts": {
"lint": "eslint .github/scripts eslint.config.mjs --max-warnings 0",
"test": "node --test .github/scripts/review-gate.test.cjs"
"test": "node --test .github/scripts/review-gate.test.cjs .github/scripts/assert-release-source.test.cjs"
},
"devDependencies": {
"@eslint/js": "10.0.1",
@@ -150,6 +150,12 @@ public partial class AppTests
CloseSettings(session);
InvokeDocumentMenu(session, sibling, "SourceToggle", menuName: "View");
AssertSavedPrintingDefaults(session, options);
session.WaitFor(() =>
{
string settings = session.ReadSettings();
using var saved = JsonDocument.Parse(settings);
return saved.RootElement.GetProperty("ShowSource").GetBoolean() ? sibling : null;
}, "the sibling source toggle to finish saving before the print-cancel snapshot");
string savedSettings = session.ReadSettings();
InvokeDocumentMenu(session, primary, "PrintMenuItem");
AssertPrintOptionControls(session, options);