Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Release

# Fires on any v*.*.* tag push (and prerelease suffixes -alpha/-beta/-rc).
# Version comes from Directory.Build.props. Publishes via NuGet Trusted
# Publishing (OIDC — no long-lived API key). Setup steps in docs/RELEASING.md.
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
inputs:
dry_run:
description: "Pack and validate only — skip nuget push"
type: boolean
default: true

jobs:
release:
runs-on: ubuntu-latest
environment: release
permissions:
contents: write # for creating the GitHub Release
id-token: write # required for Trusted Publishing OIDC exchange
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Restore
run: dotnet restore shelldocs.slnx

- name: Build
run: dotnet build shelldocs.slnx --configuration Release --no-restore

- name: Test
run: dotnet test shelldocs.slnx --configuration Release --no-build --verbosity normal

- name: Pack
run: dotnet pack shelldocs.slnx --configuration Release --no-build --output nupkgs

- name: List produced packages
run: ls -la nupkgs/

# Runs immediately before push — the temp API key is valid only 1 hour.
# `user` is the nuget.org profile name (NOT email, NOT the GH org name),
# kept as a secret so it never lives in the workflow file.
- name: Login to NuGet via Trusted Publishing
if: github.event_name == 'push' || inputs.dry_run == false
id: nuget-login
uses: NuGet/login@v1
with:
user: ${{ secrets.NUGET_USER }}

