diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..e8809c1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Maintainer review for safety-critical code and automation. +/.github/ @harder +/src/SkillView.Core/Inventory/ @harder +/src/SkillView.Core/Gh/ @harder +/src/SkillView.Core/Ui/ @harder +/packaging/ @harder diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 001e5f1..f68a845 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,211 +1,72 @@ -# Copilot Instructions — gh-skillview - -Terminal UI and CLI for viewing/managing AI agent skills. Ships as a GitHub CLI -extension (`gh skillview`) and a standalone binary (`skillview`). C# / .NET 10, -Native AOT, Terminal.Gui v2. - -## Project status - -Phases 0–8 are complete. Current phase roadmap: - -| Phase | Scope | Status | -|-------|-------|--------| -| 0 | TG2 + .NET 10 Native AOT feasibility spike | ✅ Done | -| 1 | Environment probe, capability layer, structured logging, Doctor | ✅ Done | -| 2 | Local inventory discovery, SKILL.md parsing, symlink dedup, `gh skill list` adapter | ✅ Done | -| 3 | Search + preview adapters, `SearchScreen` TUI, CLI subcommands | ✅ Done | -| 4 | Install adapter, `InstallScreen` TUI, search→install handoff, CLI subcommand | ✅ Done | -| 5 | Update adapter, dry-run parsing, `UpdateScreen` TUI, TreeSha-axis diff | ✅ Done | -| 6 | §12.1 safe-remove validator, `RemoveService`, cleanup classifier, `RemoveScreen` + `CleanupScreen` TUIs, CLI subcommands | ✅ Done | -| 7 | Argv parser polish, exit-code contract, snapshot tests, dispatcher coverage | ✅ Done | -| 8 | Release engineering — six-RID AOT matrix, SLSA attestations, end-to-end install verification | ✅ Done | -| 9 | Hardening — contract tests, error classification, TG2 upstream review, scan diagnostics | ✅ Done | - -Consult `implementation-plan.md` (§22) for detailed phase specs. Durable -agent-facing workflow notes now live in `AGENTS.md` and focused files under -`agent_docs/`. - -## Build, test, publish - -```bash -dotnet restore -dotnet build -dotnet test # full suite - -# Filtered runs: use xunit's MTP-native --filter-* flags directly (no `--` -# separator, and NOT xunit's old single-dash console-runner flags). Scope to -# --project SkillView.Tests — filtering the whole solution fails the run when -# the *other* test project legitimately matches zero tests (xunit/xunit#3077). -dotnet test --project tests/SkillView.Tests/SkillView.Tests.csproj --filter-class "SkillView.Tests.Logging.RedactorTests" # single test class -dotnet test --project tests/SkillView.Tests/SkillView.Tests.csproj --filter-query "/*/SkillView.Tests.Logging/RedactorTests/RedactsGhTokens" # single test - -# AOT publish (Linux needs clang + zlib1g-dev): -dotnet publish src/SkillView.App -c Release -r osx-arm64 \ - -p:PublishAot=true -p:StripSymbols=true -o dist/app -``` - -No separate lint step — `TreatWarningsAsErrors` and `EnforceCodeStyleInBuild` -are enabled in `Directory.Build.props`, so `dotnet build` catches style issues. - -## Architecture - -Two thin entry-point projects (`SkillView.App`, `SkillView.GhExtension`) each -call `EntryPoint.RunAsync(args)` in `SkillView.Core`. The binary name -determines `InvocationMode` (Standalone vs GhExtension); presence of a -subcommand determines `DispatchMode` (CLI vs TUI). - -``` -EntryPoint.RunAsync - ├─ ArgParser.Parse → AppOptions (mode, debug, scan-roots, subcommand) - ├─ TuiServices.Build() → composition root (manual DI, no framework) - └─ DispatchMode switch - ├─ Cli → CliDispatcher.RunAsync (pattern-match on subcommand name) - └─ Tui → SkillViewApp.Run (Terminal.Gui v2 event loop) -``` - -`TuiServices` is the composition root — a sealed record with a static -`Build()` factory. No DI container; all wiring is explicit and AOT-safe. - -### Layer map - -| Layer | Responsibility | -|---|---| -| `Bootstrapping` | Argv parsing, entry point, `AppOptions` | -| `Cli` | `CliDispatcher` routes subcommands to handlers; JSON rendering helpers | -| `Environment` | `EnvironmentProbe` — locates `gh`, checks version, probes auth + capabilities | -| `Gh` | Subprocess adapters for `gh skill {search,preview,install,update,list}` | -| `Gh.Models` | Result records for each adapter (`SearchResultSkill`, `InstallResult`, …) | -| `Inventory` | Filesystem scan, SKILL.md front-matter parsing, symlink dedup, `gh skill list` merge | -| `Inventory.Models` | `InstalledSkill`, `SkillFrontMatter`, `Scope`, `ValidityState`, `Provenance` | -| `Logging` | Ring-buffer `Logger`, `Redactor`, `FileLogSink` (daily rotation, 14-day retention) | -| `Subprocess` | `ProcessRunner` — argv-array execution, never shell composition | -| `Ui` | TG2 screens: main window, `SearchScreen`, `InstallScreen`, `UpdateScreen`, `RemoveScreen`, `CleanupScreen` | - -### Capability gating - -`GhSkillCapabilityProbe` parses `gh skill --help` output to detect which -flags the installed `gh` version supports. That covers preview probing/support -for shared flags like `--allow-hidden-dirs` as well as install-time gating. -Adapters only emit flags that the probe confirmed — this keeps the app -forward-compatible with evolving `gh` releases. - -## Key conventions - -### AOT compatibility (critical) - -Every code path must be Native AOT safe. This means: - -- **No reflection**: no `Type.GetProperties()`, `Activator.CreateInstance()`, - or attribute scanning. -- **JSON**: use `System.Text.Json` source generators (`GhJsonContext`). - Deserialize via `GhJsonContext.Default.`, never `typeof(T)`. -- **Regex**: use `[GeneratedRegex]` partial methods, never `new Regex(...)`. -- **YAML**: hand-rolled subset parser (`FrontMatterParser`), not YamlDotNet. -- **Arg parsing**: hand-rolled `ArgParser`, not any third-party library. -- **TG2 trimming**: `Terminal.Gui` is currently pinned to `2.4.2-develop.53` in - `SkillView.Core.csproj`. Keep the modern - `Application.Create().Init()` / `IApplication.Dispose()` lifecycle, and do - not add new suppressions or `[UnconditionalSuppressMessage]` workarounds - unless a new TG2 analyzer regression is reproduced, documented, and tied to a - `// TODO(tg2): upstream` note. `SkillView.GhExtension` still carries its current - project-level TG2-related `NoWarn` entries until that host is separately - re-evaluated under AOT publish verification. - -### Immutability - -- Prefer `record` (or `record struct`) over `class` for data types. -- Use `required` + `init` properties, not mutable setters. -- Collections: `ImmutableArray`, `ImmutableDictionary`, - `IReadOnlyList`. Avoid `List` in public API surfaces. - -### Error handling - -- Service methods return result records (`SearchResponse`, `InstallResult`, - etc.) with `ExitCode` / `ErrorMessage` fields — they do not throw. -- `ProcessRunner` returns a `ProcessResult` on startup failure instead of - throwing. -- Infrastructure failures (logging, file I/O) degrade gracefully and log a - warning. - -### Subprocess calls - -Always go through `ProcessRunner.RunAsync(executable, argsArray)`. -Never compose shell strings. The `Redactor` runs automatically on log output. - -### Logging - -Call `logger.Debug/Info/Warn/Error(category, message)` where `category` is a -dot-separated label (e.g., `"gh.skill.search"`). Redaction is applied at the -log-writer layer — never log raw tokens, but also don't manually redact. - -### Naming and file layout - -- Namespace mirrors directory: `SkillView.Gh.Models` → `src/SkillView.Core/Gh/Models/`. -- One type per file; filename matches type name. -- Private fields: `_camelCase`. Parameters: `camelCase`. -- Async methods end in `Async` and accept `CancellationToken`. -- Use `.ConfigureAwait(false)` on awaited calls in library code. - -### Exit codes - -Use `ExitCodes` constants (`Success = 0`, `UserError = 1`, -`InvalidUsage = 2`, `EnvironmentError = 10`, `NoMatches = 20`). These are -part of the public contract — scripts and agent hooks depend on them. - -### Testing - -- xUnit with `[Fact]` and `[Theory]`/`[InlineData]`. No mocking libraries. -- Embed test data as raw string literals; no fixture files. -- Test `internal` members via `InternalsVisibleTo` (set in `SkillView.Core.csproj`). -- Snapshot tests for JSON-emitting subcommands live in - `CliDispatcherJsonSnapshotTests`. - -### Terminal.Gui v2 - -- Create screens as `Dialog` subclasses or inline `Dialog` instances with - `Dim.Percent` / `Dim.Fill` sizing. -- Run modals via `_app.Run(dialog)`. -- Bind key handlers on the `Window.KeyDown` event (not `AddCommand` — it's - `protected` in the current TG2 RC surface). -- Use `SelectedCellChanged` on `TableView` for live detail updates. -- Track upstream issues with `// TODO(tg2):` comments. - -### Cross-references - -Code comments use `§N` notation (e.g., `§12.1`) to reference sections in -`implementation-plan.md`. Consult that file for detailed design rationale -behind validators, classifiers, and reconciliation logic. - -## Implementation rules (from §24) - -These rules govern all changes — they are the project's architectural invariants: - -1. Exactly three production projects plus one test project. -2. Domain logic, services, and most UI code live in `SkillView.Core`. -3. Entrypoints are tiny (two lines each). -4. Composition over inheritance. -5. Capability probes via flag-token membership scans, not help-text structural parsing. -6. Never delete anything above a validated skill directory. -7. Resolve symlinks via realpath before any mutation; require the resolved path to remain inside a known root. -8. Never follow symlinks that escape the scan root after resolution. -9. JSON parsing only where `gh` documents JSON output. -10. Always rescan inventory post-op (install / update / remove). -11. Subprocess invocation uses the argv-array API — never shell composition. -12. Apply redaction at the log-writer layer. -13. No central config file. State persistence lives in `.skillview-ignore` markers or flags. -14. Prefer TG2 v2 components over manual implementations. Reference the TG2 `Examples/` directory before hand-rolling UI. -15. Any TG2 workaround must be marked `// TODO(tg2): upstream` and reviewed in Phase 9. -16. Default log level is Info+; Debug requires `--debug` or `SKILLVIEW_LOG=debug`. - -## Key upstream dependencies and gating - -- **`gh` minimum**: v2.97.0. Hard-enforced in `GhBinaryLocator.MinimumVersion`; it supplies the full 48-host install selector, including Devin and Grok. -- **`gh skill list --json`**: shipped in v2.94.0. `GhSkillListAdapter` uses it as the primary inventory source. -- **`gh skill update --yes`**: not supported; `--all` is the non-interactive update path. The `UpdateScreen` has guardrails for the interactive-prompt quirk. -- **Custom install directory**: `gh skill install --dir` is the supported destination override; SkillView does not use a `--repo-path` capability. -- **Terminal.Gui v2**: defaulted via `SkillView.Core.csproj` to `2.4.17`. - Keep the modern `Application.Create().Init()` + - `IApplication.Dispose()` lifecycle, tie async UI work to app/dialog lifetime - cancellation, and track any real remaining framework workarounds with - `// TODO(tg2): upstream`. `SkillView.GhExtension` still retains its current - project-level TG2-related `NoWarn` entries pending separate re-evaluation. +# GitHub Copilot instructions + +Read [`../AGENTS.md`](../AGENTS.md) before reviewing or changing this repository. +It is the source of truth for project structure, supported versions, safety +invariants, build commands, and focused guides in `agent_docs/`. Do not copy +general project facts into this file; use the current code and `AGENTS.md` when +they disagree with an old comment, issue, or generated summary. + +## For automated reviews + +- Prioritize correctness, security, data loss, deadlocks, cross-platform + behavior, Native AOT compatibility, and user-visible regressions. Give the + exact triggering path or sequence, impact, and a small actionable fix. +- Review changed code against adjacent call sites and tests. Trace async + ownership through cancellation, queued UI dispatch, completion, and disposal. + A completed worker does not imply its UI completion callback has run. +- Treat removal as a destructive security boundary. Apply the complete + `AGENTS.md` removal contract before suggesting a simpler path or symlink + implementation. Do not recommend path-recursive deletion as a fallback. +- Check adapters against the **minimum supported** `gh` version as well as the + newest release. The minimum-version check replaces old per-flag capability + probing. Flag removals, JSON schema changes, and agent selector changes need + explicit evidence and focused tests. +- For Terminal.Gui updates, inspect lifecycle, UI-thread dispatch, key input, + scrolling, modal ownership, trimming, and both executable hosts. Do not + reintroduce obsolete `ConfigurationManager` APIs or static app lifecycle. +- For dependency PRs, read upstream release notes and the lock-file diff; + identify API or behavior changes that affect this code. A package version + change with no relevant change warrants a short review, not speculative + migration work. Never suggest a version bump that is already present. +- Review workflow edits for least-privilege permissions, pinned action SHAs, + untrusted PR input in shell expressions, concurrency, checksum integrity, + and whether a failure can silently pass. Release assets must remain + immutable once published. +- Confirm a finding against the current diff. Cite the file and line, explain + an observable failure, and separate proven defects from questions. Avoid + style-only comments already enforced by `dotnet build` and `dotnet format`. + +## For automated coding agents + +- Make the smallest change that solves the requested issue. Do not edit + `AGENTS.md` or other guidance solely to accommodate a speculative review. +- Preserve the existing public CLI, JSON, exit-code, and safety contracts + unless the request explicitly changes them. Add focused regression tests + for changed behavior, especially cancellation and removal boundaries. +- Run the narrow relevant tests first, then `dotnet restore --locked-mode`, + `dotnet build`, and `dotnet test --no-build` before reporting completion. + Run the two product publishes **sequentially** when a change can affect AOT. +- For a package update, update `packages.lock.json` through an intentional + restore, then verify locked restore. Keep the dependency monitor's version + parser, package pins, and version-sensitive tests in sync. +- For a GitHub CLI or Terminal.Gui compatibility issue, use the issue's + checklist as a starting point, inspect upstream evidence, and propose or + implement focused tests. Do not increase the `gh` minimum automatically. +- Do not execute filesystem removal against a developer's real skill roots + while validating a change. Follow the isolated PTY procedure in + `agent_docs/tui-pty-testing.md` when interactive testing is needed. +- In the PR description, state what changed, what was run, and any remaining + platform or live-terminal verification limits. Never claim a release or + deployment succeeded from a local build alone. + +## For Copilot issue and PR automation + +- Dependency monitor issues and Dependabot PRs are review inputs, not + authorization to merge or publish. Link the relevant issue and upstream + release notes when proposing a compatibility change. +- Keep bot-generated suggestions concrete: name affected SkillView code paths, + expected behavior, and a test that would detect a regression. Do not invent + breaking changes from a version number alone. +- If GitHub's automated Copilot review is enabled, apply these instructions to + its comments too. A human maintainer retains the decision to merge releases + and dependency updates. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a518f45..ba2afda 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,41 @@ version: 2 updates: - - package-ecosystem: "nuget" - directory: "/" + # A root NuGet scan discovers all five nested project files. Keep the two + # Terminal.Gui packages together while reviewing their API changes manually. + - package-ecosystem: nuget + directory: / schedule: - interval: "weekly" + interval: weekly + day: monday + time: "09:00" + timezone: America/Chicago + allow: + - dependency-type: all + assignees: [harder] open-pull-requests-limit: 10 + groups: + terminal-gui: + patterns: ["Terminal.Gui", "Terminal.Gui.Editor"] + test-platform: + patterns: ["xunit*", "Microsoft.NET.Test.Sdk"] - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: github-actions + directory: / schedule: - interval: "weekly" + interval: weekly + day: monday + time: "10:00" + timezone: America/Chicago + assignees: [harder] + open-pull-requests-limit: 10 groups: - github-actions: - patterns: - - "*" + github-maintained: + patterns: ["actions/*", "github/*"] + + # global.json is a dependency too; keep SDK roll-forwards reviewable. + - package-ecosystem: dotnet-sdk + directory: / + schedule: + interval: monthly + assignees: [harder] + open-pull-requests-limit: 5 diff --git a/.github/scripts/critical-dependencies.js b/.github/scripts/critical-dependencies.js new file mode 100644 index 0000000..a3ec41e --- /dev/null +++ b/.github/scripts/critical-dependencies.js @@ -0,0 +1,157 @@ +const fs = require('node:fs'); + +const LABEL = 'critical-dependency'; +const TERMINAL_GUI_REPO = 'Terminal.Gui'; + +function versionParts(value) { + const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(value); + return match ? match.slice(1).map(Number) : null; +} + +function compareVersions(left, right) { + const a = versionParts(left); + const b = versionParts(right); + if (!a || !b) throw new Error(`Expected stable versions, got ${left} and ${right}`); + for (let index = 0; index < a.length; index++) { + if (a[index] !== b[index]) return a[index] - b[index]; + } + return 0; +} + +function propertyVersion(project, property) { + const expression = new RegExp(`<${property}(?:\\s[^>]*)?>(\\d+\\.\\d+\\.\\d+)<\\/${property}>`); + const match = expression.exec(project); + if (!match) throw new Error(`Could not read ${property} from SkillView.Core.csproj`); + return match[1]; +} + +function minimumGhVersion(source) { + const match = /MinimumVersion\s*=\s*new\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(source); + if (!match) throw new Error('Could not read GhBinaryLocator.MinimumVersion'); + return match.slice(1).join('.'); +} + +function suggestedChecks(notes, kind) { + const text = (notes || '').toLowerCase(); + const checks = []; + if (kind === 'terminal-gui') { + if (/key|input|paste|mouse/.test(text)) checks.push('keyboard shortcuts, text fields, paste, and focus behavior'); + if (/layout|scroll|table|render/.test(text)) checks.push('table layout, scrolling, terminal resize, and theme rendering'); + if (/async|thread|dispatch|cancel|shutdown/.test(text)) checks.push('UI dispatch, cancellation ownership, modal completion, and shutdown'); + if (/trim|aot|reflection/.test(text)) checks.push('standalone and extension Native AOT warnings and startup'); + } else { + if (/skill|agent/.test(text)) checks.push('`gh skill` subcommands, agent selectors, and install defaults'); + if (/json|output|format/.test(text)) checks.push('JSON inventory/search output and parser fixtures'); + if (/extension|path|auth/.test(text)) checks.push('extension launch, `GH_PATH`, authentication, and subprocess selection'); + } + return checks.length ? checks.map(check => `- ${check}`).join('\n') + : '- Compare upstream changes with SkillView adapters and add focused tests for any affected behavior.'; +} + +async function stableNugetVersion(packageName, fetchImpl) { + const response = await fetchImpl(`https://api.nuget.org/v3-flatcontainer/${packageName.toLowerCase()}/index.json`, { + signal: AbortSignal.timeout(15000), + }); + if (!response.ok) throw new Error(`NuGet returned HTTP ${response.status} for ${packageName}`); + const data = await response.json(); + const stable = (data.versions || []).filter(version => versionParts(version)); + if (!stable.length) throw new Error(`No stable NuGet versions found for ${packageName}`); + stable.sort((left, right) => compareVersions(right, left)); + return stable[0]; +} + +async function ensureLabel(github, owner, repo) { + try { + await github.rest.issues.getLabel({ owner, repo, name: LABEL }); + } catch (error) { + if (error.status !== 404) throw error; + await github.rest.issues.createLabel({ + owner, repo, name: LABEL, color: 'B60205', + description: 'Upstream release requiring SkillView compatibility review', + }); + } +} + +async function createOnce(github, core, owner, repo, title, body) { + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner, repo, labels: LABEL, state: 'all', per_page: 100, + }); + if (issues.some(issue => !issue.pull_request && issue.title === title)) { + core.info(`Already tracked: ${title}`); + return; + } + const { data: issue } = await github.rest.issues.create({ + owner, repo, title, body, labels: [LABEL], assignees: [owner], + }); + core.info(`Created ${issue.html_url}`); +} + +async function checkTerminalGui(github, core, owner, repo, project, fetchImpl) { + const packages = [ + ['Terminal.Gui', propertyVersion(project, 'TerminalGuiVersion')], + ['Terminal.Gui.Editor', propertyVersion(project, 'TerminalGuiEditorVersion')], + ]; + const { data: releases } = await github.rest.repos.listReleases({ + owner: 'tui-cs', repo: TERMINAL_GUI_REPO, per_page: 30, + }); + for (const [packageName, current] of packages) { + const latest = await stableNugetVersion(packageName, fetchImpl); + if (compareVersions(latest, current) <= 0) { + core.info(`${packageName} is current at ${current}`); + continue; + } + const release = releases.find(item => !item.draft && item.tag_name.toLowerCase() === `v${latest}`); + const notes = release?.body || ''; + await createOnce(github, core, owner, repo, + `${packageName} ${latest} compatibility review`, ` +SkillView pins **${packageName} ${current}**; NuGet now has **${latest}**. + +- [NuGet package](https://www.nuget.org/packages/${packageName}/${latest}) +- [Upstream release](${release?.html_url || 'https://github.com/tui-cs/Terminal.Gui/releases'}) +- Find the Dependabot PR and review both Terminal.Gui packages together when appropriate. +- Run locked tests and all four Native AOT publishes; check the extension's remaining trim suppressions. +- Exercise keyboard selection, scrolling, resizing, install dialogs, and shutdown in a real terminal. +- Ask Copilot to review the update PR using \`.github/copilot-instructions.md\` and propose focused compatibility tests. + +Suggested checks from release notes: +${suggestedChecks(notes, 'terminal-gui')} +`.trim()); + } +} + +async function checkGitHubCli(github, core, owner, repo, locator) { + const minimum = minimumGhVersion(locator); + const { data: release } = await github.rest.repos.getLatestRelease({ owner: 'cli', repo: 'cli' }); + if (release.draft || release.prerelease || !versionParts(release.tag_name)) { + throw new Error(`Unexpected GitHub CLI latest release: ${release.tag_name}`); + } + await createOnce(github, core, owner, repo, + `GitHub CLI ${release.tag_name} compatibility review`, ` +[GitHub CLI ${release.tag_name}](${release.html_url}) is available. SkillView currently requires **gh ${minimum}+**. + +- Run the required contract tests against gh ${minimum} and ${release.tag_name.slice(1)}. +- Compare \`gh skill --help\`, search/preview/install/update/list flags, and JSON output with SkillView's adapters. +- Diff \`gh skill install --help\` agent selectors against \`InstallAgentCatalog\` and its tests. +- Check extension launch, \`GH_PATH\`, authentication, install defaults, and any release-note changes to \`gh skill\`. +- Update the minimum only when a needed behavior requires it. Ask Copilot to propose focused tests or fixes; keep changes under human review. + +Suggested checks from release notes: +${suggestedChecks(release.body, 'gh')} +`.trim()); +} + +module.exports = async ({ + github, context, core, fetchImpl = fetch, + project = fs.readFileSync('src/SkillView.Core/SkillView.Core.csproj', 'utf8'), + locator = fs.readFileSync('src/SkillView.Core/Gh/GhBinaryLocator.cs', 'utf8'), +}) => { + const { owner, repo } = context.repo; + await ensureLabel(github, owner, repo); + await checkTerminalGui(github, core, owner, repo, project, fetchImpl); + await checkGitHubCli(github, core, owner, repo, locator); +}; + +module.exports.compareVersions = compareVersions; +module.exports.propertyVersion = propertyVersion; +module.exports.minimumGhVersion = minimumGhVersion; +module.exports.suggestedChecks = suggestedChecks; diff --git a/.github/scripts/critical-dependencies.test.js b/.github/scripts/critical-dependencies.test.js new file mode 100644 index 0000000..53fd97f --- /dev/null +++ b/.github/scripts/critical-dependencies.test.js @@ -0,0 +1,79 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const monitor = require('./critical-dependencies.js'); + +const project = ` + 2.5.0 + 2.5.7 +`; +const locator = 'public static readonly SemVer MinimumVersion = new(2, 97, 0);'; + +test('reads package properties and the enforced GitHub CLI minimum', () => { + assert.equal(monitor.propertyVersion(project, 'TerminalGuiVersion'), '2.5.0'); + assert.equal(monitor.propertyVersion(project, 'TerminalGuiEditorVersion'), '2.5.7'); + assert.equal(monitor.minimumGhVersion(locator), '2.97.0'); + assert.ok(monitor.compareVersions('2.5.10', '2.5.9') > 0); + assert.ok(monitor.compareVersions('v2.102.0', '2.97.0') > 0); +}); + +test('creates assigned, deduplicated issues with useful checks for new releases', async () => { + const issues = []; + let labelExists = false; + const github = { + paginate: async () => issues, + rest: { + issues: { + getLabel: async () => { + if (!labelExists) throw Object.assign(new Error('missing label'), { status: 404 }); + }, + createLabel: async () => { labelExists = true; }, + listForRepo: async () => ({ data: issues }), + create: async ({ title, body, labels, assignees }) => { + const issue = { title, body, labels, assignees, html_url: `https://example.invalid/${issues.length + 1}` }; + issues.push(issue); + return { data: issue }; + }, + }, + repos: { + listReleases: async () => ({ data: [{ + tag_name: 'v2.5.1', html_url: 'https://example.invalid/tg', + body: 'Fix keyboard input and layout.', draft: false, + }] }), + getLatestRelease: async () => ({ data: { + tag_name: 'v2.102.0', html_url: 'https://example.invalid/gh', + body: 'Improve gh skill JSON output.', draft: false, prerelease: false, + } }), + }, + }, + }; + const fetchImpl = async url => ({ + ok: true, + json: async () => ({ versions: url.includes('terminal.gui.editor') + ? ['2.5.7', '2.5.8-develop.1'] + : ['2.5.0', '2.5.1', '2.5.2-develop.1'] }), + }); + const args = { + github, context: { repo: { owner: 'harder', repo: 'gh-skillview' } }, + core: { info: () => {} }, project, locator, fetchImpl, + }; + + await monitor(args); + assert.equal(issues.length, 2); + assert.deepEqual(issues.map(issue => issue.title), [ + 'Terminal.Gui 2.5.1 compatibility review', + 'GitHub CLI v2.102.0 compatibility review', + ]); + assert.ok(issues.every(issue => issue.assignees[0] === 'harder' && issue.labels[0] === 'critical-dependency')); + assert.match(issues[0].body, /keyboard shortcuts/); + assert.match(issues[1].body, /JSON inventory\/search output/); + await monitor(args); + assert.equal(issues.length, 2); +}); + +test('fails visibly when a critical version cannot be determined', async () => { + await assert.rejects(() => monitor({ + github: { rest: { issues: { getLabel: async () => {} } } }, + context: { repo: { owner: 'harder', repo: 'gh-skillview' } }, + core: { info: () => {} }, project: '', locator, + }), /TerminalGuiVersion/); +}); diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 345bcc0..47707bf 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,75 +1,52 @@ -# CI/CD Workflows - -## Workflows - -### `ci.yml` — Continuous Integration - -Runs on pushes and pull requests targeting `main`. - -- Restores dependencies -- Builds in `Release` -- Runs the full .NET test suite -- Runs three-RID AOT publish smoke coverage (`linux-x64`, `osx-arm64`, `win-x64`) -- Promotes standalone-app AOT trim warnings (`IL2026`, `IL3050`, `IL3053`) to errors during CI smoke publishes - -### `release.yml` — Build, verify, and publish tagged releases - -**Triggers** - -| Trigger | When | -|---|---| -| Tag push | `v*` tags | -| `workflow_dispatch` | Manual reruns / release packaging exercises | - -**Pipeline** - -```text -build (4 RIDs, restore/build/test/publish) -> release (tag pushes only) - -> publish-homebrew (dark-launch, stable tags only, opt-in) - -> publish-winget (dark-launch, stable tags only, opt-in) - -> notify-failure (on error) -``` - -**Build matrix** - -- `win-x64` -- `win-arm64` -- `linux-x64` -- `osx-arm64` - -Each build leg now: - -1. restores -2. builds in `Release` -3. runs the full .NET test suite -4. publishes the Native AOT standalone binary -5. publishes the Native AOT `gh` extension binary (`src/SkillView.GhExtension`) -6. uploads staged assets as artifacts - -Release runs are serialized with a workflow-level concurrency lock so two publishes cannot overlap. - -**Artifacts** - -Asset names use Go's OS/arch vocabulary (`windows`/`linux`/`darwin`, `amd64`/`arm64`) throughout — `gh extension install` requires this naming to auto-detect a precompiled binary, and using it for the standalone assets too keeps one consistent scheme. - -- Standalone binaries: `skillview-windows-amd64.exe`, `skillview-windows-arm64.exe`, `skillview-linux-amd64`, `skillview-darwin-arm64` -- `gh` extension binaries: `gh-skillview-windows-amd64.exe`, `gh-skillview-windows-arm64.exe`, `gh-skillview-linux-amd64`, `gh-skillview-darwin-arm64` -- Checksums: `SHA256SUMS-.txt` (one file per platform, covering both binaries) - -Artifacts are uploaded per RID and then merged by the final release job, which publishes a GitHub Release via `softprops/action-gh-release`. `workflow_dispatch` still exercises the build/package flow, but only tag pushes publish a release. - -**Package-manager dark launch** - -- `publish-homebrew` is gated behind `HOMEBREW_TAP_ENABLED == true` and generates a formula artifact from `packaging/homebrew/skillview.rb.tmpl` for the shipped Unix targets (`darwin-arm64`, `linux-amd64`). -- Recommended future tap target: `harder/homebrew-tap` via repo variable `HOMEBREW_TAP_REPO`. -- `publish-winget` is gated behind `WINGET_ENABLED == true` and generates manifest artifacts from `packaging/winget/`. -- Both jobs stop at generated artifacts today; they do **not** push to a tap repo or submit to `winget-pkgs` yet. - -**Failure handling** - -If any release job fails, `notify-failure` opens or reuses an open GitHub issue titled `Release workflow failed on ` with a link to the failed run. - -## Related docs - -- `agent_docs/release-engineering.md` -- `docs/runbooks/release-rollback.md` +# GitHub automation + +The repository keeps its workflows here and its committed NuGet lock files in +each project. `AGENTS.md` is the project-wide source of build and safety rules. + +| Workflow | Trigger | Purpose | +| --- | --- | --- | +| `ci.yml` | `main`, PR, manual | Action lint, monitor tests, dependency review, site check, three-OS tests, and four-RID AOT smoke | +| `codeql.yml` | `main`, PR, weekly, manual | C# and GitHub Actions security analysis | +| `contract-tests.yml` | Daily, manual | Required live `gh` tests at the 2.97.0 minimum and latest release; open an assigned issue on failure | +| `critical-dependencies.yml` | Daily, manual | Check stable Terminal.Gui packages and latest GitHub CLI; open deduplicated, assigned review issues | +| `pages.yml` | Site changes, manual | Build and deploy `site/` to GitHub Pages | +| `release.yml` | `v*` tag, manual | Build, attest, verify, and publish release assets; optionally generate package manifests | + +Dependabot checks NuGet weekly, GitHub Actions weekly, and `global.json` monthly. +Terminal.Gui packages are grouped for joint review. The workflow Action pins are +full commit SHAs; the version comments show the release each SHA represents. +The dependency monitor's source and tests are in `.github/scripts/`. + +## Release pipeline + +`release.yml` validates a version tag and refuses to republish an existing +release. Four native jobs (`win-x64`, `win-arm64`, `linux-x64`, `osx-arm64`) +restore in locked mode, build and test, then publish standalone and `gh` +extension binaries sequentially. Each leg checks `--version`, stages two +binaries and a SHA-256 file, and attests the binaries. + +The release job requires exactly 12 assets: eight binaries and four checksum +files. It checks every checksum before creating a draft release, checks the +published asset count, then makes the release public. Do not re-run a published +tag to replace binaries; cut a corrected tag. A failed job opens or reuses a +failure issue. See `docs/runbooks/release-rollback.md` for recovery. + +Stable-tag Homebrew and WinGet jobs are opt-in via `HOMEBREW_TAP_ENABLED` and +`WINGET_ENABLED`. They download **published** release assets and generate +formula/manifests as workflow artifacts. They do not push to a tap or submit +to WinGet. `HOMEBREW_TAP_REPO` records the intended tap name. + +Manual `workflow_dispatch` exercises release packaging without publishing a +GitHub Release. Keep the two product publishes sequential: they share core +intermediate files. Linux AOT requires `clang` and `zlib1g-dev`. + +## Repository settings to keep enabled + +- Dependabot alerts and security updates, the dependency graph, and secret + scanning where available. +- GitHub Pages from Actions with custom domain `skillview.dev`. +- Release immutability once all assets and checks are validated. +- A `main` ruleset requiring PR review and passing CI checks; allow the + maintainer to merge reviewed dependency PRs, not bots to bypass checks. + +`agent_docs/release-engineering.md` contains release development notes. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b69a29c..b096e20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,24 +5,61 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: permissions: contents: read +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: + workflow-lint: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Lint GitHub Actions workflows + uses: rhysd/actionlint@914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12 + - name: Test dependency monitor + run: node --test .github/scripts/critical-dependencies.test.js + + dependency-review: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Reject newly vulnerable dependencies + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + + site-check: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Build and validate local site links + run: python3 site/build.py + - name: Check JavaScript syntax + run: node --check site/app.js + test: strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-15, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - - uses: actions/checkout@v7 - - uses: actions/setup-dotnet@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: global-json-file: global.json - - name: Restore - run: dotnet restore + cache: true + cache-dependency-path: '**/packages.lock.json' + - name: Restore locked dependencies + run: dotnet restore --locked-mode - name: Build run: dotnet build --configuration Release --no-restore - name: Verify code style — dotnet format @@ -34,32 +71,41 @@ jobs: aot-smoke: name: AOT publish smoke (${{ matrix.rid }}) runs-on: ${{ matrix.os }} + timeout-minutes: 60 strategy: fail-fast: false matrix: include: - rid: linux-x64 os: ubuntu-latest + ext: '' - rid: osx-arm64 os: macos-15 + ext: '' - rid: win-x64 os: windows-latest + ext: .exe + - rid: win-arm64 + os: windows-11-arm + ext: .exe steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Linux AOT toolchain if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y clang zlib1g-dev - - uses: actions/setup-dotnet@v6 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: global-json-file: global.json + cache: true + cache-dependency-path: '**/packages.lock.json' - name: AOT publish (standalone) shell: bash run: | dotnet publish src/SkillView.App/SkillView.App.csproj \ -c Release -r ${{ matrix.rid }} \ - -p:PublishAot=true -p:StripSymbols=true \ + -p:PublishAot=true -p:StripSymbols=true -p:RestoreLockedMode=true \ -warnaserror:IL2026 -warnaserror:IL3050 -warnaserror:IL3053 \ -o dist/app - name: AOT publish (extension) @@ -67,8 +113,13 @@ jobs: run: | dotnet publish src/SkillView.GhExtension/SkillView.GhExtension.csproj \ -c Release -r ${{ matrix.rid }} \ - -p:PublishAot=true -p:StripSymbols=true \ + -p:PublishAot=true -p:StripSymbols=true -p:RestoreLockedMode=true \ -o dist/ext - - name: Record sizes + - name: Smoke-test native binaries shell: bash - run: ls -la dist/app dist/ext + run: | + set -euo pipefail + test -s 'dist/app/skillview${{ matrix.ext }}' + test -s 'dist/ext/gh-skillview${{ matrix.ext }}' + 'dist/app/skillview${{ matrix.ext }}' --version + 'dist/ext/gh-skillview${{ matrix.ext }}' --version diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 540c05b..163e14d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,39 +7,49 @@ on: branches: [main] schedule: - cron: '30 3 * * 1' + workflow_dispatch: permissions: contents: read jobs: analyze: - name: Analyze (csharp) + name: Analyze (${{ matrix.language }}) + if: github.event_name == 'pull_request' || github.actor != 'dependabot[bot]' runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + language: [csharp, actions] permissions: security-events: write actions: read contents: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-dotnet@v6 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + if: matrix.language == 'csharp' with: global-json-file: global.json - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 with: - languages: csharp - build-mode: manual + languages: ${{ matrix.language }} + build-mode: ${{ matrix.language == 'csharp' && 'manual' || 'none' }} - name: Restore - run: dotnet restore + if: matrix.language == 'csharp' + run: dotnet restore --locked-mode - name: Build + if: matrix.language == 'csharp' run: dotnet build --configuration Release --no-restore - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 with: - category: "/language:csharp" + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/contract-tests.yml b/.github/workflows/contract-tests.yml index b927661..23f2352 100644 --- a/.github/workflows/contract-tests.yml +++ b/.github/workflows/contract-tests.yml @@ -2,11 +2,11 @@ name: contract-tests on: schedule: - - cron: '0 6 * * *' + - cron: '17 6 * * *' workflow_dispatch: inputs: gh_version: - description: 'Override gh version (e.g., 2.101.0 or latest)' + description: 'Override the latest lane only (e.g., 2.102.0); the minimum lane always runs' required: false permissions: @@ -15,34 +15,42 @@ permissions: jobs: contract: name: gh ${{ matrix.gh-version }} contract tests + timeout-minutes: 30 strategy: fail-fast: false matrix: include: - - gh-version: '2.101.0' - required: true + - gh-version: '2.97.0' - gh-version: 'latest' - required: false runs-on: ubuntu-latest - continue-on-error: ${{ !matrix.required }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-dotnet@v6 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: global-json-file: global.json + cache: true + cache-dependency-path: '**/packages.lock.json' - name: Install gh ${{ matrix.gh-version }} env: GH_TOKEN: ${{ github.token }} + GH_VERSION_OVERRIDE: ${{ inputs.gh_version }} run: | - VERSION="${{ inputs.gh_version || matrix.gh-version }}" + VERSION="${{ matrix.gh-version }}" + if [ "$VERSION" = latest ] && [ -n "$GH_VERSION_OVERRIDE" ]; then + VERSION="$GH_VERSION_OVERRIDE" + fi + if [ "$VERSION" != latest ] && ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid gh version: $VERSION" >&2 + exit 1 + fi if [ "$VERSION" = "latest" ]; then # Install latest from GitHub's own apt repo (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ && sudo mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && out=$(mktemp) && wget -nv -O"$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat "$out" | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ && sudo apt update \ @@ -64,7 +72,7 @@ jobs: run: gh auth status - name: Restore - run: dotnet restore + run: dotnet restore --locked-mode - name: Build run: dotnet build --configuration Release --no-restore @@ -77,3 +85,33 @@ jobs: dotnet test --project tests/SkillView.Tests/SkillView.Tests.csproj \ --configuration Release --no-build \ --filter-trait "Category=Contract" + + notify-failure: + needs: contract + if: failure() + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - name: Create or reuse compatibility issue + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { owner, repo } = context.repo; + const title = 'GitHub CLI contract tests failed'; + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner, repo, state: 'open', per_page: 100, + }); + if (issues.some(issue => !issue.pull_request && issue.title === title)) return; + await github.rest.issues.create({ + owner, repo, title, assignees: [owner], + body: [ + 'A required SkillView contract test failed against the minimum supported or newest GitHub CLI.', + '', + `- [Failed workflow run](${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId})`, + '- Compare `gh skill` flags, agent selectors, JSON output, and installation behavior.', + '- Review whether `GhBinaryLocator.MinimumVersion` or `InstallAgentCatalog` needs an update.', + '- Keep this issue open until both required lanes pass.', + ].join('\n'), + }); diff --git a/.github/workflows/critical-dependencies.yml b/.github/workflows/critical-dependencies.yml new file mode 100644 index 0000000..f280c59 --- /dev/null +++ b/.github/workflows/critical-dependencies.yml @@ -0,0 +1,26 @@ +name: Critical dependencies + +on: + schedule: + - cron: '23 14 * * *' + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: critical-dependencies + cancel-in-progress: false + +jobs: + monitor: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Check Terminal.Gui packages and GitHub CLI releases + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + await require('./.github/scripts/critical-dependencies.js')({github, context, core}); diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index be28731..4bb75be 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -27,15 +27,15 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Build static site run: python3 site/build.py - name: Configure Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - name: Upload site - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 with: path: dist/skillview-site - name: Deploy Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61f2a70..53955b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,15 +6,35 @@ on: workflow_dispatch: permissions: - contents: write - issues: write + contents: read concurrency: group: release-${{ github.repository }} cancel-in-progress: false jobs: + preflight: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Validate release tag and reject published reruns + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then + echo "Invalid release tag: $RELEASE_TAG" >&2 + exit 1 + fi + if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "A release already exists for $RELEASE_TAG; use a new tag instead of replacing published bytes." >&2 + exit 1 + fi + build: + needs: preflight name: Build ${{ matrix.rid }} strategy: fail-fast: false @@ -43,7 +63,7 @@ jobs: attestations: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Linux AOT toolchain if: runner.os == 'Linux' @@ -51,9 +71,11 @@ jobs: sudo apt-get update sudo apt-get install -y clang zlib1g-dev - - uses: actions/setup-dotnet@v6 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: global-json-file: global.json + cache: true + cache-dependency-path: '**/packages.lock.json' - name: Determine version id: version @@ -67,7 +89,7 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" - name: Restore - run: dotnet restore + run: dotnet restore --locked-mode - name: Build run: dotnet build --configuration Release --no-restore @@ -81,6 +103,7 @@ jobs: dotnet publish src/SkillView.App/SkillView.App.csproj \ -c Release -r ${{ matrix.rid }} \ -p:PublishAot=true -p:StripSymbols=true \ + -p:RestoreLockedMode=true \ -p:Version=${{ steps.version.outputs.version }} \ -o dist/app @@ -90,9 +113,24 @@ jobs: dotnet publish src/SkillView.GhExtension/SkillView.GhExtension.csproj \ -c Release -r ${{ matrix.rid }} \ -p:PublishAot=true -p:StripSymbols=true \ + -p:RestoreLockedMode=true \ -p:Version=${{ steps.version.outputs.version }} \ -o dist/ext + - name: Smoke-test exact release binaries + shell: bash + run: | + set -euo pipefail + for binary in "dist/app/skillview${{ matrix.ext }}" "dist/ext/gh-skillview${{ matrix.ext }}"; do + test -s "$binary" + output=$("$binary" --version) + echo "$output" + [[ "$output" == *"${{ steps.version.outputs.version }}"* ]] || { + echo "Release binary has an unexpected version: $output" >&2 + exit 1 + } + done + - name: Stage release assets shell: bash run: | @@ -120,13 +158,13 @@ jobs: [System.IO.File]::WriteAllText($sumsFile, $content, [System.Text.Encoding]::ASCII) - name: Attest build provenance - uses: actions/attest-build-provenance@v4 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 with: subject-path: | dist/out/skillview-${{ matrix.asset_name }}${{ matrix.ext }} dist/out/gh-skillview-${{ matrix.asset_name }}${{ matrix.ext }} - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dist-${{ matrix.rid }} path: dist/out/* @@ -136,17 +174,39 @@ jobs: if: ${{ startsWith(github.ref, 'refs/tags/v') }} needs: build runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Download all artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: + pattern: dist-* path: artifacts/ merge-multiple: true - - name: List downloaded artifacts - run: ls -lh artifacts/ + - name: Verify the complete asset set and checksums + shell: bash + run: | + set -euo pipefail + expected=( + skillview-windows-amd64.exe skillview-windows-arm64.exe + skillview-linux-amd64 skillview-darwin-arm64 + gh-skillview-windows-amd64.exe gh-skillview-windows-arm64.exe + gh-skillview-linux-amd64 gh-skillview-darwin-arm64 + SHA256SUMS-windows-amd64.txt SHA256SUMS-windows-arm64.txt + SHA256SUMS-linux-amd64.txt SHA256SUMS-darwin-arm64.txt + ) + mapfile -t actual < <(find artifacts -maxdepth 1 -type f -printf '%f\n' | sort) + mapfile -t wanted < <(printf '%s\n' "${expected[@]}" | sort) + if [[ "${actual[*]}" != "${wanted[*]}" ]]; then + printf 'Expected:\n%s\nActual:\n%s\n' "${wanted[*]}" "${actual[*]}" >&2 + exit 1 + fi + for file in "${expected[@]}"; do test -s "artifacts/$file"; done + (cd artifacts && for sums in SHA256SUMS-*.txt; do sha256sum --check "$sums"; done) - name: Compose release notes env: @@ -186,7 +246,7 @@ jobs: EOF - name: Publish release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: files: | artifacts/skillview-* @@ -196,27 +256,45 @@ jobs: generate_release_notes: true append_body: true fail_on_unmatched_files: true + overwrite_files: false + draft: true prerelease: ${{ contains(github.ref_name, '-') }} + - name: Publish completed draft + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + count=$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets | length') + if [ "$count" -ne 12 ]; then + echo "Expected 12 published assets, found $count" >&2 + exit 1 + fi + gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false + # Dark-launch scaffold only. Generates a formula from the currently shipped # Unix release assets but does not commit or push to a tap yet. publish-homebrew: needs: [build, release] runs-on: ubuntu-latest + timeout-minutes: 10 if: ${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') && vars.HOMEBREW_TAP_ENABLED == 'true' }} env: RELEASE_REF: ${{ github.ref_name }} RELEASE_REPO: ${{ github.repository }} HOMEBREW_TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Download release artifacts - uses: actions/download-artifact@v8 - with: - pattern: dist-* - path: artifacts/ - merge-multiple: true + - name: Download and verify published release assets + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p artifacts + gh release download "$RELEASE_REF" --repo "$RELEASE_REPO" --dir artifacts + (cd artifacts && for sums in SHA256SUMS-*.txt; do sha256sum --check "$sums"; done) - name: Generate formula shell: bash @@ -238,7 +316,7 @@ jobs: printf 'Dark-launch only: generated formula for %s (target tap: %s)\n' "${RELEASE_REF}" "${HOMEBREW_TAP_REPO:-harder/homebrew-tap}" - name: Upload generated formula - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: homebrew-formula-${{ github.ref_name }} path: generated/homebrew/skillview.rb @@ -249,20 +327,35 @@ jobs: publish-winget: needs: [build, release] runs-on: windows-latest + timeout-minutes: 10 if: ${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') && vars.WINGET_ENABLED == 'true' }} env: RELEASE_REF: ${{ github.ref_name }} RELEASE_REPO: ${{ github.repository }} WINGET_PACKAGE_ID: harder.SkillView steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Download Windows release artifacts - uses: actions/download-artifact@v8 - with: - pattern: dist-win-* - path: artifacts/ - merge-multiple: true + - name: Download published Windows assets + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + New-Item -ItemType Directory -Force -Path artifacts | Out-Null + gh release download $env:RELEASE_REF --repo $env:RELEASE_REPO --pattern '*windows*' --dir artifacts + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Verify published Windows checksums + shell: pwsh + run: | + Get-ChildItem artifacts/SHA256SUMS-windows-*.txt | ForEach-Object { + Get-Content $_.FullName | ForEach-Object { + $parts = $_ -split '\s+', 2 + $name = $parts[1].TrimStart('*') + $actual = (Get-FileHash (Join-Path artifacts $name) -Algorithm SHA256).Hash + if ($actual -ne $parts[0]) { throw "Checksum mismatch: $name" } + } + } - name: Generate manifests shell: pwsh @@ -291,21 +384,23 @@ jobs: Write-Host "Dark-launch only: generated WinGet manifests for $env:WINGET_PACKAGE_ID $version" - name: Upload generated WinGet manifests - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: winget-manifests-${{ github.ref_name }} path: generated/winget/* retention-days: 30 notify-failure: - needs: [build, release, publish-homebrew, publish-winget] + needs: [preflight, build, release, publish-homebrew, publish-winget] if: failure() runs-on: ubuntu-latest + permissions: + issues: write env: RELEASE_REF: ${{ github.ref_name }} steps: - name: Create failure issue - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const title = `Release workflow failed on ${process.env.RELEASE_REF}`; @@ -334,6 +429,6 @@ jobs: '', `- Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`, '- Workflow guide: `.github/workflows/README.md`', - '- Rollback runbook: `docs/runbooks/release-rollback.md` *(planned backlog item #8)*' + '- Rollback runbook: `docs/runbooks/release-rollback.md`' ].join('\n') }); diff --git a/AGENTS.md b/AGENTS.md index ba5bcde..1d8324d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,16 +23,52 @@ the terminal, with both a full-screen TUI and scriptable CLI commands. ## How +- Install the SDK selected by `global.json`, then run `dotnet restore --locked-mode`. + NuGet lock files are committed for all five projects and cover `win-x64`, + `win-arm64`, `linux-x64`, and `osx-arm64`. Change dependencies with an + intentional `dotnet restore --force-evaluate`, commit the lock-file diff, + and confirm locked restore again. - Build/style verification: `dotnet build` - Full tests: `dotnet test --no-build` - Run product publish commands sequentially. The App and gh-extension publishes share `SkillView.Core` intermediate/output files, so parallel publishes can race while writing `SkillView.Core.deps.json` and cause false build failures. - Integration tests only: `dotnet test --project tests/SkillView.IntegrationTests/SkillView.IntegrationTests.csproj` (a bare project path no longer works under the .NET 10 SDK's `dotnet test` runner) -- Launch TUI: `src/SkillView.App/bin/Debug/net10.0/osx-arm64/skillview` +- Launch TUI with the built `src/SkillView.App` executable for the current RID. - For CLI global flags such as `--scan-root`, pass them **before** the subcommand: `skillview --scan-root /tmp/root list --json` +## Automation and dependencies + +- `.github/dependabot.yml` scans NuGet projects, GitHub Actions, and the .NET + SDK in `global.json`. It groups the two Terminal.Gui packages for joint + review, leaves unrelated third-party Actions separate, and assigns update + PRs to the maintainer. Verify a Dependabot PR with locked restore, tests, + and affected AOT hosts before merging. +- `.github/workflows/critical-dependencies.yml` checks stable NuGet releases + of Terminal.Gui and Terminal.Gui.Editor and the latest GitHub CLI release. + Its tested script opens deduplicated, assigned compatibility issues with + release links and concrete checks. It does not change the `gh` minimum or + merge package PRs automatically. Update its parsers/tests if version storage + changes. +- `ci.yml` checks Actions syntax, the dependency monitor, the static site, + dependency diffs, .NET tests on Linux/macOS/Windows, and four native AOT + targets. `codeql.yml` analyzes C# and Actions. `contract-tests.yml` tests + both the minimum supported `gh` and the latest `gh`; both lanes are required + and failures create a follow-up issue. +- `release.yml` uses locked restore, tests, AOT publishes, version and checksum + checks, provenance attestations, a complete-asset check, and a draft release + before publication. Published tag reruns are refused. Homebrew and WinGet + jobs remain opt-in artifact generation and consume published release bytes. +- Workflow Actions are pinned to full commit SHAs. Update the SHA and version + comment together after checking the upstream release. Keep permissions at + workflow or job minimums; issue writers need `issues: write`, releases need + `contents: write`, and provenance needs `id-token: write` and + `attestations: write`. +- `.github/copilot-instructions.md` links here and contains only advice for + Copilot review/coding automation. Keep general project facts and durable + invariants in this file or focused `agent_docs/` guides. + ## Critical agent notes - Keep this file and `agent_docs/` up to date as new durable agent-facing @@ -92,8 +128,7 @@ the terminal, with both a full-screen TUI and scriptable CLI commands. matching `.agents/skills` entry so the inventory scan actually discovers skills installed there — keep both in sync if this default ever changes. - Current package compatibility: SkillView is pinned to Terminal.Gui `2.5.0` - and Terminal.Gui.Editor `2.5.7`, the latest stable releases of each as of - 2026-09. Test projects use `Microsoft.NET.Test.Sdk` `18.10.1`, `xunit.v3` + and Terminal.Gui.Editor `2.5.7`. Test projects use `Microsoft.NET.Test.Sdk` `18.10.1`, `xunit.v3` `4.0.1`, and `xunit.runner.visualstudio` `4.0.0`. If tests fail to compile on missing `TestContext`, rerun `dotnet restore` so stale xUnit 2.x assets are replaced. `tests/SkillView.Tests/Build/PackageReferenceTests.cs` and diff --git a/Directory.Build.props b/Directory.Build.props index 19b3a4f..2ee1432 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,6 +6,9 @@ enable true true + true + + win-x64;win-arm64;linux-x64;osx-arm64 true SkillView SkillView diff --git a/agent_docs/release-engineering.md b/agent_docs/release-engineering.md index 7f387ef..bfbc953 100644 --- a/agent_docs/release-engineering.md +++ b/agent_docs/release-engineering.md @@ -3,15 +3,22 @@ - The release workflow lives in `.github/workflows/release.yml`. - Publish Native AOT standalone artifacts *and* `gh` extension binaries for four RIDs on native runners: `win-x64`, `win-arm64`, `linux-x64`, `osx-arm64`. - Release assets use Go's OS/arch vocabulary (`windows`/`linux`/`darwin`, `amd64`/`arm64`) for the asset suffix — required for `gh extension install` to auto-detect a precompiled binary, and kept consistent across every asset to reduce confusion. Standalone binaries: `skillview-windows-amd64.exe`, `skillview-windows-arm64.exe`, `skillview-linux-amd64`, `skillview-darwin-arm64`. `gh` extension binaries (built from `src/SkillView.GhExtension`, required naming `gh---[.exe]`): `gh-skillview-windows-amd64.exe`, `gh-skillview-windows-arm64.exe`, `gh-skillview-linux-amd64`, `gh-skillview-darwin-arm64`. Each platform also gets one `SHA256SUMS-.txt` covering both its binaries. -- Releases are published directly with `softprops/action-gh-release`, modeled after the simpler `winget-tui-sharp` release flow. +- NuGet dependencies are restored from committed lock files in locked mode. + Update lock files deliberately before cutting a tag. +- A tag preflight validates the version format and rejects a tag whose release + already exists. The release job requires all eight binaries and four checksum + files, verifies checksums, creates a draft with `softprops/action-gh-release`, + confirms its asset count, and only then publishes it. Do not overwrite + published release bytes; publish a corrected version from a new tag. - Linux AOT publish still needs `clang` and `zlib1g-dev`. - Keep `workflow_dispatch` enabled so release packaging can be exercised without pushing a tag; only tag pushes publish a GitHub Release. - `release.yml` now serializes publishes with a workflow-level concurrency lock. -- Each release build leg restores, builds in `Release`, and runs the full test suite before publishing AOT assets. +- Each release build leg restores, builds in `Release`, runs the full test suite, + publishes both AOT hosts sequentially, and checks each binary's `--version`. - CI's standalone AOT smoke publish treats `IL2026`, `IL3050`, and `IL3053` as errors so the app entrypoint stays warning-clean even while the gh extension keeps its local suppression. - Release artifact uploads keep 30-day retention, and a failed release opens or reuses an issue with the run link for follow-up. - `.github/workflows/README.md` is the operator-facing overview for CI/release workflow behavior. - `docs/runbooks/release-rollback.md` is the rollback procedure for live GitHub Releases and the current dark-launch package-manager jobs. -- Homebrew dark-launch scaffolding lives in `packaging/homebrew/skillview.rb.tmpl` and currently generates a formula artifact for the shipped Unix targets (`darwin-arm64` and `linux-amd64`) from stable-tag assets. -- WinGet dark-launch scaffolding lives in `packaging/winget/` and currently only generates manifest artifacts for package id `harder.SkillView`. +- Homebrew dark-launch scaffolding lives in `packaging/homebrew/skillview.rb.tmpl` and currently generates a formula artifact for the shipped Unix targets (`darwin-arm64` and `linux-amd64`) from published stable-tag assets. +- WinGet dark-launch scaffolding lives in `packaging/winget/` and currently only generates manifest artifacts for package id `harder.SkillView` from published Windows assets. - Keep package-manager jobs gated behind repo variables (`HOMEBREW_TAP_ENABLED`, `HOMEBREW_TAP_REPO`, `WINGET_ENABLED`) until real publish automation is ready. diff --git a/agent_docs/running-tests.md b/agent_docs/running-tests.md index d9a8806..2c3093c 100644 --- a/agent_docs/running-tests.md +++ b/agent_docs/running-tests.md @@ -30,10 +30,17 @@ logs show the args reached the process. `--treenode-filter` (the platform-level filter option) also doesn't work against xunit v3/v4 here — use the `--filter-*` flags instead. -- **Default verification:** `dotnet build` then `dotnet test --no-build` (from repo root; runs both test projects). +- **Default verification:** `dotnet restore --locked-mode`, `dotnet build`, then `dotnet test --no-build` (from repo root; runs both test projects). - **UI-focused tests:** `dotnet test --project tests/SkillView.Tests/SkillView.Tests.csproj --filter-namespace "SkillView.Tests.Ui"` - **Resource stress tests:** `dotnet test --project tests/SkillView.Tests/SkillView.Tests.csproj --filter-class "SkillView.Tests.Ui.ResourceStressTests"`. These allocation/process tests are serialized through the `ResourceStress` collection. - **Contract tests:** require a real `gh` binary plus auth. CI runs them with `SKILLVIEW_CONTRACT_TESTS=true dotnet test --project tests/SkillView.Tests/SkillView.Tests.csproj --configuration Release --no-build --filter-trait "Category=Contract"` via `.github/workflows/contract-tests.yml`. - **PTY startup smoke:** opt-in only. Run `dotnet build`, then `SKILLVIEW_PTY_TESTS=true dotnet test --project tests/SkillView.Tests/SkillView.Tests.csproj --filter-trait "Category=PTY"`. The test uses the built `src/SkillView.App/bin/Debug/net10.0//skillview` host under `/usr/bin/script`, supplies the terminal-probe responses that `script` itself cannot provide, and expects either `GH_TOKEN` or a working `gh auth token`. Keep failure cleanup bounded: terminate and wait for the child before reading redirected streams to EOF. -- The contract-test workflow runs nightly and on `workflow_dispatch` against a pinned `gh` lane (kept in step with the latest adopted release — see `.github/workflows/contract-tests.yml`) that must pass plus a `latest` lane allowed to fail. Keep the pinned lane at or above `GhBinaryLocator.MinimumVersion` — pinning below SkillView's own hard minimum tests a `gh` version the app refuses to run against. +- The contract-test workflow runs daily and on `workflow_dispatch` against + **two required lanes**: the `GhBinaryLocator.MinimumVersion` (currently + 2.97.0) and the latest GitHub CLI release. A manual version override applies + only to the latest lane. A failure opens an assigned compatibility issue. + Keep the minimum lane equal to the app's hard minimum. +- **Automation checks:** `node --test .github/scripts/critical-dependencies.test.js`, + `python site/build.py`, and `node --check site/app.js`. CI also runs + `actionlint` and dependency review for PRs. - For PTY-driven TUI checks, prefer the built binary and follow `agent_docs/tui-pty-testing.md`. diff --git a/docs/runbooks/release-rollback.md b/docs/runbooks/release-rollback.md index 3de45cf..6b369a6 100644 --- a/docs/runbooks/release-rollback.md +++ b/docs/runbooks/release-rollback.md @@ -12,13 +12,20 @@ Use this runbook when a `release.yml` run publishes a bad `gh-skillview` / `skil 1. Open the failed or suspect workflow run. 2. Determine whether the bad state is: - a failed build with no release published + - an unpublished draft release with incomplete assets - a published GitHub Release with bad binaries or metadata - only generated Homebrew / WinGet artifacts from dark-launch jobs 3. Capture the run URL and note the affected ref/tag in the failure issue, if one exists. ## GitHub Release rollback -If the GitHub Release for a tag is already live and should be withdrawn, remove the release and its tag together rather than editing assets in place. +If a draft is incomplete, inspect the failed run and delete the **draft** +before retrying the same tag. A published release is immutable when the +repository setting is enabled: its assets cannot be replaced. Withdraw the +affected release if necessary, fix the code, and publish a **new version tag**. +Never reuse the withdrawn tag name. + +For a published release that must be withdrawn: ```bash gh release delete --repo harder/gh-skillview --cleanup-tag -y @@ -47,15 +54,18 @@ After deletion: ``` 3. If the underlying commit is also bad, revert or fix it on `main` before cutting a replacement tag. -4. Re-run the release from a corrected tag only after the replacement build is verified locally and in CI. +4. Publish from a new corrected tag only after the replacement build is + verified locally and in CI. Immutable release tags cannot be reused after + their release is deleted. ## Failed release before publish If `build` failed or the workflow stopped before the `release` job published assets: -1. Do **not** create a new tag yet. +1. Check for an incomplete draft release and remove it before retrying. 2. Fix the underlying issue on `main`. -3. Re-run the workflow on the existing tag only if no release was published, or cut a replacement tag if the old tag was already used for a live release and then rolled back. +3. Re-run the workflow on the existing tag only if no release was published. + If it was live, cut a new tag after the fix. ## Homebrew / WinGet dark-launch rollback diff --git a/packaging/homebrew/skillview.rb.tmpl b/packaging/homebrew/skillview.rb.tmpl index 8511963..0af79fd 100644 --- a/packaging/homebrew/skillview.rb.tmpl +++ b/packaging/homebrew/skillview.rb.tmpl @@ -17,7 +17,8 @@ class Skillview < Formula end def install - bin.install "skillview" => "skillview" + asset = OS.mac? ? "skillview-darwin-arm64" : "skillview-linux-amd64" + bin.install asset => "skillview" end test do diff --git a/src/SkillView.App/packages.lock.json b/src/SkillView.App/packages.lock.json new file mode 100644 index 0000000..bd4177d --- /dev/null +++ b/src/SkillView.App/packages.lock.json @@ -0,0 +1,308 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ==" + }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==" + }, + "ColorHelper": { + "type": "Transitive", + "resolved": "1.8.1", + "contentHash": "rblWXd/02TfwJjX53xEfQnbnDRKxCm09VtavV8JaIcmIFWt9efNf6xTQ3WeXgy/she5R31eRTtnizFoUoDrs5w==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2026.2.0", + "contentHash": "drvAEaI7Xf4wU6dx4UTB9MoZWXZlraaQ0qoeThPQ411FxNhId7QdUTukIC9I8aUHP0ycAaGwZNKqSvfOZQvUlg==" + }, + "Markdig": { + "type": "Transitive", + "resolved": "1.3.2", + "contentHash": "fZgOC/3CswUrndjDTac70aQpYdtxbW5+5bRumR7vzvI2HJbkmgKisB1c9oT+GA6v0jB/JDR9BLa9FiPzQmaK6A==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "wlhRqZW8LcJPa+vk2oLAc/REXDItHtkFQdf/QcXYGZbZOO13izcsKY1pCvuFQYwUiZD+hwSZwsKASjqT+BNaVg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "fVi053xdpda9Em7vSkmgVxO/PtgC2m78ekReKWsgcyskqY0U82Bz/MONwxpGzI0hElYKJfw+fupqMVeKW3fSaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "rFn8RuszZn3qquPVkDytMUlPc2+rXl9MCoygwc1XmAgC5vg5/oXJ8hkOosOrLoBLsqdTy4lFwP6iQdPS9uSYOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "mDW7KVFB05M6jiRUyaZiOMWhS31n5HlSZwoYctHAZAucD4sMDJ70IxOmkGDt6RpstchD+keWBjhdzcMpSkWvWQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Physical": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "nSPrT8U/cNoB4coqkmnanAMK9PsL7lsjG+LLUKEwHRFwS6E78b8S1wdv/y88EOxBhasWov1rLd7RTHmmsYPOLg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "/a1aJz4m7ylhEDf25ugQChLQoN5XwoGjWw/BoR/ZWWKsO1v4DdJElS1uyngahz4B/eOzjFk1KNTkarRLE5wsIg==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "JOjac6SQQgZmdmB8WGEw61/7siqMZoWJMkmq2p1goJGxqI59lO6oB4bOl0jNsbaPBdYy5Mlkb+6U7T4+CjnD8Q==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "Tq/UqMaczePv9yWwSsJZRgKtgA46djVR5xHj/lZBCueQ3ag8f9v5mu0EdhrNx7tXxNk+Y9OurG2oKuSKINjr0A==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "2i6rtW/B5rCnWCnhdmWWEmaM9O0HD0zsPY9eRqa++y4tclI3Uw8zvGbBvhY/LjAdtf8gUHhUPcAWj3DRlWMXmQ==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.7", + "contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "eY1GAKcTfD2maP27J84X9IovT3yjHJ2dVDzPmDg6/XqYvt3jMzJhtfQCLjG9pVsZGAd+8DQ2QrjaDcs2+VQLGw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "SXcz+kF+4Oo9b1+55zntpJFYfwb1jw66ioxptyNOOTDc8g2FHnBFWjZpsWfCvZIhzr0x+4e2trVTs4OKwQfBtw==" + }, + "Onigwrap": { + "type": "Transitive", + "resolved": "1.0.11", + "contentHash": "5/WAxSYWfiiPzp1X13qqdqjFmYLKDD/U7Vh3WCkKxWG9BjqdrXnQE5fCKCFJX+xWvuYLTxwZIVWBwBWObViE8g==" + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "22.2.0", + "contentHash": "fwnNXyIPb7ABPLae6K9YZz0FAkRjb2s/nElRWrg73usMveY/qdOHjKaNvAF7iAfmzTl1W4wa7S/SnmIc4LDvTg==", + "dependencies": { + "TestableIO.System.IO.Abstractions": "22.2.0", + "TestableIO.System.IO.Abstractions.Wrappers": "22.2.0" + } + }, + "Terminal.Gui": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "TSSgilWaPD0powRvSYhroPVsz+3Q0E4bgrDg4NDpGI8FLjgvPqZue8FSroQz6XtcyvdpCjOF9xzQnJTQf3hLGg==", + "dependencies": { + "ColorHelper": "1.8.1", + "JetBrains.Annotations": "2026.2.0", + "Markdig": "1.3.2", + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Binder": "10.0.11", + "Microsoft.Extensions.Configuration.Json": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.7", + "Microsoft.Extensions.Options": "10.0.11", + "System.IO.Abstractions": "22.2.0", + "TextMateSharp": "2.0.4", + "TextMateSharp.Grammars": "2.0.4", + "Wcwidth": "4.0.1" + } + }, + "Terminal.Gui.Editor": { + "type": "Transitive", + "resolved": "2.5.7", + "contentHash": "7AjENfZ2xfRvaDQLXrkxttSkr1XAynH6KlRyn8eRSVpzKtthBUki3LV20AaN174Jg2yXQV+e017Mb5vs0d+4Bw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", + "Terminal.Gui": "2.4.17" + } + }, + "TestableIO.System.IO.Abstractions": { + "type": "Transitive", + "resolved": "22.2.0", + "contentHash": "BQPyr0A1s2HvpVTZIyolrZ3ZdPn8DU2tqciaK1oco3pjm76fiKIk75eup4R9reZjNS75M+WJIICOlNX5OFt50g==", + "dependencies": { + "Testably.Abstractions.FileSystem.Interface": "10.3.0" + } + }, + "TestableIO.System.IO.Abstractions.Wrappers": { + "type": "Transitive", + "resolved": "22.2.0", + "contentHash": "0GW9i46ozm687WGAWsBHWMhLR7l1HmKwhiSU2hQrbFnI5m2YRfqXwqTQNjnOwT8BdbvcGqwMr97/zlnPWa1u1Q==", + "dependencies": { + "Testably.Abstractions.FileSystem.Interface": "10.3.0" + } + }, + "Testably.Abstractions.FileSystem.Interface": { + "type": "Transitive", + "resolved": "10.3.0", + "contentHash": "5ebMaoADkzjfW7GnZYKaUebV8XmUn++C/4sKk1PTzCiddEgbSKBq3pW4EWEi70rYJAK0gA1yDWrYFeHq18FvHQ==" + }, + "TextMateSharp": { + "type": "Transitive", + "resolved": "2.0.4", + "contentHash": "5Pvn+A4zb1IF+ACAVhgw1aGf9kTyx6j7v5hdup7nCS89Nzby8KQAlCfQ54VC/GAsEoQwh4zKa5MKlAaql8nkPg==", + "dependencies": { + "Onigwrap": "1.0.11" + } + }, + "TextMateSharp.Grammars": { + "type": "Transitive", + "resolved": "2.0.4", + "contentHash": "jzOY00q5u5Vs4L9n2m2PnRZDGwWt2uwkOLh7qlVdCx1kkGSn5eaW5XgnYCyTzEbsCwLV2xlEolnNkr/wuIMAbA==", + "dependencies": { + "TextMateSharp": "2.0.4" + } + }, + "Wcwidth": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "PMU8u3pGf9nUkee39KMrB8qqId7u7kyGWaZpOh9E4Mal3xChq+relS/hHQOicGAswyzi7F5DjXjac0mTnRBX8Q==" + }, + "skillview.core": { + "type": "Project", + "dependencies": { + "Terminal.Gui": "[2.5.0, )", + "Terminal.Gui.Editor": "[2.5.7, )" + } + } + }, + "net10.0/linux-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ==", + "dependencies": { + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.12" + } + }, + "Onigwrap": { + "type": "Transitive", + "resolved": "1.0.11", + "contentHash": "5/WAxSYWfiiPzp1X13qqdqjFmYLKDD/U7Vh3WCkKxWG9BjqdrXnQE5fCKCFJX+xWvuYLTxwZIVWBwBWObViE8g==" + }, + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "wAa+LAE6+Bkd+pEJvGdnQU8PZQRzvhurpQ4hzsOTpRuedIq1kunJcz86xBCBlzsc+QlVdCKVUPzEfB3uYVB7Gw==" + } + }, + "net10.0/osx-arm64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ==", + "dependencies": { + "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": "10.0.12" + } + }, + "Onigwrap": { + "type": "Transitive", + "resolved": "1.0.11", + "contentHash": "5/WAxSYWfiiPzp1X13qqdqjFmYLKDD/U7Vh3WCkKxWG9BjqdrXnQE5fCKCFJX+xWvuYLTxwZIVWBwBWObViE8g==" + }, + "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "WwM79vRmPYfsV7wVjFVYaZWc853weM+HpY917SbWI52go/PU+A2L24fMaGkYB3bunhjiLb/SMNlNT0fuyjbQqA==" + } + }, + "net10.0/win-arm64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ==", + "dependencies": { + "runtime.win-arm64.Microsoft.DotNet.ILCompiler": "10.0.12" + } + }, + "Onigwrap": { + "type": "Transitive", + "resolved": "1.0.11", + "contentHash": "5/WAxSYWfiiPzp1X13qqdqjFmYLKDD/U7Vh3WCkKxWG9BjqdrXnQE5fCKCFJX+xWvuYLTxwZIVWBwBWObViE8g==" + }, + "runtime.win-arm64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "2qwg37KiPUnsvPjdLhAMjcRTxLFAC5BhIBkJd1HEBGzgSK4WT+gg/C+kh5Gwg53EQv6xTqF2oGks+c49JLa8jw==" + } + }, + "net10.0/win-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ==", + "dependencies": { + "runtime.win-x64.Microsoft.DotNet.ILCompiler": "10.0.12" + } + }, + "Onigwrap": { + "type": "Transitive", + "resolved": "1.0.11", + "contentHash": "5/WAxSYWfiiPzp1X13qqdqjFmYLKDD/U7Vh3WCkKxWG9BjqdrXnQE5fCKCFJX+xWvuYLTxwZIVWBwBWObViE8g==" + }, + "runtime.win-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "clsgU9GnioCJ+PBzQTCoJHxLXRU+7O/BzYrwRT8CUP7jgyYjNgJmNsQ/kbzAA7MG5kDvFoFvIBGHGzYdINh/EQ==" + } + } + } +} \ No newline at end of file diff --git a/src/SkillView.Core/SkillView.Core.csproj b/src/SkillView.Core/SkillView.Core.csproj index 9ed1e4d..9651d0d 100644 --- a/src/SkillView.Core/SkillView.Core.csproj +++ b/src/SkillView.Core/SkillView.Core.csproj @@ -4,6 +4,9 @@ Library SkillView SkillView.Core + + true 2.5.0 2.5.7