Author SHA1 Message Date
james.maesandCodex 2688e3fc75 fix(ci): retain analyzer rules across isolated casing passes
Refs #3. Match the verified MarkdownViewr mitigation for the terminal PSScriptAnalyzer 1.25.0 Stack empty crash. All configured rules pass locally on every build script.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-11 16:41:03 -05:00
james.maesandCodex e4c4a5013f feat(template): add repository issue and community Help links
Refs #3. Generate owner-specific Help URLs and require Discussions during setup/readback; preserve native About, activity and updates.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-11 16:34:41 -05:00
13 changed files with 122 additions and 55 deletions

No files matched your search

+6 -1
View File
@@ -54,7 +54,12 @@ jobs:
# Analyze each file separately: recursive analysis can crash inside 1.25.0.
foreach($script in Get-ChildItem -LiteralPath build -Recurse -File | Where-Object Extension -In '.ps1', '.psm1', '.psd1')
{
$findings = @(Invoke-ScriptAnalyzer -Path $script.FullName -Settings ./PSScriptAnalyzerSettings.psd1)
# Run casing separately: the combined analyzer pass crashes in 1.25.0.
# Both passes together retain every configured rule and severity.
$findings = @(
Invoke-ScriptAnalyzer -Path $script.FullName -Settings ./PSScriptAnalyzerSettings.psd1 -IncludeRule PSUseCorrectCasing
Invoke-ScriptAnalyzer -Path $script.FullName -Settings ./PSScriptAnalyzerSettings.psd1 -ExcludeRule PSUseCorrectCasing
)
if($findings.Count -gt 0) { $findings | Format-Table; throw "Script analysis failed: $($script.Name)" }
}
- name: Verify script boundary tests
+2 -2
View File
@@ -90,8 +90,8 @@ pre-commit install --hook-type pre-commit --hook-type commit-msg
| `ci / build-test` | Locked restore, zero-warning build, unit tests, 100% coverage gate |
| `ci / format` | `dotnet format --verify-no-changes --severity warn` |
| `ci / ui-tests` | FlaUI end-to-end tests of the app |
| `installed-updates` | Real stable/dev installers, upgrades, restart and published-output UI smoke |
| `powershell`, `javascript` | Tool lint, setup/coverage boundary tests, review tests and dependency signatures |
| `ci / installed-updates` | Real stable/dev installers, upgrades, restart and published-output UI smoke |
| `scripts / powershell`, `scripts / javascript` | Tool lint, setup/coverage boundary tests, review tests and dependency signatures |
| `codeql / analyze (javascript-typescript)` | Static analysis of repository review tooling |
| `review / automated` (single maintainer) | Current-commit automated review acknowledged by the configured human maintainer |
+1 -2
View File
@@ -43,6 +43,7 @@ The command-line tool ships in each release as a self-contained single-file
startup update check on or off. Choices are saved immediately.
- **Help → Activity log:** a live view of the app's log, with an errors-only filter and a
shortcut to the log folder.
- **Help → View on GitHub / Submit an issue… / Community support…:** this app's repository, bug/feature templates and GitHub Discussions.
- **Help → Check for updates** and **Help → About**.
<!--#if (cli) -->
- **Command line:** `kof22-app greet --name Ada`, `kof22-app --version`, `kof22-app --help`.
@@ -61,8 +62,6 @@ Prerequisites:
- Optionally Visual Studio 2026 (Windows App SDK workload) or Rider
```powershell
# Once after generation, create and commit the NuGet locks locally:
# dotnet restore Kof22App.slnx -p:Platform=x64
dotnet restore Kof22App.slnx -p:Platform=x64 --locked-mode
dotnet build Kof22App.slnx -p:Platform=x64 -warnaserror
dotnet test Kof22App.slnx -p:Platform=x64 --no-build --filter "FullyQualifiedName!~UiTests"
+15 -17
View File
@@ -7,6 +7,13 @@ repository files, and the shared CI, security, and release workflows. It impleme
This file is not copied into generated apps.
## Default Help menu
Generated apps include gclo's repository, issue-template and community-support
commands. Their URLs use the generated `AppInfo.RepositoryUrl`, so `--owner` and
`-n` point to the new project. Repository setup enables and validates GitHub
Discussions; this is required for Community support to work.
## Create an app
```powershell
@@ -18,15 +25,6 @@ dotnet new k22-winapp -n Contoso.Notes --appId contoso-notes --displayName "Cont
--description "Quick notes that stay out of the way." -o Contoso.Notes
```
To try the installable package before its first publication, run from this clone:
```powershell
dotnet pack templates/KofTwentyTwo.Templates.WindowsApp.csproj -c Release -p:Version=0.1.0 -p:RestoreLockedMode=true -o artifacts/template
dotnet new install ./artifacts/template/KofTwentyTwo.Templates.WindowsApp.0.1.0.nupkg
dotnet new k22-winapp -n Contoso.Notes --appId contoso-notes --displayName "Contoso Notes" -o artifacts/Contoso.Notes
# Add --cli false for a GUI-only app; use a fresh output directory each time.
```
| Option | Default | What it sets |
| --- | --- | --- |
| `-n`, `--name` | (required) | Solution, project, namespace, and executable names (`Contoso.Notes.slnx`, `Contoso.Notes.Core`, `Contoso.Notes.exe`), and the repository name in links |
@@ -41,10 +39,8 @@ The copyright year in license headers becomes the current year.
## After generating
The generator omits NuGet dependency lock files. Before the first commit, run
`dotnet restore <name>.slnx -p:Platform=x64` in the generated directory to create
them, then run the README's locked restore, build, test, coverage and format
commands. Commit the resulting
The generator omits dependency lock files. Before the first commit, run the README's
restore, build, test, coverage and format commands, then commit the resulting
`packages.lock.json` files. Install the included hooks with
`pre-commit install --hook-type pre-commit --hook-type commit-msg` after `git init`.
The generated PowerShell tooling gate uses PSScriptAnalyzer 1.25.0.
@@ -52,10 +48,12 @@ The generated PowerShell tooling gate uses PSScriptAnalyzer 1.25.0.
1. Follow [the preview/apply/validate setup runbook](docs/repository-setup.md).
Create the GitHub repository (public, Product tier), push `main`, and apply the
[repository standard's](https://github.com/KofTwentyTwo/standards/blob/main/standards/repository.md)
settings and rulesets. Use the setup tool's required contexts and
[CONTRIBUTING.md](CONTRIBUTING.md)'s check table; standalone job names have no
workflow-name prefix. The source repository additionally requires `template`;
generated application repositories do not.
settings and rulesets, with these required checks: `pr / title`, `pr / dco`,
`pr / dependency-review`, `security / secrets`, `security / sca`,
`security / workflows`, `codeql / analyze (csharp)`, `codeql / analyze (actions)`,
`ci / build-test`, `ci / format`, `ci / ui-tests`, `scripts / powershell`,
`scripts / javascript`, `codeql / analyze (javascript-typescript)`, and
`review / automated` for the single-maintainer policy.
2. Create the `release` environment limited to `v*` tags, and list the repository in the
[standards repository inventory](https://github.com/KofTwentyTwo/standards/blob/main/MAINTAINERS.md#repositories)
(`K22-REPO-42`; the conformance checker fails until it is listed).
+10 -15
View File
@@ -47,21 +47,6 @@ Describe 'Required checks match standalone workflow jobs' `
Describe 'Generated repository administration' `
{
It 'allows the digest-pinned security containers without permitting arbitrary container actions' `
{
$plan = Get-GitHubRepositoryPlan -Repository 'Example/Notes'
$policy = ($plan.Requests | Where-Object Path -Like '*/actions/permissions/selected-actions').Body
$policy.patterns_allowed.Count | Should -Be 3
$policy.patterns_allowed | Should -Contain 'KofTwentyTwo/standards/*@*'
$containers = @($policy.patterns_allowed | Where-Object { $_.StartsWith('docker://', [StringComparison]::Ordinal) })
$containers.Count | Should -Be 2
foreach($container in $containers)
{
$container | Should -Match '^docker://ghcr\.io/(gitleaks/gitleaks:v8\.30\.1|aquasecurity/trivy:0\.75\.0)@sha256:[0-9a-f]{64}$'
$container | Should -Not -Match '\*'
}
}
InModuleScope GitHubRepository `
{
BeforeEach `
@@ -150,6 +135,16 @@ Describe 'Generated repository administration' `
@($script:Writes | Where-Object { $_ -match 'POST|environments/release$' }).Count | Should -Be 0
}
It 'requires community Discussions and detects disabled support without writing' `
{
Invoke-GitHubRepositorySetup -Plan $script:Plan -Mode Apply -CheckSha ('a' * 40) -Confirm:$false
$script:State[$script:Root].has_discussions | Should -BeTrue
$script:State[$script:Root].has_discussions = $false
$script:Writes.Clear()
{ Invoke-GitHubRepositorySetup -Plan $script:Plan -Mode Validate } | Should -Throw '*Configuration mismatch: repos/Example/Notes*'
$script:Writes.Count | Should -Be 0
}
It 'previews all mutations without writing' `
{
Invoke-GitHubRepositorySetup -Plan $script:Plan -Mode Apply -CheckSha ('a' * 40) -WhatIf
+2 -9
View File
@@ -60,7 +60,7 @@ function Get-GitHubRepositoryPlan
@{ Method = 'PATCH'; Path = $root; Body = @{
allow_squash_merge = $true; allow_merge_commit = $false; allow_rebase_merge = $false
squash_merge_commit_title = 'PR_TITLE'; squash_merge_commit_message = 'PR_BODY'
delete_branch_on_merge = $true; has_issues = $true; has_wiki = $false
delete_branch_on_merge = $true; has_issues = $true; has_discussions = $true; has_wiki = $false
homepage = "https://github.com/$Repository#readme"
security_and_analysis = @{
secret_scanning = @{ status = 'enabled' }; secret_scanning_push_protection = @{ status = 'enabled' }
@@ -74,14 +74,7 @@ function Get-GitHubRepositoryPlan
@{ Method = 'PUT'; Path = "$root/actions/permissions/workflow"; Body = @{ default_workflow_permissions = 'read'; can_approve_pull_request_reviews = $false } },
@{ Method = 'PUT'; Path = "$root/actions/permissions/fork-pr-contributor-approval"; Body = @{ approval_policy = 'all_external_contributors' } },
@{ Method = 'PUT'; Path = "$root/actions/permissions"; Body = @{ enabled = $true; allowed_actions = 'selected' } },
@{ Method = 'PUT'; Path = "$root/actions/permissions/selected-actions"; Body = @{
github_owned_allowed = $true; verified_allowed = $true
# Exact container actions used by the published v0.1.2 security builder.
patterns_allowed = @('KofTwentyTwo/standards/*@*',
'docker://ghcr.io/gitleaks/gitleaks:v8.30.1@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f',
'docker://ghcr.io/aquasecurity/trivy:0.75.0@sha256:af6acf9a6b85dfe389a1941505c0ce9efef52a4719635e1a962f022a3d855daa')
}
},
@{ Method = 'PUT'; Path = "$root/actions/permissions/selected-actions"; Body = @{ github_owned_allowed = $true; verified_allowed = $true; patterns_allowed = @('KofTwentyTwo/standards/*@*') } },
@{ Method = 'PUT'; Path = "$root/environments/release"; Body = @{ deployment_branch_policy = @{ protected_branches = $false; custom_branch_policies = $true } } },
@{ Method = 'POST'; Path = "$root/environments/release/deployment-branch-policies"; Body = @{ name = 'v*'; type = 'tag' } },
@{ Method = 'POST'; Path = "$root/rulesets"; Body = $main },
-6
View File
@@ -66,12 +66,6 @@ and environment protections are retained. API failures stop immediately; earlier
successful requests may remain, so validate and resolve the reported failure before
rerunning. The tool never creates repositories, pushes, merges, tags or releases.
The selected Actions policy permits the shared standards workflows, GitHub-owned
and verified actions, and the exact digest-pinned Gitleaks/Trivy container actions
used by the published security builder. GitHub also checks container actions
against this policy; missing entries prevent the security workflow from starting.
Review these exact references whenever the shared builder pin changes.
## Finish and verify
Register ownership/tier in the standards `MAINTAINERS.md` inventory through a
+10
View File
@@ -52,6 +52,16 @@ public static class AppIdentity
/// <summary>The app's bug/feature templates, independent of any upstream project credited in About.</summary>
public static Uri IssuesUrl { get; } = new(Info.RepositoryUrl!.AbsoluteUri.TrimEnd('/') + "/issues/new/choose");
/// <summary>Community questions and tips in the app's own repository.</summary>
public static Uri SupportUrl { get; } = new(Info.RepositoryUrl!.AbsoluteUri.TrimEnd('/') + "/discussions");
/// <summary>Loads and saves <see cref="AppSettings"/> at <see cref="AppPaths.SettingsFile"/>.</summary>
public static SettingsStore<AppSettings> SettingsStore { get; } = new(Paths.SettingsFile, AppSettingsContext.Default.AppSettings);
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using Microsoft.UI.Xaml;
namespace Kof22App;
/// <summary>Opens only the app's fixed public Help destinations after deliberate native menu actions.</summary>
public sealed partial class MainWindow
{
/// <summary>Shows this Windows app's repository in the user's default browser.</summary>
private async void GitHub_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(AppIdentity.Info.RepositoryUrl!);
/// <summary>Offers the app's issue templates without sending private document data.</summary>
private async void SubmitIssue_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(AppIdentity.IssuesUrl);
/// <summary>Opens community questions and tips in this app's repository.</summary>
private async void CommunitySupport_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(AppIdentity.SupportUrl);
/// <summary>A missing browser or OS launch failure is visible; no document path/content is included.</summary>
private async Task OpenHelpPageAsync(Uri target)
{
try
{
if(!await global::Windows.System.Launcher.LaunchUriAsync(target))
{
StatusText.Text = "The link could not be opened. Check your default browser.";
}
}
catch(Exception exception) when(exception is System.Runtime.InteropServices.COMException or InvalidOperationException or ArgumentException)
{
StatusText.Text = "The link could not be opened. Check your default browser.";
App.Log.Warning(FormattableString.Invariant($"A Help link could not be opened ({exception.GetType().Name}, 0x{exception.HResult:X8})."));
}
}
}
+5 -1
View File
@@ -26,7 +26,11 @@
<ToggleMenuFlyoutItem x:Name="SplashToggle" Text="Show splash at startup" Click="Splash_Click" />
<ToggleMenuFlyoutItem x:Name="UpdateCheckToggle" Text="Check for updates at startup" Click="UpdateCheck_Click" />
</MenuBarItem>
<MenuBarItem Title="Help">
<MenuBarItem Title="Help" AccessKey="H">
<MenuFlyoutItem Text="View on GitHub" AccessKey="G" AutomationProperties.AutomationId="MenuGitHub" Click="GitHub_Click" />
<MenuFlyoutItem Text="Submit an issue…" AccessKey="I" AutomationProperties.AutomationId="MenuSubmitIssue" ToolTipService.ToolTip="Report a bug or request a feature on GitHub" Click="SubmitIssue_Click" />
<MenuFlyoutItem Text="Community support…" AccessKey="S" AutomationProperties.AutomationId="MenuCommunitySupport" ToolTipService.ToolTip="Ask a question or share tips in GitHub Discussions" Click="CommunitySupport_Click" />
<MenuFlyoutSeparator />
<MenuFlyoutItem x:Name="CheckForUpdatesMenuItem" Text="Check for updates" Click="CheckForUpdates_Click" />
<MenuFlyoutItem x:Name="ActivityLogMenuItem" Text="Activity log" Click="ActivityLog_Click" />
<MenuFlyoutSeparator />
@@ -20,6 +20,8 @@ public class AppIdentityTests
Assert.EndsWith("_DATA_DIR", AppIdentity.Info.DataDirectoryVariable, StringComparison.Ordinal);
Assert.NotNull(AppIdentity.Info.RepositoryUrl);
Assert.NotEmpty(AppIdentity.Info.Attributions);
Assert.Equal("https://github.com/KofTwentyTwo/windows-app-template/issues/new/choose", AppIdentity.IssuesUrl.AbsoluteUri);
Assert.Equal("https://github.com/KofTwentyTwo/windows-app-template/discussions", AppIdentity.SupportUrl.AbsoluteUri);
}
+7 -2
View File
@@ -151,7 +151,12 @@ public sealed class AppSession : IDisposable
/// expand (its flyout opens before the menu is ready), so the menu is re-opened
/// until the item appears.
/// </summary>
public void InvokeMenuItem(string menuName, string itemAutomationId)
public void InvokeMenuItem(string menuName, string itemAutomationId) => Invoke(OpenMenuItem(menuName, itemAutomationId));
/// <summary>Opens an owned native menu and returns its item without launching an external destination.</summary>
public AutomationElement OpenMenuItem(string menuName, string itemAutomationId)
{
MainWindow.SetForeground();
AutomationElement menu = WaitFor(
@@ -171,7 +176,7 @@ public sealed class AppSession : IDisposable
Wait.UntilInputIsProcessed();
item = Retry.WhileNull(() => FindInApp(cf => cf.ByAutomationId(itemAutomationId)), timeout: TimeSpan.FromSeconds(3), interval: s_pollInterval, ignoreException: true).Result;
}
Invoke(item ?? throw new TimeoutException($"Menu item '{itemAutomationId}' never appeared under '{menuName}'. (app exited: {App.HasExited})"));
return item ?? throw new TimeoutException($"Menu item '{itemAutomationId}' never appeared under '{menuName}'. (app exited: {App.HasExited})");
}
+17
View File
@@ -36,6 +36,23 @@ public class AppTests
/// <summary>The real Help menu exposes the app's repository, issue reporting and community commands without launching a browser.</summary>
[Fact]
public void Help_PublicDestinations_AreAvailableInTheNativeMenu()
{
using var session = new AppSession();
_ = session.OpenMenuItem("Help", "MenuGitHub");
foreach((string identity, string label) in new[] { ("MenuGitHub", "GitHub"), ("MenuSubmitIssue", "issue"), ("MenuCommunitySupport", "support") })
{
AutomationElement item = session.WaitForElement(identity);
Assert.True(item.IsEnabled);
Assert.False(item.IsOffscreen);
Assert.Contains(label, item.Name, StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>About: shows the build version.</summary>
[Fact]
public void About_ShowsTheBuildVersion()