Skip to content

release/post-release hook: project-defined repository updates at release time #312

Description

@rdeago

Blocked by #311 (the well-known hook path lives under .buildvana/).

Reference version

2.1.12-preview

Background and motivation

SelfReferenceUpdater rewrites the three well-known files (global.json, .config/dotnet-tools.json, Directory.Packages.props). But repos can embed the released version in arbitrary other files. Concrete case: Buildvana's own buildvana.jsonc carries a $schema URL pinned to a release tag, which today must be hand-amended into the dogfood commit after every release.

Hard-wiring more targets doesn't generalize (the $schema case only ever fires in Buildvana's own repo — a consumer's release never produces Buildvana packages), and a declarative rewrite list would grow into a bad programming language. The right escape hatch is real code: an optional, repo-owned hook run at a named moment of the release flow.

The hook is named after the moment it fires — the assembly of the post-release commit (see AddPostReleaseCommit) — not after dogfooding, its motivating use case. Hooks are event handlers; naming one after its first use case misdescribes the mechanism and, as an earlier draft of this proposal demonstrated, invites wrongly coupling the trigger to the use case (gating the hook on the dogfood option).

Proposed enhancement

  • If .buildvana/hooks/release/post-release.cs exists, bv release runs it (via dotnet run) from the home directory, at the moment the post-release commit is assembled: after the well-known self-reference rewrites (when dogfooding is enabled) and before anything is pushed. The hook runs whether or not dogfooding is enabled; the dogfood option gates only the built-in rewrites. When the file is absent, the hook is skipped with an info message.
  • Hook path convention: .buildvana/hooks/<command>/<moment>.cs — the directory names the command, the file names the moment. This proposal ships exactly one moment.
  • Context is passed via a JSON context file: bv serializes the release context (home directory, version being released in plain and full-SemVer forms, previously released version, prerelease and public-release flags, artifacts directory, the produced package id→version map, and whether dogfooding ran) to a temporary file and sets a single environment variable, BV_HOOK_CONTEXT, to its absolute path. The file is deleted when the hook completes; its content is logged at Detail verbosity for post-mortems.
  • Buildvana SDK ships typed loaders: when building a file-based app located under .buildvana/hooks/, the SDK injects Compile items for source files it ships, providing internal types: a BvHookContext record with a static Load() method (reads BV_HOOK_CONTEXT, deserializes), and a Buildvana configuration loader that probes the four configuration-file candidates under the current directory (the hook runs from home; discovery has already happened), applies the existing exactly-one rule, and parses jsonc-tolerantly. The context carries the facts of the run; the configuration loader serves hooks that need any standing repo setting. Hooks in Buildvana-SDK repos therefore start at var context = BvHookContext.Load(); — no scaffolding. The detection is path-based, not gated on a bv-passed property, so hooks stay runnable by hand (BV_HOOK_CONTEXT=... dotnet run post-release.cs) for testing outside a release.
  • The hook reports nothing back. bv snapshots git status --porcelain before and after; files changed by the hook join the post-release commit alongside the well-known rewrites (or constitute it entirely, when dogfooding is off or rewrote nothing).
  • A non-zero exit code aborts the release before anything is pushed.
  • bv clean also runs dotnet clean on each *.cs file under .buildvana/hooks/ (recursively), clearing its file-based-app build cache.
  • Documentation states the contract, conventions, and sharp edges rather than enforcing anything:
    • "post-release" names the post-release commit: when the hook runs, nothing has been pushed or published yet, and a non-zero exit aborts the entire release. Announcements and other externally-visible actions don't belong here.
    • Prefer BCL-only hooks; the BCL (including System.Text.Json) covers version-rewriting jobs.
    • For third-party dependencies, prefer versionless #:package resolved through the repo's Directory.Packages.props (supported by file-based apps under central package management) — the version then lives where dependency updates already look.
    • Never reference self-produced packages via versionless #:package: at hook time Directory.Packages.props has already been rewritten to the version being released, which is on no feed until the release completes — restore fails mid-release, deterministically.
    • #:project is the sanctioned way to use repo-local library code: no version pin, compiles against HEAD.
    • Pinned #:package Foo@x.y.z is allowed but owned by the repo: pins drift on dependency updates, and a pin on a self-produced package lags its own release by one. If you break your own repo, you own both pieces.
    • Hooks require Buildvana SDK, which reaches them through the repo's Directory.Build.{props,targets} parent-inclusion chain. A repo may add its own .buildvana/Directory.Build.{props,targets}, but they must follow the well-known parent-inclusion pattern; otherwise hooks break and the repo owns both pieces.
    • Local file-based-app caching may not notice implicit-build-file changes; CI is always a cold build. bv clean clears it.
  • Buildvana's own repo adds .buildvana/hooks/release/post-release.cs rewriting the $schema version segment (BCL regex), guarded on the dogfooded context flag, serving as the worked example and ending the hand-amend ritual.