- name: Push to NuGet
if: github.event_name == 'push' || inputs.dry_run == false
env:
NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }}
run: |
for pkg in nupkgs/*.nupkg; do
echo "Publishing $pkg"
dotnet nuget push "$pkg" \
--api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
done

- name: Create GitHub Release
if: github.event_name == 'push'
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: nupkgs/*.nupkg
99 changes: 99 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Changelog

All notable changes to ShellDocs land here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning is [SemVer](https://semver.org/spec/v2.0.0.html) with prerelease suffixes (`-alpha`, `-beta`, `-rc`) — the alpha window explicitly reserves the right to break APIs on minor bumps.

## [Unreleased]

## [0.1.0-alpha] — 2026-07-25

First public release. The whole Phase 1 target is shipped, plus most of Phase 2's primitives + consumer DX polish. See [ROADMAP.md](docs/ROADMAP.md).

### Packages

Published to NuGet:

- `ShellDocs.CLI` — global tool: `dotnet tool install -g ShellDocs.CLI --prerelease`. Commands: `init`, `add`, `dev`, `build`, `preview`
- `ShellDocs.Components` — RCL with `<DocsLayout>`, `<DocsHeader>`, `<DocsSidebar>`, `<TableOfContents>`, `<PrevNextNav>`, `<DocsBreadcrumb>`, `<SearchDialog>`, content primitives, API-reference primitives
- `ShellDocs.Core` — navigation graph, search index model, routing helpers, markdown plain-text extractor
- `ShellDocs.Markdown` — Markdig pipeline with frontmatter, `razor:preview` fenced blocks, inline Razor component tags
- `ShellDocs.Templates` — starter markdown + Program.cs snippets for `shelldocs init` scaffolding
- `ShellDocs.Tokens` — RCL with `tokens.css` — shadcn-compatible palette + spacing scale, single source of truth for `--background`, `--foreground`, `--primary`, `--radius`, dark mode

### Added

**Markdown pipeline (`ShellDocs.Markdown`)**
- YAML frontmatter parsing via YamlDotNet
- ` ```razor:preview ` fenced blocks — live-rendered previews with source-view toggle
- Inline Razor component tags mid-markdown (`<Callout />`, `<Card ... />`)
- Component type registry (`RegisterComponent<T>()`) with per-type tag aliases (`RegisterComponent<Button>("Btn")`)
- Bulk `RegisterComponentsFromAssembly<TMarker>()` scan + `[ShellDocsIgnore]` opt-out attribute
- Automatic string→typed coercion for `bool`, `int`, `enum` attribute values

**Content primitives (`ShellDocs.Components`)**
- `<Callout Variant="info|warning|danger|tip">` — coloured info box with icon + title + body
- `<Card>` / `<CardGrid Columns="1|2|3">` / `<LinkCard>` — responsive card family
- `<Steps>` / `<Step>` — CSS-counter numbered list with badge-on-rail spine
- `<FileTree>` / `<FileTreeItem>` — recursive project-layout diagram
- `<CodeGroup SyncKey>` / `<CodeTab>` — tabbed code samples with cross-page sync

**API-reference primitives (`ShellDocs.Components`)**
- `<TypeTable>` / `<TypeRow Name Type Default Description Required>` — props/API reference table
- `<ComponentPreview Component="..." ...props>` — declarative-prop single-component demos

**Chrome (`ShellDocs.Components`)**
- `<DocsLayout>` with two variants (`TopNav`, `Sidebar` floating card)
- `<DocsHeader>` with primary nav mega-menu, GitHub link, theme toggle
- `<DocsSidebar>` with grouped nav, collapsible sections (animated grid-rows), auto-open on active path
- `<TableOfContents>` — right-rail, h2/h3 auto-extraction, scroll-spy indicator with smooth slide
- `<PrevNextNav>` — auto-derived from nav-graph adjacency
- `<DocsBreadcrumb>` — auto-generated from nav path; sections render as text, current page as `aria-current`, only leaf pages become links
- `<PackageSelector>` — consumer-configurable multi-package selector; hides when 0 or 1 packages declared
- `<BrandLogo>` — consumer-configurable logo with three modes: `LogoSvg` (inline SVG, tints via `currentColor`), `LogoLight`/`LogoDark` (theme-paired image URLs), or dot placeholder fallback
- `<SearchDialog>` — Cmd+K modal, client-side substring scoring against title / description / section / body, snippet extraction for body-only matches
- `<DocsFooter>` / `<DocsMobileBar>` / `<ThemeToggle>`

**Auto-chrome via `DocsPageState`**
- Consumer's docs page collapses to just `<MarkdownContent Document="_document" />` — TOC, PrevNext, Breadcrumb all auto-render from a shared scoped service
- Recomputes on `NavigationManager.LocationChanged`

**Search (`ShellDocs.Core`)**
- `SearchIndex.FromGraph()` — page + heading entries with URL, title, description, section
- Page entries carry extracted plain-text `Body` (frontmatter / fences / HTML / Razor tags / images / links / inline code / emphasis / heading `#` all stripped)
- `MarkdownPlainText.Extract()` — reusable helper for body extraction, 8KB default cap

**Code highlighting (`ShellDocs.Components`)**
- Shiki via WASM (bundle configurable)
- Dual-theme via `--shiki-light` / `--shiki-dark` CSS custom properties

**Design tokens (`ShellDocs.Tokens`)**
- Standalone RCL with `tokens.css` (base + full variants)
- Shadcn-compatible variable names for interop with ShellUI and other consumers

**CLI (`ShellDocs.CLI`)**
- `shelldocs init` — two modes: create (default, scaffolds a fresh Blazor Web App) and attach (`--attach`, augments existing project via `SHELLDOCS_SETUP.md`)
- `shelldocs add <component|guide|page> <name>` — scaffolds starter `.md` from template into `content/`
- `shelldocs dev` — dotnet watch with .md hot-reload
- `shelldocs build` — publishes static site, handles base-href rewrite + SPA 404 fallback

**Animation polish (Phase 2)**
- Native view-transitions API for cross-fade on route change (Chromium — silent no-op elsewhere)
- Sidebar section collapse animates via `grid-template-rows: 0fr → 1fr`
- Copy-icon success bounce
- Global `@media (prefers-reduced-motion: reduce)` guard — all animations collapse to instant

**Consumer configuration (`ShellDocsOptions`)**
- `RegisterComponentsFromAssembly<TMarker>(filter?)` — bulk-register a whole component library in one line
- `AddPackage(id, title, description, rootUrl, iconPath?)` — declares consumer's package family for the sidebar selector
- `SetLogo(url)` / `SetLogo(light, dark, alt?)` / `LogoSvg` — brand logo
- `AddNavLink` / `AddNavMenu` — top-nav wiring
- `LayoutVariant` — TopNav or Sidebar

### Known limitations

- Body-text search uses substring scoring, not an inverted index — fine for docs-sized corpora (~100 pages), will need rebuilding at 1000+
- Search snippets don't yet highlight the matched substring
- `<TypeTable>` is hand-authored today; XML-doc auto-generation ships in `ShellDocs.Xml` (Phase 4)
- No `<DocsBreadcrumb>` opt-out — currently hides when the trail has ≤ 1 node, otherwise always renders

[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.0-alpha...HEAD
[0.1.0-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.0-alpha
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**The docs framework for .NET.** Beautiful, animated, Cmd+K-searchable documentation sites, powered by Blazor and Tailwind. Compose with ShellUI (or any Blazor component library) — like fumadocs composes with shadcn/ui.

> Status: **`0.1.0-alpha` in progress.** Not yet published to NuGet. See [ROADMAP](docs/ROADMAP.md).
> Status: **`0.1.0-alpha`** — first public release. See [CHANGELOG](CHANGELOG.md) and [ROADMAP](docs/ROADMAP.md). Publish steps live in [docs/RELEASING.md](docs/RELEASING.md).

## Why ShellDocs

Expand Down
105 changes: 105 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Releasing ShellDocs

Runbook for cutting a NuGet release. First time = read start to finish; steady state = jump to "Steady-state release" at the bottom.

## One-time setup (before the very first release)

We use **NuGet Trusted Publishing** — the workflow requests a short-lived (1-hour) API key from nuget.org via OIDC on each run. No long-lived API key stored as a secret. Official docs: <https://learn.microsoft.com/nuget/nuget-org/trusted-publishing>.

### 1. Verify package IDs are available on NuGet

Run once, before you register anything, so you don't discover a naming collision at t=publish:

```powershell
foreach ($id in "ShellDocs.CLI","ShellDocs.Components","ShellDocs.Core","ShellDocs.Markdown","ShellDocs.Templates","ShellDocs.Tokens") {
Write-Host "-- $id"
dotnet nuget search $id --exact-match --source https://api.nuget.org/v3/index.json | Select-String $id
}
```

If any ID is taken by another author, decide: rename (`ShellUI.ShellDocs.*`?) or reach out to the owner. Do NOT publish under a different-looking name and hope no one notices — that's how brand-confusion issues start.

### 2. Create the `release` GitHub environment

Repo → Settings → Environments → **New environment** → name it exactly `release`.

Nothing to configure inside (optional: add required reviewers if you want a manual gate on each publish). The environment's existence is what the workflow's `environment: release` line references, and matching against the TP policy in step 3 is what proves this workflow is what it says it is.

### 3. Register a Trusted Publishing policy on NuGet

1. Sign in at [nuget.org](https://www.nuget.org/) → click your username → **Trusted Publishing** → **Add**
2. Choose the owner (individual user OR organization — the policy applies to all packages owned by that account)
3. Fill (all values are case-insensitive):
- **Repository Owner:** `shellui-dev` (the GitHub organization/user name)
- **Repository:** `shelldocs`
- **Workflow File:** `release.yml` — **filename only**, no `.github/workflows/` prefix
- **Environment:** `release` — must match `environment: release` in our workflow. If you skip this, remove `environment: release` from the workflow too, or the policy match will fail.
4. Save.

**Note on private repos:** first-time policies for private GitHub repos are provisional for 7 days. NuGet needs to see one successful publish (which carries GitHub's repository + owner IDs in the OIDC token) to lock the policy permanently. If no publish happens in 7 days, the policy goes inactive — you'd re-activate it and try again.

### 4. Add the `NUGET_USER` secret

The workflow's `NuGet/login@v1` action needs your **nuget.org profile username** (NOT email, NOT the GitHub org name — the visible profile name you sign in with, e.g. what shows on `nuget.org/profiles/<name>`).

Repo → Settings → Secrets and variables → Actions → New repository secret:
- **Name:** `NUGET_USER`
- **Value:** your nuget.org profile name

### 5. Local pack dry-run

Confirm the pack works locally before trusting CI. From repo root:

```powershell
./scripts/pack-dry-run.ps1
```

The script packs every `IsPackable=true` project into `./nupkgs-dryrun/`, prints IDs + sizes, and verifies `README.md` is embedded in each. Any missing README or unexpected package = fix before releasing.

### 6. First release — expect the 7-day provisional window

The very first `git push origin v0.1.0-alpha` triggers the workflow, which does OIDC exchange, publishes, and locks the policy permanently. Watch the Actions tab — if OIDC exchange fails, the most likely causes (in order) are: `NUGET_USER` secret missing or wrong, TP policy's `Workflow File` field includes a path prefix (should be just `release.yml`), or workflow's `environment: release` doesn't match the policy's Environment field.

## Steady-state release

Once the one-time setup is done, cutting a release is three commands.

### 1. Bump the version

Edit `Directory.Build.props` → `<Version>0.X.Y[-suffix]</Version>`. That propagates to every packable project via the shared props file.

For a prerelease bump: `0.1.0-alpha` → `0.1.1-alpha` (patch) or `0.2.0-alpha` (minor).
For the first stable: strip the `-alpha` suffix → `1.0.0`.

### 2. Update `CHANGELOG.md`

Move the entries out of `[Unreleased]` into a new dated section (`[0.1.1-alpha] — YYYY-MM-DD`). Update the comparison links at the bottom.

### 3. Commit, tag, push

```bash
git add Directory.Build.props CHANGELOG.md
git commit -m "chore: release 0.X.Y[-suffix]"
git tag "v0.X.Y[-suffix]"
git push
git push origin "v0.X.Y[-suffix]"
```

The tag push triggers `.github/workflows/release.yml`:
1. Builds Release
2. Runs the test suite
3. Packs every `IsPackable=true` project
4. Pushes each `.nupkg` to nuget.org (`--skip-duplicate` so re-runs are safe)
5. Creates a GitHub Release from the tag with auto-generated notes

Watch the run under Actions. If NuGet push fails on one package (e.g. `409 Conflict — already exists`), `--skip-duplicate` handles it silently; a real failure (bad API key, network) will surface as a red X.

## Dry-run without publishing

To validate the whole workflow without shipping to NuGet, go to Actions → Release → Run workflow → check "Pack and validate only". Runs build + pack, skips the push step.

## After the release

- Verify the packages appear at `https://www.nuget.org/packages/ShellDocs.CLI/`, etc. (indexing takes a few minutes)
- Test the install locally: `dotnet tool install -g ShellDocs.CLI --prerelease` in a scratch directory
- Announce as appropriate (blog post / X / whatever). Alpha releases are usually announced only internally
65 changes: 65 additions & 0 deletions scripts/pack-dry-run.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Packs every IsPackable=true project into ./nupkgs-dryrun/ and validates each
# .nupkg — verifies README embed, checks size, prints package ID + version.
# Run from repo root.
#
# ./scripts/pack-dry-run.ps1

$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
Set-Location $repoRoot

$outDir = Join-Path $repoRoot "nupkgs-dryrun"
if (Test-Path $outDir) { Remove-Item $outDir -Recurse -Force }
New-Item -ItemType Directory -Path $outDir | Out-Null

Write-Host ""
Write-Host "-> dotnet pack shelldocs.slnx -c Release -o $outDir"
Write-Host ""
dotnet pack shelldocs.slnx --configuration Release --output $outDir
if ($LASTEXITCODE -ne 0) {
Write-Host "PACK FAILED" -ForegroundColor Red
exit 1
}

Write-Host ""
Write-Host "Produced packages:"
Write-Host ""

$fail = 0
foreach ($nupkg in Get-ChildItem $outDir -Filter *.nupkg | Sort-Object Name) {
$sizeKB = [math]::Round($nupkg.Length / 1KB, 1)
Write-Host (" {0} ({1} KB)" -f $nupkg.Name, $sizeKB)

# A .nupkg is a zip — extract to a temp dir to inspect.
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("nupkg-check-" + [guid]::NewGuid().ToString("N").Substring(0, 8))
Expand-Archive -Path $nupkg.FullName -DestinationPath $tmp -Force

# Every packable project ships README.md via <PackageReadmeFile>.
$readme = Get-ChildItem $tmp -Filter README.md -Recurse | Select-Object -First 1
if (-not $readme) {
Write-Host " MISSING README.md" -ForegroundColor Red
$fail++
}

# Sanity: nuspec present with expected version.
$nuspec = Get-ChildItem $tmp -Filter *.nuspec | Select-Object -First 1
if ($nuspec) {
$xml = [xml](Get-Content $nuspec.FullName)
$id = $xml.package.metadata.id
$ver = $xml.package.metadata.version
Write-Host " id=$id version=$ver"
}

Remove-Item $tmp -Recurse -Force
}

Write-Host ""
if ($fail -gt 0) {
Write-Host "$fail package(s) failed validation" -ForegroundColor Red
exit 1
}
Write-Host "OK — all packages passed validation" -ForegroundColor Green
Write-Host ""
Write-Host "Ship it with:"
Write-Host " git tag v<version>"
Write-Host " git push origin v<version>"
Loading
Loading