string? VersionText { get; set; }
Source
Gets the SemVer 2.0 release version without a leading v
.
Resolves prepared release artifacts into a tagged projection bound to an annotated Git tag.
This is the single authority for the prepared-to-tagged transition. It reads tag objects and artifact blobs through Git, validates the checked-in evidence bundle and canonical tag trailers, then returns an in-memory sidecar projection. It never mutates repository files, creates tags, or calls GitHub.
Task<string> GenerateTagMessageAsync(SemVer version, CancellationToken cancellationToken)
Generates canonical annotated-tag trailers from the prepared artifacts at HEAD.
version
cancellationToken
Canonical trailer block with a trailing newline.
Task<ReleaseTaggedProjection> ResolveAsync(ReleaseOptions options, CancellationToken cancellationToken)
Resolves and validates the tag-bound tagged projection for inspect and publish.
options
cancellationToken
Validated immutable tag details and transient tagged sidecar YAML.
Exact release artifacts read from one Git revision.
Result of a successful prepared-to-tagged projection resolution.
Schema-v2 release evidence implementation for frozen coordinated release pointers.
This type deliberately has its own JSON shape and reader. A v2 bundle must never be deserialized as the v1 shape: v1 could silently omit the current-pointer fields that make a versioned documentation tree historically honest.
ReleaseEvidenceBundleV2 RefreshSubject(ReleaseEvidenceBundleV2 bundle)
Recomputes the v2 subject after a maintainer or workflow supplies catalog-bound docs archive fields.
The release-preparation draft intentionally leaves stable docs archive proof unconfigured until the staged exact tree exists. Callers that add that proof must refresh the subject rather than hand-editing its digest.
ReleaseEvidenceBundle ToCompatibilityBundle()
Adapts already-parsed v2 docs archive fields for the existing archive gate; this is not JSON deserialization.
The immutable commit identity included in the V2 evidence subject digest.
The reviewed preparation source is stable release evidence. Later preparation, tag, and workflow identities are intentionally excluded because they are supplied after the preparation artifact set is generated.
Shared CliFx option surface and diagnostic handling for release commands.
ValueTask ExecuteWithDiagnosticsAsync(IConsole console, Func<ReleaseOptions, CancellationToken, Task<int>> executeAsync)
Runs command logic with release diagnostic rendering.
console
executeAsync
A task that completes after command execution.
ReleaseServices CreateServices(ReleaseOptions options)
Creates service objects for the resolved repository root.
options
Workspace, checker, preparation, tagged projection resolver, and publishing services.
string? ResolveTag(SemVer version)
Resolves the command tag, when relevant.
version
The tag value, or null
for commands that do not use a tag.
string? ResolveGitHubOutputPath(string repoRoot)
Resolves the GitHub Actions output path, when relevant.
repoRoot
The output path, or null
for commands that do not write workflow outputs.
string? ResolveDocsCatalogPath(string repoRoot)
Resolves the AppSurface Docs version catalog path, when the command supports stable docs evidence verification.
repoRoot
The catalog path, or null
when the command does not accept docs catalog input.
Overrides should resolve relative option values from repoRoot
instead of the process working directory so local shells, CI jobs, and generated reports all describe the same artifact. Return null
for commands that do not verify stable docs evidence, and for check
when callers want the local dist/docs/versions.json
fallback. Stable publish
callers should prefer an explicit staged catalog path because checkout-local docs may not be the artifact being published.
string? ResolveDocsTrustedReleaseRootPath(string repoRoot)
Resolves the trusted release root path used to interpret docs catalog exact-tree paths.
repoRoot
The trusted release root, or null
to default to the catalog directory.
Overrides should resolve relative option values from repoRoot
. Returning null
tells the verifier to derive the trusted root from the catalog directory, which is the right default for colocated review artifacts. Pass an explicit root when the catalog is staged beside, but not above, the exact release trees; otherwise catalog exactTreePath
values may resolve as missing or unsafe.
string ResolveBaseRef()
Resolves the branch name used for publish-time reachability and source workflow checks.
The base branch name for release validation. Commands default to main
; publish accepts branch-ish inputs such as release/0.1.0
, origin/release/0.1.0
, refs/heads/release/0.1.0
, or refs/remotes/origin/release/0.1.0
and normalizes them before validators query origin/<branch>
. Tags and commit SHAs are not valid base refs because publish validation must prove protected branch reachability.
Task WriteReportAsync(ReleaseOptions options, string rendered, TextWriter standardOut, CancellationToken cancellationToken)
Writes the rendered command report to stdout and to the optional report path.
options
rendered
standardOut
cancellationToken
A task that completes after the report is written.
string? VersionText { get; set; }
Source
Gets the SemVer 2.0 release version without a leading v
.
string? DateText { get; set; }
Source
Gets the release date for prepare, formatted as YYYY-MM-DD
.
bool DryRun { get; set; }
Source
Gets a value indicating whether the command should avoid repository mutations or publishing.
string? RepositoryRoot { get; set; }
Source
Gets the repository root. Defaults to the current directory.
string? ReportPath { get; set; }
Source
Gets an optional readiness report output path.
string CommandName { get; }
Source
Gets the command name used by release validation logic.
bool FailOnWarnings { get; }
Source
Gets whether this command should turn warning diagnostics into a failing exit code.
bool AllowExistingTargets { get; }
Source
Gets whether this command may review already-generated release artifacts.
Command services bound to a resolved workspace.
Workspace
Checker
Preparation
TaggedProjectionResolver
Publishing
string RollForward(string changelog, SemVer version, DateOnly date, string releasePath)
Resets the compact Unreleased ledger and inserts a tagged changelog section after it.
changelog
## Unreleased
heading.version
## {version} - yyyy-MM-dd
.date
yyyy-MM-dd
formatting.releasePath
Updated changelog content with a reset Unreleased section and the tagged section inserted or appended.
The algorithm is intentionally text-based to preserve surrounding Markdown. The detailed release narrative lives in releases/unreleased.md
before preparation and releases/v{version}.md
after preparation; the changelog keeps only the durable compact ledger. If ## Unreleased
is missing, the canonical compact section is appended before the new tagged section. If the first-release placeholder follows ## Unreleased
, that placeholder block is replaced. Duplicate release sections are not de-duplicated; callers should run readiness checks before calling this method. Malformed heading hierarchies and concurrent changelog edits can therefore produce surprising placement, so this helper should only be used on the repository's canonical changelog shape.
Command runner abstraction for git and GitHub CLI calls.
Task<CommandResult> RunAsync(CommandInvocation invocation, CancellationToken cancellationToken)
Runs a process and captures stdout and stderr.
invocation
cancellationToken
Command result.
Process command runner used by the default CLI.
Clock abstraction used by tests to make generated release dates deterministic.
DateOnly TodayUtc()
System clock used by the default CLI.
Dispatches and validates the schema-v2 release-manifest contract before evidence consumes it.
This deliberately performs raw JSON validation before typed deserialization. System.Text.Json otherwise ignores unknown fields, which would make an accidental V1/V2 hybrid look valid even though the checked-in JSON schemas forbid it.
bool TryValidatePackageSet(ReleaseManifestV2 manifest, IReadOnlyList<PackageIndexEntry> packages, out string issue)
Confirms that a V2 manifest attests to exactly the release surface declared by the package index.
manifest
packages
issue
true
when the manifest and package index describe the same release surface.
AppSurface Console root module for the release cockpit.
Per-invocation execution context supplied by the CLI entry point.
CurrentDirectory
void EnsureAppSurfaceUnreleasedEntryMarkers(string unreleasedTemplate)
Verifies the repository-owned living-note layout before generic entry composition begins.
unreleasedTemplate
UnreleasedEntryException
The public composer intentionally lets consumer templates define their own sections. AppSurface release preparation has a stricter contract because its reset placeholders, sidecar narrative, and release review guidance own the taking-shape
, included
, and migration-watch
sections. Keep that repository policy here rather than constraining consumer projects through the public command.
string StripResetOnlyTemplatePlaceholders(string unreleasedTemplate)
Removes reset-only template bullets directly before their canonical entry markers.
unreleasedTemplate
The template with canonical reset-only bullets removed while preserving source line endings.
This must run before UnreleasedEntryComposer.Compose
, because composition replaces the markers that identify canonical template bullets. The narrow structural match deliberately skips fenced code and HTML blocks so examples or embedded markup can use the same text without being rewritten.
string Build(SemVer version, DateOnly date, string unreleased)
Converts the living unreleased note into a tagged release note.
version
date
yyyy-MM-dd
formatting.unreleased
# Unreleased
heading.Tagged release Markdown with a generated comment header and a trailing newline.
The method first parses Markdown to catch syntax problems, but it does not use the returned syntax tree to rewrite content. It then replaces only the exact top-level # Unreleased
heading without consuming its following blank line and two known narrative phrases using ordinal matching. Release preparation removes reset-only placeholders from the raw template before composing entries. Variants in casing or wording are left unchanged. Output is deterministic apart from the supplied version and date, uses Environment.NewLine
for generated sections, and trims trailing whitespace from the source body. Callers should run release readiness checks first because duplicate headings, missing phrases, or concurrently edited Markdown are not treated as errors.
string ResetUnreleased(SemVer previousVersion)
Creates the next-cycle unreleased proof artifact.
previousVersion
Canonical unreleased Markdown for the next cycle, including the previous version reference and a trailing newline.
This reset intentionally discards the prior living-release body after it has been copied into a tagged release note. It preserves the expected section order for future checks: overview, shaping work, included changes, and migration watch.
string BuildCurrentReleasePointer(SemVer version)
Builds the tree-local pointer used by coordinated package and documentation links.
version
Deterministic Markdown that links to the exact release note.
Do not replace this link with a global release lookup. Release archives copy this file into their immutable exact trees; a historical current
route must therefore point to the release that was current when that tree was published.
IReadOnlyList<string> UnreleasedTemplatePlaceholders { get; }
Source
Gets the reset-only placeholder bullets that must not appear in a tagged release note.
Repository path helper for release-owned files.
Paths are rooted under RepositoryRoot
and accept slash-separated repository-relative inputs. PathFor
rejects rooted paths and traversal that would escape the repository. Use IsUnderPath
when checking paths that come from command-line input, temporary files, or other untrusted sources.
string ReleaseNotePath(SemVer version)
Gets the absolute path for a release note file.
version
Absolute release note path.
string ReleaseSidecarPath(SemVer version)
Gets the absolute path for a release note sidecar file.
version
Absolute sidecar path.
string ReleaseManifestPath(SemVer version)
Gets the absolute path for a release manifest file.
version
Absolute manifest path.
string ReleaseEvidencePath(SemVer version)
Gets the absolute path for a release evidence bundle file.
version
Absolute release evidence bundle path.
string PathFor(string relativePath)
Resolves a repository-relative path and verifies that the result stays inside the repository root.
relativePath
Absolute path under RepositoryRoot
.
ArgumentException
relativePath
is rooted or traverses outside the repository.string DisplayPath(string path)
Formats an absolute path as a slash-normalized repository-relative path.
path
Repository-relative path when possible.
bool IsUnderPath(string root, string path)
Determines whether a path is under the supplied root.
root
path
true
when the path is equal to or below the root.
string RepositoryRoot { get; }
Source
Gets the absolute repository root.
string ChangelogPath { get; }
Source
Gets the absolute changelog path.
string UnreleasedPath { get; }
Source
Gets the absolute unreleased note path.
string UnreleasedEntriesDirectory { get; }
Source
Gets the absolute append-only unreleased-entry directory.
Feature pull requests add independently named Markdown entries here instead of editing the living-note template. Release preparation composes and archives the entries, then removes only the entries it consumed.
string UnreleasedSidecarPath { get; }
Source
Gets the absolute unreleased sidecar path.
string CurrentReleasePath { get; }
Source
Gets the absolute current coordinated release pointer path.
This pointer is overwritten only by release preparation. Once docs are exported, its bytes live inside that exact tree, so the same releases/current.md
route stays frozen to the release represented by the tree.
string CurrentReleaseSidecarPath { get; }
Source
Gets the absolute permanent current coordinated release pointer metadata sidecar path.
string PackageIndexPath { get; }
Source
Gets the absolute package index manifest path.
string TemplatePath { get; }
Source
Gets the absolute tagged-release template path.
A single entry from git diff --name-status
.
Status
A
, M
, D
, or R100
.Path
OriginalPath
Result of validating the exact release-preparation change set.
bool IsValid { get; }
Source
Gets whether the diff is exactly the generated release artifacts and next-cycle rollover files for the requested version.
Enforces the narrow change contract for a release-preparation pull request.
The policy applies to the complete Git diff between the pull request base and head. It intentionally excludes releases/current.md.yml
: that sidecar is permanent, version-independent metadata and must never be regenerated or changed by release preparation.
ReleasePreparationChangePolicyResult Validate(string version, IEnumerable<ReleasePreparationChange> changes, IEnumerable<string>? consumedUnreleasedEntryPaths = null)
Validates that the diff contains the versioned release artifacts, frozen current pointer, changelog, and next-cycle files for version
.
version
v
.changes
consumedUnreleasedEntryPaths
Validation errors, or an empty result when the diff is valid.
IReadOnlyList<ReleasePreparationChange> ParseNameStatus(string nameStatusOutput)
Parses tab-delimited output from git diff --name-status --find-renames
.
nameStatusOutput
Parsed change entries.
Parsed release CLI options shared by every command.
Command
RepositoryRoot
Version
v
.Tag
Date
DryRun
ReportPath
GitHubOutputPath
FailOnWarnings
AllowExistingTargets
BaseRef
DocsCatalogPath
versions.json
used for stable docs evidence verification. check
may leave this null to use the local dist/docs/versions.json
fallback; release publication should use the docs-publication
command for the public archive/catalog path.DocsTrustedReleaseRootPath
Minimal SemVer 2.0 model used by release automation.
SemVer Parse(string value)
Parses a release version and rejects leading-v tags, build metadata, and invalid SemVer shapes.
value
The parsed version.
bool TryParse(string value, out SemVer? version)
Tries to parse a SemVer value without producing a command-line diagnostic.
value
version
true
when the input is a supported SemVer value.
int CompareTo(SemVer? other)
Compares two versions with SemVer 2.0 precedence.
other
A negative value, zero, or a positive value according to SemVer precedence.
bool IsStable { get; }
Source
Gets whether the version is a stable SemVer identity.
bool IsProtectedPrereleaseWorkflowCompatible { get; }
Source
Gets whether this prerelease version can trigger the protected prerelease package workflow.
string TagName { get; }
Source
Gets the annotated git tag expected for this version.
CLI entry point for AppSurface release preparation and publishing validation.
Task<int> Main(string[] args)
Launches the release CLI with process IO streams.
args
Process exit code where 0
indicates success.
Task<int> RunAsync(string[] args, TextWriter standardOut, TextWriter standardError, string currentDirectory, CancellationToken cancellationToken = default, ICommandRunner? commandRunner = null, IReleaseClock? clock = null)
Runs the release CLI against supplied IO streams and an explicit working directory.
args
standardOut
standardError
currentDirectory
cancellationToken
commandRunner
clock
0
for success; otherwise a non-zero exit code.
Canonical annotated-tag binding for one prepared AppSurface release.
The binding repeats the prepared sidecar and manifest digests alongside the release evidence subject so a maintainer can identify the mismatched artifact directly. It is not a replacement for the checked-in evidence bundle; the resolver validates both sources and uses the tag object as the immutable state-transition record.
string Render()
Renders the exact trailing message block accepted by the resolver.
Canonical four-line trailer block with a trailing newline.
void ParseAndValidate(string tag, string tagObject, ReleaseTagBinding expected)
Parses a Git tag object and validates its final AppSurface release trailer block.
tag
tagObject
expected
DateTimeOffset ParseTaggerTimestamp(string tag, string tagObject)
Reads the tagger timestamp from a raw annotated Git tag object.
tag
tagObject
Timestamp and offset recorded by the annotated tagger.
int RequiredKeyCount { get; }
Source
Gets the number of canonical trailers required to bind a tagged release.
Prevents trimming coverage from depending on generated process-start wiring.
Creates release artifacts from the living unreleased note.
Task<ReleasePreparationResult> PrepareAsync(ReleaseOptions options, CancellationToken cancellationToken)
Generates release files or, in dry-run mode, returns the planned edits.
options
cancellationToken
Preparation result containing readiness diagnostics and planned or written repository-relative paths.
Preparation is a deterministic repository-file rewrite: it runs readiness checks, reads the unreleased note and sidecar, builds versioned release artifacts, refreshes the frozen tree-local current pointer, rolls CHANGELOG.md
, resets unreleased files, removes consumed append-only unreleased entries, and records diagnostics in the release manifest. Coordinated package rows are intentionally not rewritten: each docs export freezes the current pointer that was generated for its release. Dry-run mode performs all reads and rendering but does not write files. The method does not create git branches, tags, commits, package artifacts, or GitHub Releases; workflows own those operations. Callers should treat any readiness errors as blocking and should avoid running against a dirty or concurrently modified tree. Writes are sequential rather than transactional. The current pointer is written last, so a partial write cannot advance the visible coordinated alias before the matching versioned artifacts and living-note roll-forward exist. If the local process fails, rerun git status
and remove or revert the partial generated artifacts before retrying so create-only target checks do not stop the next run.
Task ArchiveUnreleasedEntryAsync(UnreleasedEntrySnapshot snapshot, CancellationToken cancellationToken)
Removes one consumed entry without deleting a concurrently replaced file.
Filesystem deletion is pathname-based, so a digest check immediately followed by File.Delete(string)
could delete a replacement written in the intervening window. The guarded handoff atomically moves the current pathname to a private recovery location, verifies the moved bytes, and deletes only that verified private file. When the bytes differ, it restores the candidate without overwrite; if another writer already recreated the source pathname, the changed candidate remains in the recovery location for manual reconciliation instead of being lost.
Immutable command invocation for release-owned external processes.
Executable
Arguments
WorkingDirectory
Timeout
Captured command result.
Release readiness result.
bool HasErrors { get; }
Source
Gets whether the report contains errors.
Release preparation result.
IReadOnlyList<string> ArchivedUnreleasedEntryPaths { get; init; }
Source
Gets the append-only unreleased entries that preparation plans to archive or archived during a real run.
These paths are intentionally separate from generated artifacts: recovery must restore them to their pre-run state, whereas generated files may be removed or restored before a retry.
Machine-readable release manifest.
Machine-readable schema-v2 release manifest for frozen coordinated release links.
V2 deliberately retains V1 as a separate type so checked-in historical manifests are never deserialized through a newer contract. Its package resolutions record the tree-local alias and the immutable tagged note it resolves to at preparation time.
IReadOnlyList<string> ConsumedUnreleasedEntryPaths { get; init; }
Source
Gets the append-only unreleased-entry paths composed into this release and removed during preparation.
The V2 evidence bundle digests this manifest, so this ordered list is the proof that a release-preparation pull request may delete precisely these source entries and no others.
Records how a public package's coordinated release alias resolves in the prepared documentation tree.
Project
Source
coordinated
.AliasPath
ResolvedPath
ReleaseTag
PreparationBaseCommit
Package release note path update recorded in the release manifest.
Serializable diagnostic record for release manifests.
ReleaseDiagnosticRecord FromDiagnostic(ReleaseDiagnostic diagnostic)
Creates a serializable diagnostic record.
diagnostic
Serializable record.
Structured publish outputs for GitHub Actions.
Maintainer-facing release evidence summary rendered in command reports and workflow outputs.
Path
Schema
Status
SubjectSha256
DocsReleaseManifestSha256
CatalogExactTreePath
DocsArchiveVerificationState
DocsCatalogPath
DocsTrustedReleaseRootPath
DocsPhysicalExactTreePath
DocsVerifiedFileCount
TagCommit
Attestation
Verifies the complete release-preparation pull-request diff from a base ref to the checked-out HEAD.
This command is the supported local and CI entry point for release-preparation provenance. Normal callers should use ./eng/release verify-prep-diff --base-ref main
; --witness
exists only as a controlled test/CI seam.
string? RepositoryRoot { get; set; }
Source
Gets the repository root. Defaults to the command invocation directory.
string? BaseRef { get; set; }
Source
Gets the base branch or ref. The default is main
, fetched as origin/main
.
bool NoFetch { get; set; }
Source
Gets whether to skip the base-ref refresh for an intentionally offline, already-current checkout.
string? WitnessPath { get; set; }
Source
Gets an optional pre-created PackageIndex witness path used only by controlled CI/test integrations.
string? ReportPath { get; set; }
Source
Gets an optional Markdown report destination.
Markdown renderer for ReleasePreparationDiffResult
.
string Render(ReleasePreparationDiffResult result)
Renders the full-diff identity, changes, and structured diagnostics without allowing diff content to alter the table shape.
result
Stable Markdown report suitable for the GitHub step summary.
Writes tagged release projections through retained directory handles.
Every directory component is opened without following links and remains open until the temporary file is atomically renamed. Unix uses openat
and renameat
relative to the retained descriptor. Windows retains non-delete-sharing directory handles, which pins the traversed path while the temporary file is created, and uses a relative FILE_RENAME_INFO
target for replacement.
Task WriteAsync(string outputPath, string yaml, CancellationToken cancellationToken)
Creates the output directory securely and atomically replaces the target projection.
outputPath
yaml
cancellationToken
IDisposable UseDirectoryOpenedHookForTesting(Action<string> callback)
Runs a callback after the target directory is safely opened and before the temporary file is created.
callback
A scope that restores the previous callback.
The callback is async-flow-local so concurrent tests cannot alter another write. Production code leaves the callback unset.
IDisposable UseTemporaryFileOpenedHookForTesting(Action callback)
Runs a callback after the temporary output file is created and before its content is written.
callback
A scope that restores the previous callback.
The callback is async-flow-local so concurrent tests cannot alter another write. Production code leaves the callback unset.
IDisposable UseUnixFChmodFailureForTesting(int error)
Forces Unix temporary-file permission hardening to fail with a specified native error code.
error
A scope that restores the previous failure seam.
The seam is async-flow-local so tests can verify cleanup after a permission-hardening failure without depending on host filesystem behavior. Production code leaves the seam unset and calls fchmod
directly.
string NormalizePlatformPath(string path, bool? isMacOs = null)
Canonicalizes fixed macOS temporary-directory aliases before no-follow traversal.
path
isMacOs
The physical macOS temporary path or the original path on other platforms.
Creates deterministic AppSurface Docs release archives, version catalogs, Pages staging payloads, and maintainer recovery summaries.
This planner is the release-tool owned boundary between a tag-local AppSurface Docs export and the public publication workflow. It does not deploy Pages or mutate GitHub Releases directly; instead it emits deterministic local artifacts that GitHub Actions uploads, deploys, verifies, and promotes in separate jobs. Callers must provide an already-exported exact tree for the tag, a disposable Pages staging root, and output paths that do not overlap the exact tree. Supplying DocsPublicationRequest.ExistingPagesRoot
means prior Pages content is required and will be copied before the new immutable releases/{version}/
tree is staged. The staging root is deleted and recreated during planning, so it must never point at the repository, an exact tree, or any durable artifact directory.
Task<DocsPublicationPlan> CreateAsync(DocsPublicationRequest request, CancellationToken cancellationToken)
Produces the release docs publication plan and all local artifacts the publish workflow transports.
request
cancellationToken
The completed publication plan that names every generated artifact and the recovery summary path.
The method validates the tag/version pairing, verifies the exact-tree release manifest digest against release evidence when supplied, writes a deterministic .tar.gz
plus .sha256
, resets the Pages staging directory, copies existing Pages content when requested, writes the merged catalog, and emits a recovery summary. It rejects missing existing Pages roots, output paths under the exact tree, and reparse-point entries so a release archive cannot accidentally include generated output or follow a symlink outside the trusted tree.
Task WriteOutputsAsync(DocsPublicationPlan plan, string? githubOutputPath, CancellationToken cancellationToken)
Writes GitHub Actions outputs for the generated publication plan.
plan
githubOutputPath
cancellationToken
Request for creating docs publication artifacts.
Version
v
. Stable versions may become recommendedVersion
.Tag
v{Version}
.ExactTreePath
ExistingPagesRoot
ArchivePath
.tar
path must also be outside the exact tree.PagesStagingRoot
PlanPath
SummaryPath
ExpectedReleaseManifestSha256
PromoteRecommended
Machine-readable release docs publication plan.
Schema
Version
Tag
PlanPath
ArchiveAssetName
ArchivePath
ArchiveSha256
Sha256Path
ExactTreePath
ReleaseManifestSha256
PagesStagingRoot
CatalogPath
versions.json
path.RecommendedVersion
CatalogEntry
RetryPolicy
Recovery
Version catalog entry produced for the released docs tree.
Version
Label
Summary
SupportState
Current
for promoted stable releases or Maintained
.Visibility
AdvisoryState
ExactTreePath
ReleaseManifestSha256
Draft/public asset retry policy emitted with the publication plan.
DraftAssetReplaceAllowed
PublicAssetReplaceAllowed
Maintainer recovery summary metadata.
SummaryPath
string RenderCheck(ReleaseCheckResult result)
Renders a check report.
result
Markdown report.
The report shape is stable for workflow comments and maintainer review: # Release readiness report
, a summary bullet list, ## Generated files
, optional ## Release evidence bundle
, ## Errors
, then ## Warnings
. Empty diagnostics render as - None
. Each diagnostic renders its complete severity/code/problem/cause/fix/docs envelope. Generated file paths and diagnostic codes are wrapped in inline code; diagnostic text is not escaped beyond normal Markdown rendering. Consumers should key off headings and diagnostic codes rather than line numbers.
string RenderPreparation(ReleasePreparationResult result)
Renders a prepare report.
result
Markdown report.
Preparation reports begin with the check report contract, then append a manual review gate, optional evidence summary, either ## Dry-run plan
or ## Files written
based on ReleasePreparationResult.DryRun
, a separate append-only entry archive section, and structured recovery guidance. Paths are repository-relative bullets. This distinction is the only dry-run marker in the report, so callers that publish the report should preserve that heading.
Task<PackageIndexSummary> LoadAsync(string path, CancellationToken cancellationToken)
Loads a package index summary from YAML.
path
cancellationToken
Package index summary.
PackageIndexSummary Load(string content)
Parses a package-index document into the release-owned public package summary.
content
The public publish package rows and their release-link contracts.
IReadOnlyList<PackageIndexEntry> PublicPublishedPackages { get; }
Source
Gets public package rows whose publish decision is publish.
Package manifest root shape used by the release tool.
List<PackageIndexYamlEntry> Packages { get; init; }
Source
Gets the package rows.
Package manifest row shape used by the release tool.
string Project { get; init; }
Source
Gets the project path.
string Classification { get; init; }
Source
Gets the classification string.
string? PublishDecision { get; init; }
Source
Gets the publish decision string.
string? ReleaseTrack { get; init; }
Source
Gets the release-link policy.
string? ReleaseNotesPath { get; init; }
Source
Gets the explicit release notes path when the package uses an explicit link.
string? ReadinessBlocker { get; init; }
Source
Gets the same-repository issue or pull request that blocks publication, when one remains unresolved.
Package row included in a release manifest.
Project
ReleaseLink
ReadinessBlocker
string ReleaseNotesPath { get; }
Source
Gets the repository-relative release note path after applying the package release-link policy.
Verifies stable release evidence against a staged AppSurface Docs version catalog and exact archive tree.
Runtime docs catalog loading is deliberately lenient so one bad version cannot break unrelated docs. Stable release publishing is the opposite boundary: the selected release version must be present, public, catalog-pinned, and byte verified before the release tool allows the stable GitHub Release path to continue.
Task<ReleaseDocsArchiveGateResult> ValidateStableAsync(ReleaseWorkspace workspace, ReleaseOptions options, ReleaseEvidenceBundle bundle, CancellationToken cancellationToken)
Validates the stable release docs archive contract using command-supplied catalog inputs.
bool TryValidateNoReparseSegments(string rootPath, string candidatePath, out string? detail)
Validates that a physical directory candidate stays below an ordinary trusted root without crossing reparse segments.
rootPath
candidatePath
.candidatePath
detail
true
when the candidate equals or descends from the root and every directory segment is ordinary.
This test seam validates directory ancestors. File leaves still need a separate FileInfo
check before hashing or reading bytes so a manifest entry cannot point at a symlinked file inside an otherwise ordinary directory.
bool TryResolveExactTreePath(string trustedReleaseRoot, string exactTreePath, out string? physicalExactTreePath, out string? issue)
Resolves a catalog exactTreePath
into a physical path beneath the trusted release root.
trustedReleaseRoot
exactTreePath
physicalExactTreePath
null
unless containment fails after normalization.issue
true
when the authored path can be safely resolved under trustedReleaseRoot
.
The trusted root is supplied by release operators or defaults from the catalog directory. Callers should validate the returned directory exists and has no reparse segments before reading archive content.
IReadOnlySet<string> CreatePhysicalManifestPathSet(IEnumerable<string> manifestPaths, StringComparer pathComparer)
Creates the physical-path coverage set for a release manifest under the filesystem's casing rules.
manifestPaths
pathComparer
A set used to match paths returned by physical archive enumeration.
StringComparer ResolvePhysicalPathComparer(string rootPath, Func<string, bool> fileExists, Func<string, IEnumerable<string>> enumerateFileSystemEntries)
Resolves physical filesystem casing behavior without writing probe files into an immutable archive.
rootPath
fileExists
enumerateFileSystemEntries
An ordinal comparer matching the archive root's case behavior.
IDisposable UseFileSystemInspectorForTesting(IFileSystemInspector inspector)
Overrides release docs archive filesystem inspection for the current async test flow.
inspector
A disposable scope that restores the previous inspector.
Production code uses the default physical inspector. Tests use this seam to force deterministic metadata and enumeration failures that operating systems otherwise expose only through race-prone permission or reparse-point behavior.
Reads filesystem metadata used by stable docs archive validation.
The interface is intentionally narrow and internal: callers should not use it to virtualize archive bytes, only metadata operations whose real filesystem failures are difficult to trigger deterministically in tests.
bool DirectoryExists(DirectoryInfo directory)
Returns whether the directory currently exists.
FileAttributes GetDirectoryAttributes(DirectoryInfo directory)
Reads directory attributes, throwing the same filesystem exceptions as FileSystemInfo.Attributes
.
FileSystemInfo[] EnumerateFileSystemInfos(DirectoryInfo directory)
Enumerates immediate entries in a directory.
FileAttributes GetFileSystemInfoAttributes(FileSystemInfo entry)
Reads attributes for a file or directory entry discovered during archive traversal.
bool FileExists(FileInfo file)
Returns whether the file currently exists.
FileAttributes GetFileAttributes(FileInfo file)
Reads file attributes, throwing the same filesystem exceptions as FileSystemInfo.Attributes
.
Result of stable docs archive verification.
Proof
null
.Diagnostics
Successful results have a non-null proof and no diagnostics. Failure results keep proof null so check and publish callers cannot accidentally treat a partially inspected archive as verified.
Immutable proof that the stable release docs catalog entry and staged exact tree were verified.
State
ReleaseDocsArchiveGate.VerifiedState
.CatalogPath
versions.json
that supplied the selected catalog entry.TrustedReleaseRootPath
CatalogExactTreePath
CatalogReleaseManifestSha256
PhysicalExactTreePath
VerifiedFileCount
The proof intentionally carries both authored catalog values and resolved physical paths. Maintainers should review the authored values for release identity and the physical values for staging provenance.
Mutable YAML sidecar metadata for release-note documentation pages.
Sidecars are dictionary-backed so unknown fields are preserved. The committed release artifact remains in the prepared
state; only a tag-bound resolver creates the transient tagged
projection used by docs and publishing.
Task<ReleaseSidecar> LoadAsync(string path, CancellationToken cancellationToken)
Loads sidecar metadata from YAML.
path
cancellationToken
Loaded sidecar whose unknown keys are preserved for later serialization.
The current release flow expects the unreleased sidecar to contain documentation keys such as title
, summary
, page_type
, nav_group
, order
, breadcrumbs
, and trust
. Missing keys are tolerated because ToPreparedRelease
writes the release-owned fields. Malformed YAML is wrapped in ReleaseToolException
with release-sidecar-invalid
; I/O and cancellation failures bubble to the shared command diagnostic layer.
string ToPreparedRelease(SemVer version, DateOnly date)
Converts unreleased metadata into a committed prepared-release sidecar.
version
date
Prepared release sidecar YAML using underscored key naming for generated CLR-backed values.
This method mutates the loaded dictionary before serializing. It replaces release-owned metadata with provisional prepared-release values and preserves unrelated keys from the source sidecar. The result deliberately makes no tag-derived or GitHub Release existence claim.
ReleaseSidecar Parse(string content, string source)
Parses a sidecar YAML payload that was read from a tag or another explicit source.
content
source
Parsed release sidecar with unknown fields preserved.
void EnsurePrepared(SemVer version, string source)
Ensures that this sidecar is an explicit prepared artifact for the requested version.
version
source
Missing state is treated as a legacy artifact rather than being silently interpreted as prepared. This keeps new release publication deterministic while leaving historical docs and archive reads independent of the new resolver.
string ToTaggedProjection(SemVer version, DateTimeOffset taggerTimestamp, string source)
Creates the transient tagged sidecar projection after a resolver has verified the annotated tag.
version
taggerTimestamp
source
Rendered tagged sidecar YAML. The caller owns any temporary output path.
This method mutates and consumes the loaded metadata. After projection, the instance declares release.state: tagged
and final trust wording, so it no longer passes EnsurePrepared
. Parse a fresh ReleaseSidecar
for each projection.
string UnreleasedTemplate()
Creates reset unreleased metadata for the next release cycle.
Canonical unreleased sidecar YAML with title, navigation, trust metadata, and source guidance.
The template is complete enough for docs harvesting without requiring the previous sidecar. It intentionally resets release-specific trust metadata back to provisional language.
JSON serializer configuration for release artifacts.
JsonSerializerOptions Options { get; }
Source
Gets indented camel-case JSON options.
Creates and validates checked-in release evidence bundles.
Release evidence is repository consistency evidence, not a signature or hosted-build attestation. The bundle ties together release-owned files, package release-note paths, optional docs archive catalog fields, and split commit identities so release preparation can be reviewed in a pull request and publishing can validate the same bundle at the annotated tag commit.
ReleaseEvidenceBundle BuildDraft(ReleaseWorkspace workspace, SemVer version, string releaseClassification, DateOnly date, string? contentSourceCommit, string releaseNoteContent, string releaseSidecarContent, string releaseManifestContent, IReadOnlyList<PackagePathUpdate> packagePathUpdates)
Builds a draft release evidence bundle for release preparation.
ReleaseEvidenceBundleV2 BuildDraftV2(ReleaseWorkspace workspace, SemVer version, string releaseClassification, DateOnly date, string? contentSourceCommit, string releaseNoteContent, string releaseSidecarContent, string releaseManifestContent, string currentReleaseContent, string currentReleaseSidecarContent, IReadOnlyList<CoordinatedPackageReleaseNoteResolution> coordinatedPackageReleaseNoteResolutions)
Builds schema-v2 evidence for a release whose package links use the frozen coordinated current pointer.
workspace
version
releaseClassification
prerelease
or stable
.date
contentSourceCommit
releaseNoteContent
releaseSidecarContent
releaseManifestContent
currentReleaseContent
currentReleaseSidecarContent
coordinatedPackageReleaseNoteResolutions
Task<ReleaseEvidenceValidationResult> ValidatePreparedAsync(ReleaseWorkspace workspace, SemVer version, string releaseClassification, string? contentSourceCommit, CancellationToken cancellationToken)
Validates a checked-in release evidence bundle in the current worktree.
ReleaseEvidenceValidationResult ValidateTag(SemVer version, string releaseClassification, string tag, string tagCommit, string releaseNoteJson, string releaseSidecarJson, string releaseManifestJson, string evidenceJson, string? currentReleaseContent = null, string? currentReleaseSidecarContent = null)
Validates release evidence read from an annotated tag.
version
releaseClassification
tag
tagCommit
releaseNoteJson
releaseSidecarJson
releaseManifestJson
evidenceJson
currentReleaseContent
currentReleaseSidecarContent
string Serialize(ReleaseEvidenceBundleV2 bundle)
Serializes a schema-v2 coordinated release evidence bundle.
bool IsV2(string evidenceJson)
Determines whether a raw evidence document declares schema v2 without deserializing it as a v1 object.
Describes the outcome of release evidence validation.
Summary
null
.Diagnostics
Bundle
null
for unreadable evidence and can be non-null while Diagnostics
contains errors.
Stable docs archive verification depends on Bundle
only after callers have checked diagnostics. Do not treat a non-null bundle as proof that the evidence is publishable.
Validates release readiness without mutating repository files.
bool FailOnWarningsOption { get; set; }
Source
Gets a value indicating whether check should fail on warning diagnostics.
bool AllowExistingTargetsOption { get; set; }
Source
Gets a value indicating whether check may review already-generated release artifacts.
string? DocsCatalogPath { get; set; }
Source
Gets the AppSurface Docs version catalog used to verify stable release evidence.
check
may omit this value to use dist/docs/versions.json
when that local review fallback exists. Stable checks that review prepared artifacts should pass the staged catalog explicitly when possible. Relative paths are resolved from the repository root, and invalid or missing catalogs surface release diagnostics rather than mutating release files.
string? DocsTrustedReleaseRootPath { get; set; }
Source
Gets the trusted release root used to resolve catalog exactTreePath values.
When omitted, the verifier uses the catalog directory as the trusted release root. Pass this option when the catalog is staged outside the directory that contains the exact tree paths. The root must be an ordinary directory, and catalog exactTreePath
values must stay relative to it without hidden or parent segments.
Generates the coordinated release pull request payload.
Renders canonical annotated-tag trailers from the prepared release artifacts at HEAD.
Inspects a local annotated release tag and renders the validated tagged projection.
string? Tag { get; set; }
Source
Gets the annotated release tag to inspect.
string? BaseRef { get; set; }
Source
Gets the branch that must contain the tag commit.
string? OutputPath { get; set; }
Source
Gets an optional explicit output path for the tagged sidecar projection.
The command validates all tag-bound inputs before writing this path, then opens each parent directory without following links and atomically replaces the output through that retained directory object. The release-publish docs job uses a runner-temporary file and explicitly overlays it only in its disposable detached checkout. The path must resolve outside the repository source tree so inspection cannot overwrite a prepared release artifact.
Validates tag state and emits GitHub Release workflow outputs.
string? Tag { get; set; }
Source
Gets the annotated release tag to publish.
string? GitHubOutputPath { get; set; }
Source
Gets an optional GitHub Actions output file.
string? BaseRef { get; set; }
Source
Gets the branch that must contain the annotated tag commit.
Publish defaults to main
. Use this option when a maintained release branch, such as release/0.1.0
, owns the tag provenance for a release. The command accepts branch names and branch refs shaped as origin/<branch>
, refs/heads/<branch>
, or refs/remotes/origin/<branch>
, then normalizes them before validation fetches and checks origin/<branch>
. Tags, SHAs, empty branch names, and unsupported refs such as refs/tags/v1.2.3
are invalid because publish validation must prove protected branch reachability.
string? DocsCatalogPath { get; set; }
Source
Gets the staged AppSurface Docs version catalog used to verify stable release evidence.
Stable publish accepts this path for local diagnostics, but the public release workflow creates its staged docs artifact through docs-publication
. Relative paths are resolved from the repository root. Prerelease publish accepts the option but does not require docs archive proof.
string? DocsTrustedReleaseRootPath { get; set; }
Source
Gets the trusted release root used to resolve catalog exactTreePath values.
When omitted, stable publish resolves exact trees relative to the staged catalog directory. Supply this option when the artifact layout stores versions.json
separately from the exact release trees; the path must resolve under the repository root when relative and must point at the ordinary directory that owns the catalog exact-tree paths.
Creates AppSurface Docs publication artifacts for the release publish workflow.
This command is the maintainer-facing CLI seam for the public release docs trust path. It expects docs to have already been exported for the annotated tag commit and produces the deterministic archive, digest ledger, Pages staging payload, catalog entry, publication plan, and recovery summary consumed by release-publish.yml
. Use it when the release workflow needs a durable GitHub Release asset and a Pages catalog update from the same tag-bound exact tree. Do not use it to publish arbitrary local docs: the tag must match --version
, --docs-exact-tree
must be an ordinary exported tree with the release manifest, and --pages-staging-root
is deleted before the merged payload is written. Disable --promote-recommended
only for a documented recovery or prerelease-style staging run where the current stable recommendation must remain unchanged.
string? Tag { get; set; }
Source
Gets the annotated release tag that owns the docs publication.
This option is required and must be the canonical v{version}
tag. A mismatch fails before any staging directory is reset.
string? DocsExactTreePath { get; set; }
Source
Gets the exported docs exact tree for the tag.
The exact tree must be a completed AppSurface Docs export for the tag commit and must contain .appsurface-docs-release-manifest.json
. The planner rejects hidden repository-relative paths, generated output paths under this tree, and reparse-point entries so archive bytes come only from ordinary exported files.
string? ExistingPagesRootPath { get; set; }
Source
Gets the optional current Pages payload to copy before adding the immutable release tree.
When supplied, this directory must exist. Use it to preserve existing versions.json
, /docs
, and prior releases/*
content before the new release exact tree is copied into the staging root.
string? ArchiveOutputPath { get; set; }
Source
Gets the tar.gz archive output path.
The command writes this file and a sibling .sha256
file. The path and its temporary .tar
sibling must be outside the exact tree so the archive cannot include its own generated bytes.
string? PagesStagingRootPath { get; set; }
Source
Gets the Pages staging root output path.
This directory is destructive scratch space: it is deleted and recreated before existing Pages content and the new releases/{version}/
tree are copied. It must not overlap the repository, exact tree, existing Pages root, archive, publication plan, or recovery summary paths.
string? PlanOutputPath { get; set; }
Source
Gets the publication plan JSON output path.
The plan is the machine-readable artifact handoff between docs archive creation, Pages deployment, public verification, and release promotion. Store it outside the exact tree and staging root.
string? SummaryOutputPath { get; set; }
Source
Gets the optional recovery summary output path.
The summary contains exact resume, publish, and abort commands for partial failures. Store it outside the exact tree and staging root so it cannot be served as release docs content.
string? ExpectedReleaseManifestSha256 { get; set; }
Source
Gets the optional release evidence docs manifest digest that the exact tree must match.
Stable release workflows pass this from tag-bound release evidence. A mismatch means the exported docs tree does not match the reviewed evidence and must be regenerated from the annotated tag commit.
string PromoteRecommendedText { get; set; }
Source
Gets whether stable docs publication should promote the version to recommendedVersion.
Defaults to true
. The command rejects values other than true
or false
. Passing false
keeps the existing recommendation and is intended for prereleases or explicit recovery where maintainers do not want to change the stable docs pointer.
string? GitHubOutputPath { get; set; }
Source
Gets an optional GitHub Actions output file.
When supplied, the command appends scalar outputs such as archive name, digest, catalog path, exact tree path, and recovery summary path using GitHub Actions output-file syntax. The option must name a file, not a root directory.
Parses and renders the frozen tree-local coordinated release pointer.
The current pointer deliberately has a very small, byte-stable surface. Exact documentation trees copy this file, so accepting free-form prose would allow a historical current
route to silently stop identifying the release represented by its tree.
string BuildNone()
Builds the initial pointer used before the repository has a reachable coordinated tag.
string Build(SemVer version)
Builds the canonical pointer for a tagged coordinated release.
bool TryParse(string content, out SemVer? version)
Validates canonical pointer bytes and returns its optional referenced tag version.
Finds annotated, reachable coordinated release tags and enforces the current-pointer advancement rule.
Diagnostic envelope with a stable code and reader-actionable context.
ReleaseDiagnostic Error(string code, string problem, string cause, string fix, string docs)
Creates an error diagnostic.
ReleaseDiagnostic Warning(string code, string problem, string cause, string fix, string docs)
Creates a warning diagnostic.
ReleaseDiagnostic InvalidUnreleasedEntry(string cause)
Creates the diagnostic emitted when append-only unreleased entries cannot be composed.
cause
Stable diagnostic for callers of release check and preparation.
string Render()
Exception that carries a structured release diagnostic.
ReleaseDiagnostic Diagnostic { get; }
Source
Gets the structured diagnostic.
DI-backed execution runtime for release CliFx commands.
Task RunAsync(string[] args, Action<ConsoleOptions>? configureOptions = null)
Runs release commands through the shared AppSurface command-service primitive.
args
configureOptions
A task that completes when command execution finishes.
Classifies the complete release-preparation pull-request diff and admits generated package documentation only with a matching PackageIndex provenance witness.
This is a maintainer-integrity gate for repository-owned release preparation. It is not a hostile-fork security boundary: the evaluator and witness generator run from the checked-out pull-request tree. See tools/ForgeTrust.AppSurface.Release/README.md#verify-prep-diff
for the exact local and CI workflow.
Task<ReleasePreparationDiffResult> VerifyAsync(string repositoryRoot, string baseRef, bool noFetch, string? witnessPath, CancellationToken cancellationToken)
Verifies a release-preparation diff from an explicit base branch or ref to HEAD.
repositoryRoot
baseRef
main
.noFetch
witnessPath
cancellationToken
Identity, changed-path, and typed diagnostic report.
bool TryParseNameStatus(string output, out IReadOnlyList<ReleasePreparationChange> changes, out string issue)
Strictly parses NUL-delimited git diff --name-status -z --find-renames
output.
output
changes
issue
Whether the stream was unambiguous and safe to classify.
void ValidateReleaseArtifactChanges(string version, IReadOnlyList<ReleasePreparationChange> changes, IReadOnlyList<string> consumedEntryPaths, List<ReleaseDiagnostic> diagnostics)
Appends diagnostics when the complete diff does not contain the exact artifact statuses required for a versioned release.
version
v
, used to derive required release artifact paths.changes
consumedEntryPaths
diagnostics
Task ValidateWitnessAsync(ReleasePreparationWitnessDocument witness, IReadOnlyList<ReleasePreparationChange> changes, string repositoryRoot, string baseRef, string baseTip, string mergeBase, string head, List<ReleaseDiagnostic> diagnostics, CancellationToken cancellationToken)
Validates a parsed PackageIndex witness against the complete diff and appends diagnostics for every provenance mismatch.
witness
changes
repositoryRoot
baseRef
baseTip
mergeBase
head
diagnostics
cancellationToken
Direct callers should first parse the witness with TryParseWitness
and resolve the exact identities from the same complete diff. This method treats invalid provenance as diagnostics so the CLI can render a complete report; ordinary I/O cancellation and unexpected filesystem failures still propagate.
Typed report produced by ReleasePreparationDiffVerifier
.
bool IsValid { get; }
Source
Gets whether no blocking diff diagnostic was produced.
Parsed PackageIndex provenance witness that binds changed semantic inputs and generated output hashes to Git identities.
Schema
BaseRef
BaseTipCommit
MergeBaseCommit
HeadCommit
Verification
verified
.ChangedInputs
Surfaces
One changed semantic input and the ordered generated surfaces it authorizes.
Kind
package-index-manifest
or release-guidance-template
.Path
Surfaces
One generated package documentation surface and the canonical SHA-256 digest it must match at HEAD.
Kind
chooser
, readiness
, or managed-readme
.Path
Sha256
Validates tag state and produces GitHub Release workflow outputs.
Task<PublishOutputs> PublishAsync(ReleaseOptions options, CancellationToken cancellationToken)
Validates an existing annotated tag and extracts release notes from the tag commit.
options
cancellationToken
Structured workflow outputs for GitHub Release creation.
PublishAsync
verifies annotated tag shape, reachability from the configured base ref, package publication, draft-safe GitHub Release state, and presence of releases/v{version}.md
in the tag commit. The tag commit must also contain the release sidecar, release manifest, and release evidence bundle; missing or invalid tag-bound artifacts fail fast before a GitHub Release is created or promoted. The method writes the tag's release note to a temporary file so workflows can pass a stable notes path to GitHub's release action.
Task WriteOutputsAsync(PublishOutputs outputs, ReleaseOptions options, CancellationToken cancellationToken)
Writes publish outputs to a GitHub Actions output file when requested.
outputs
options
ReleaseOptions.GitHubOutputPath
must be a file path, not a root directory.cancellationToken
Scalar outputs use name=value
. Multiline outputs use GitHub's delimiter form. Existing files are appended to match GITHUB_OUTPUT
behavior.
Validates release inputs and computes release readiness diagnostics.
Task<ReleaseCheckResult> CheckAsync(ReleaseOptions options, CancellationToken cancellationToken)
Runs local release readiness checks.
options
cancellationToken
Readiness result with errors, warnings, and generated paths.