Acceptance criteria:

  • Hook detected and executed at the documented moment, with the documented context contract, independently of the dogfood option; skipped (with an info message) when absent.
  • Hook-modified files land in the post-release commit; hook failure aborts the release with the hook's output surfaced.
  • The post-release commit message remains accurate when the hook contributes changes (see implementation proposals).
  • bv clean clears hook build caches.
  • The SDK injects the typed loaders for file-based apps under .buildvana/hooks/, with tests where the SDK test infrastructure allows.
  • Documentation covering the contract and the sharp edges above.
  • This repo's own $schema hook in place.
  • Tests for detection, context-file contract (including the boolean flags and previousVersion's first-release case), diff-detection, execution with dogfooding disabled, failure handling.
  • Changelog entry under ## Unreleased changes.

Implementation proposals

  • ReleaseCommand: run the hook through IProcessRunner immediately after the dogfood block (unconditionally), before PushUpdates; merge diff-detected paths with the SelfReferenceUpdater results (empty when dogfooding is off) into a single AddPostReleaseCommit call, made only when the merged list is non-empty.
  • Porcelain snapshot via the existing Git service; the working tree is clean at that point apart from the well-known rewrites, so attribution is unambiguous.
  • Commit message: Post-release updates for {version} [skip ci], fixed. A separate historical message (Update self-references to {version} [skip ci]) for commits containing only the built-in rewrites was considered and dropped: the message composition will be revisited anyway (for one, [skip ci] should be provided by the server adapter, which opens the Git-host vs. CI-environment distinction — out of scope here).
  • Context file shape (camelCase; bikeshed welcome): homeDirectory, releaseVersion (plain form), releaseSemVer (full SemVer, including build metadata when the build embeds it in artifact names), previousVersion (null when no previous release exists), isPrerelease, isPublicRelease (currently invariantly true, since bv release requires a public release; the invariant falls the day bv release learns to run locally), artifactsDirectory (absolute), producedPackages (object mapping package id → version), dogfooded (whether the built-in self-reference rewrites ran in this release — the resolved outcome, which the --dogfood flag may have overridden away from the configured value).
  • Previous version source: latest release tag reachable from HEAD (GetLatestVersions already exists); covers repos with nothing to dogfood, and reachability keeps it correct when releasing from a maintenance branch alongside a newer line.
  • Contract evolution is additive-only: bv (the installed tool) writes the file, the pinned SDK's loaders read it, and the two can transiently diverge (see bv should verify the repo's pinned Buildvana SDK version before running commands that involve the SDK #317). New fields may be added; none removed or repurposed. System.Text.Json ignores unknown fields by default, so old loaders tolerate new bv output.

Usage examples

Buildvana's own hook, approximately:

using System.Text.Json;
using System.Text.RegularExpressions;

// Bootstrap: this repo builds with the previous published SDK, which does not
// yet ship BvHookContext — read the context file directly. Consumers get
// `var context = BvHookContext.Load();` instead.
using var context = JsonDocument.Parse(
    File.ReadAllText(Environment.GetEnvironmentVariable("BV_HOOK_CONTEXT")!));
if (!context.RootElement.GetProperty("dogfooded").GetBoolean())
{
    return;
}

// The $schema URL pins a release tag; tags use the full SemVer form.
var version = context.RootElement.GetProperty("releaseSemVer").GetString()!;
var path = "buildvana.jsonc";
var text = File.ReadAllText(path);
text = Regex.Replace(text, @"(Tenacom/Buildvana/)[^/]+(/schemas/)", $"${{1}}{version}$2");
File.WriteAllText(path, text);

(The guard mirrors the built-in rewrites: the $schema URL is itself a self-reference, so it moves only when dogfooding moves the rest.)

Risks

  • Consumer code now runs inside bv release and can fail it; mitigated by running before any push, so failures abort cleanly.
  • Same trust model as the rest of the build (MSBuild targets, tests already execute repo code with release secrets in the environment); releases don't run on fork PRs. No new attack surface.
  • Scope creep toward a general-purpose hook matrix; guarded by shipping exactly one named moment (release/post-release) until a second need is demonstrated. The <command>/<moment> convention leaves room (e.g., a hypothetical release/post-publish) without promising anything.
  • Hooks referencing self-produced packages: versionless-via-CPM fails the release deterministically (documented as "never"); a pin lags its own release by one (documented, owned by the repo).

Additional information

Consumer-side $schema staleness (updating Buildvana in a consumer repo) is intentionally out of scope; it happens at tool-update time, when bv isn't running, and may get a different mechanism or none.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancement[issue/PR] requests / implements new or improved functionality.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions