diff --git a/.github/workflows/scripts.yml b/.github/workflows/scripts.yml
index 6987c96..e6d8bb8 100644
--- a/.github/workflows/scripts.yml
+++ b/.github/workflows/scripts.yml
@@ -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
diff --git a/README.md b/README.md
index 8357752..36be59a 100644
--- a/README.md
+++ b/README.md
@@ -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**.
- **Command line:** `kof22-app greet --name Ada`, `kof22-app --version`, `kof22-app --help`.
diff --git a/TEMPLATE.md b/TEMPLATE.md
index f747fdc..ff935f2 100644
--- a/TEMPLATE.md
+++ b/TEMPLATE.md
@@ -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
diff --git a/build/GitHubRepository.Tests.ps1 b/build/GitHubRepository.Tests.ps1
index 6a33f1c..1788e4c 100644
--- a/build/GitHubRepository.Tests.ps1
+++ b/build/GitHubRepository.Tests.ps1
@@ -135,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
diff --git a/build/GitHubRepository.psm1 b/build/GitHubRepository.psm1
index 572b03c..542d3c8 100644
--- a/build/GitHubRepository.psm1
+++ b/build/GitHubRepository.psm1
@@ -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' }
diff --git a/src/Kof22App.Core/AppIdentity.cs b/src/Kof22App.Core/AppIdentity.cs
index 4b48e70..adaab14 100644
--- a/src/Kof22App.Core/AppIdentity.cs
+++ b/src/Kof22App.Core/AppIdentity.cs
@@ -52,6 +52,16 @@ public static class AppIdentity
+ /// The app's bug/feature templates, independent of any upstream project credited in About.
+ public static Uri IssuesUrl { get; } = new(Info.RepositoryUrl!.AbsoluteUri.TrimEnd('/') + "/issues/new/choose");
+
+
+
+ /// Community questions and tips in the app's own repository.
+ public static Uri SupportUrl { get; } = new(Info.RepositoryUrl!.AbsoluteUri.TrimEnd('/') + "/discussions");
+
+
+
/// Loads and saves at .
public static SettingsStore SettingsStore { get; } = new(Paths.SettingsFile, AppSettingsContext.Default.AppSettings);
diff --git a/src/Kof22App/MainWindow.Help.cs b/src/Kof22App/MainWindow.Help.cs
new file mode 100644
index 0000000..c949866
--- /dev/null
+++ b/src/Kof22App/MainWindow.Help.cs
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2026 James Maes
+ * SPDX-License-Identifier: MIT
+ */
+
+using Microsoft.UI.Xaml;
+
+
+namespace Kof22App;
+
+/// Opens only the app's fixed public Help destinations after deliberate native menu actions.
+public sealed partial class MainWindow
+{
+ /// Shows this Windows app's repository in the user's default browser.
+ private async void GitHub_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(AppIdentity.Info.RepositoryUrl!);
+
+
+
+ /// Offers the app's issue templates without sending private document data.
+ private async void SubmitIssue_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(AppIdentity.IssuesUrl);
+
+
+
+ /// Opens community questions and tips in this app's repository.
+ private async void CommunitySupport_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(AppIdentity.SupportUrl);
+
+
+
+ /// A missing browser or OS launch failure is visible; no document path/content is included.
+ 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})."));
+ }
+ }
+}
diff --git a/src/Kof22App/MainWindow.xaml b/src/Kof22App/MainWindow.xaml
index d60c3f8..77ae468 100644
--- a/src/Kof22App/MainWindow.xaml
+++ b/src/Kof22App/MainWindow.xaml
@@ -26,7 +26,11 @@
-
+
+
+
+
+
diff --git a/tests/Kof22App.Core.Tests/AppIdentityTests.cs b/tests/Kof22App.Core.Tests/AppIdentityTests.cs
index 361a188..0a17b15 100644
--- a/tests/Kof22App.Core.Tests/AppIdentityTests.cs
+++ b/tests/Kof22App.Core.Tests/AppIdentityTests.cs
@@ -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);
}
diff --git a/tests/Kof22App.UiTests/AppSession.cs b/tests/Kof22App.UiTests/AppSession.cs
index c9a9ec7..1f11aeb 100644
--- a/tests/Kof22App.UiTests/AppSession.cs
+++ b/tests/Kof22App.UiTests/AppSession.cs
@@ -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.
///
- public void InvokeMenuItem(string menuName, string itemAutomationId)
+ public void InvokeMenuItem(string menuName, string itemAutomationId) => Invoke(OpenMenuItem(menuName, itemAutomationId));
+
+
+
+ /// Opens an owned native menu and returns its item without launching an external destination.
+ 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})");
}
diff --git a/tests/Kof22App.UiTests/AppTests.cs b/tests/Kof22App.UiTests/AppTests.cs
index fbf0498..a9063a8 100644
--- a/tests/Kof22App.UiTests/AppTests.cs
+++ b/tests/Kof22App.UiTests/AppTests.cs
@@ -36,6 +36,23 @@ public class AppTests
+ /// The real Help menu exposes the app's repository, issue reporting and community commands without launching a browser.
+ [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);
+ }
+ }
+
+
+
/// About: shows the build version.
[Fact]
public void About_ShowsTheBuildVersion()