Post-1.0 hardening: deps, release/CodeQL gates, engine containment and clone recovery, account consistency, NuGet update workflow #51

Merged
KofTwentyTwo merged 10 commits from dev into main 2026-10-09 11:22:49 +00:00
10 Commits
Author SHA1 Message Date
James MaesandClaude Fable 5.1 53a5e0f1f5 CI: exempt file-licensed NuGet packages from the dependency license check
Dependency review on the dev -> main PR failed on licenses only (no
vulnerabilities): LibGit2Sharp, Microsoft.WindowsAppSDK and
Microsoft.Windows.SDK.BuildTools declare their license as a file in the
nuspec, so the dependency graph reports "unknown"/NOASSERTION and any version
bump fails the gate. All three have been dependencies since the first commit;
their licenses (MIT, Microsoft Software License Terms, Windows SDK license)
are recorded next to the exemption. Vulnerability scanning is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 06:15:47 -05:00
James MaesandClaude Fable 5.1 eca5fefda3 Release: make the UI smoke suite advisory for tags, matching main (#44)
The rebuilt release chain called ci.yml wholesale, which made the FlaUI UI
end-to-end job a hard release gate while it is deliberately NOT a required
check on main (hosted-runner UI automation is still proving itself). A flaky
UI run that would never block a merge could therefore block an immutable tag.

ci.yml gains a workflow_call input, ui-tests-advisory (default false), and
the ui-tests job sets continue-on-error from it; push/PR runs are unchanged.
The release workflow passes true, so build, coverage and format still block
publishing and a red UI job is visible but not fatal. Documented in
release.yml and docs/RELEASING.md as the explicit exception #44 allows; flip
the input (and add the job to main's required checks) once the suite has
earned it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 06:08:57 -05:00
James MaesandClaude Fable 5.1 c37e411c0e CI: replace Dependabot's NuGet updates with a workflow that can see WinUI TFMs
Dependabot's NuGet updater cannot evaluate Windows-versioned target frameworks
(dependabot/dependabot-core#13923, open since January). On this repo it logged
every newer package as "not compatible. Incompatible project frameworks:
net10.0-windows10.0.19041", and because it updates a package consistently
across projects, the same packages were blocked in the plain net10.0 test
projects too. Net effect: WindowsAppSDK, Velopack, xunit.runner.visualstudio
and Microsoft.NET.Test.Sdk were never offered, and the one PR it did open was
closed by the bot itself.

- .github/scripts/Update-NuGetPackages.ps1 drives the bump from
  `dotnet list package --outdated --format json` (real MSBuild/NuGet
  evaluation): applies minor/patch bumps by exact textual edit of each
  PackageReference (encoding and line endings preserved), holds major bumps
  and prereleases for review, and writes a Markdown report. Verified on a
  copy of the repo with four packages downgraded: three minor/patch bumps
  applied, the major held, and the edited csproj byte-identical to HEAD.
- .github/workflows/nuget-updates.yml runs it weekly (Mondays 06:00 ET) or on
  demand (with an include-major switch), runs the build/test/format gates
  against the bumped tree, and opens or refreshes one PR on deps/nuget-weekly
  against dev with the report and gate results in the body. The third-party
  create-pull-request action is pinned to a commit SHA. With GITHUB_TOKEN the
  PR gets no checks of its own (GitHub does not chain workflows from it); an
  optional DEPS_PAT secret enables that.
- dependabot.yml keeps only the github-actions ecosystem and explains why.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:58:15 -05:00
James MaesandClaude Fable 5.1 bf902d1575 Accounts: make metadata and token-vault updates consistent under failure (#43)
Save and Delete touched two stores (accounts.json and the Credential Manager
vault) with no defined order or compensation: a vault failure after the
metadata write left an account with no usable credential, and a vault failure
after a metadata delete orphaned the PAT.

Both operations now have a fixed order and a compensating step:

- Save with a token: vault write FIRST, then metadata. A vault failure changes
  nothing (Store either replaces the entry or leaves the previous one intact,
  so an update keeps the credential that worked). A metadata failure restores
  the previous token, or deletes the new entry for a brand-new account.
- Delete: vault delete first, then metadata. A vault failure changes nothing.
  A metadata failure stores the removed token again, so a listed account never
  silently loses its credential and no token is left without its account.
- In-memory state changes only after both stores succeeded.
- When compensation itself fails, the new AccountConsistencyException states
  the exact leftover state and how a plain retry reconciles it (Save overwrites
  the vault entry and rewrites the file; Delete tolerates an absent entry), and
  the activity log records the same. Neither ever contains a token.

ITokenVault.Store's contract now states the leave-unchanged-on-failure rule the
store relies on; CredentialManagerVault already behaves that way (CredWriteW is
an atomic replace) and InMemoryVault trivially does.

Tests (AccountsStoreConsistencyTests, with a faultable vault and a recording
log): vault-write failure for new and updated accounts, metadata-write failure
with and without a token, compensation failure for save and delete, vault-delete
failure, idempotent retry after every leftover state, and no token in any
exception or log message. Coverage gate stays 100%.

Closes #43

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:58:15 -05:00
James MaesandClaude Fable 5.1 c2be6f4f4c Engine: never report an incomplete clone as up to date (#46)
A clone that failed or was killed after its fetch but before/during checkout
could leave a libgit-valid .git with a partial working tree whenever the
best-effort cleanup could not delete the directory (open handle, antivirus,
permissions). The next run treated it as an existing repository, saw equal
commit tips, and returned "already up to date".

The existing gclo.checkoutpending local-config marker (previously set only for
Windows-invalid-path clones) is now set the moment the fetch lands, before
anything is checked out, and cleared only after checkout completes. It is
therefore durable evidence of an incomplete clone regardless of what
interrupted it or whether cleanup succeeded. FetchAndPull already routes
marked repositories through CompletePendingCheckout, which re-validates the
tip's paths on Windows and force-checks-out the whole tree, so a marked repo is
repaired rather than trusted. Empty-origin clones clear the marker so the
unborn-HEAD path keeps handling them. Recovery metadata (.git/gclo-recovery.json)
is checked first and is unaffected.

Tests: a finished clone and an empty-origin clone leave the marker cleared; a
marked repo with a partial tree and EQUAL tips is checked out instead of
reported up to date; the same with the remote ahead materializes the new tip;
a marked repo whose tip gained a Windows-invalid path fails with the typed
exception and keeps the marker.

Closes #46

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:43:33 -05:00
James MaesandClaude Fable 5.1 dd78fedf7b Engine: validate repository names and contain every target under TargetRoot (#47)
The public list overload of OrgSyncEngine.SyncAsync combined caller-supplied
RepoDescriptor.Name values with TargetRoot via Path.Combine and went straight to
clone/pull. A rooted name, a traversal (../x), or a name with separators could
operate outside the root the caller asked for. GitHub's own names are safe, but
gclo.Engine ships on NuGet.

New public RepositoryPathResolver:
- TryValidateName: rejects blank, '.', '..', any directory separator (covers
  slash-rooted and UNC), ':' on every platform (drive-relative forms such as
  'C:' and 'C:repo'), and file-system-invalid or control characters.
- Resolve: normalizes root and candidate with Path.GetFullPath and requires the
  candidate to sit strictly inside the root, catching anything the textual
  checks miss (e.g. '...' which Windows normalization trims onto the root).

The engine resolves every target before it creates TargetRoot, reports Queued,
or calls the git client, so a bad name fails the whole call with an
ArgumentException naming the repository and nothing on disk is touched. The
git client now receives the normalized absolute folder.

Tests: resolver unit tests (valid GitHub-style names, every rejection class,
both separator styles, drive-rooted, UNC, containment backstop, relative and
messy roots) and engine tests proving rejection happens before any side
effect, for both the list and the lister overloads. Coverage gate stays 100%.

Closes #47

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:43:32 -05:00
James MaesandClaude Fable 5.1 a81de9ec25 Release: require tags on main and gate publishing on the full CI workflow (#44)
The tag-triggered release accepted any v* tag and ran only a build plus the
Engine tests, so an immutable public release could be cut from a commit that
never passed main's gates. The workflow is now three jobs, each needing the
previous:

- verify: strict-semver tag check (unchanged) plus `git merge-base
  --is-ancestor <tag> origin/main`; a tag off main fails here and nothing
  downstream runs (push a v* tag on an unmerged commit to see it).
- gates: calls ci.yml (new workflow_call trigger) so a release passes exactly
  what a PR does: zero-warning x64 build, 100% line coverage on
  Engine/ViewModels/Cli, the FlaUI UI smoke suite, and the format gate. The
  UI suite is not yet a required check on main PRs; it is required here.
- release: the existing publish steps, reading version/channel from the
  verify job's outputs. contents: write is now scoped to this job only.

Verified: all three workflows parse; the ancestry check was run locally and
passes for v1.0.0 and origin/main and fails for the unmerged dev head.
docs/RELEASING.md describes the three stages.

Closes #44

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:35:12 -05:00
James MaesandClaude Fable 5.1 0bc425268a CI: run CodeQL on dev pushes and pull requests (#45)
Feature PRs target dev, but CodeQL only watched main, so code reached the
integration branch unanalyzed. Scan pushes and PRs on both branches; the
weekly scheduled scan is unchanged. CONTRIBUTING's check table now says so.

Closes #45

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:35:12 -05:00
James MaesandClaude Fable 5.1 6cf5f8be03 Deps: Microsoft.WindowsAppSDK 2.2.0 -> 2.5.1
Three minor WinUI releases. Verified locally with a clean x64 unpackaged build
(warning-free) and the full FlaUI end-to-end suite (8/8 passing) against the
new runtime. Note for anyone bumping WinUI again: wipe gclo/obj first, the XAML
compiler's incremental pass-1 output from the previous SDK can mix with pass-2
from the new one and fail with CS0759 on _XamlGeneratedCreateApplicationInstance.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:31:50 -05:00
James MaesandClaude Fable 5.1 818c0e1763 Deps: setup-dotnet v6, LibGit2Sharp 0.32 (security), Velopack, test tooling
Dependabot has been closing its own PRs and failing weekly on actions/setup-dotnet
("no longer updatable") since the v6 release in July, so nothing has landed since.
Bump everything it should have delivered:

- actions/setup-dotnet v5 -> v6 in all five workflow usages (ESM migration only)
- LibGit2Sharp 0.31.0 -> 0.32.0 (security release, bundles libgit2 1.8.6)
- Velopack 1.2.0 -> 1.2.161
- Microsoft.Windows.SDK.BuildTools 10.0.28000.2270 -> .2705
- Microsoft.NET.Test.Sdk 18.7.0 -> 18.10.1, coverlet.collector 10.0.1 -> 10.1.0,
  xunit.runner.visualstudio 3.1.5 -> 4.0.0 (all three test projects)

Verified locally: solution builds warning-free, 267 Engine + 65 CLI tests pass,
coverage gate still reports 100% on gclo.Engine / gclo.ViewModels / gclo,
dotnet format verify clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 05:29:36 -05:00