Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2688e3fc75
|
||
|
|
e4c4a5013f
|
No files matched your search
@@ -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
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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})."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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})");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user