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
6 changes: 5 additions & 1 deletion .fallout/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"GitHubRelease",
"Images",
"Push",
"PushDevelop",
"PushDockerHub",
"PushEdge",
"PushGhcr",
"ReleaseBundle",
"Test",
Expand Down Expand Up @@ -115,6 +115,10 @@
"allOf": [
{
"properties": {
"DockerHubNamespace": {
"type": "string",
"description": "Docker Hub namespace — the account images are mirrored under"
},
"ImageTag": {
"type": "string",
"description": "Image tag. Defaults to the git tag on a tag build, otherwise 'dev'"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
#
# - To trigger manual generation invoke:
#
# fallout --generate-configuration GitHubActions_publish-edge --host GitHubActions
# fallout --generate-configuration GitHubActions_publish-develop --host GitHubActions
#
# </auto-generated>
# ------------------------------------------------------------------------------

name: publish-edge
name: publish-develop

on:
push:
Expand Down Expand Up @@ -49,8 +49,8 @@ jobs:
global-json-file: global.json
- name: 'Restore: dotnet tools'
run: dotnet tool restore
- name: 'Run: PushEdge'
run: dotnet fallout PushEdge
- name: 'Run: PushDevelop'
run: dotnet fallout PushDevelop
env:
RegistryUser: ${{ secrets.REGISTRY_USER }}
RegistryPassword: ${{ secrets.REGISTRY_PASSWORD }}
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ dotnet fallout Test # same thing via the tool, which is what CI invokes
> Change the attribute, then regenerate — once per workflow:
> ```bash
> dotnet fallout --generate-configuration GitHubActions_build --host GitHubActions
> # …and publish-edge · publish-ghcr · publish-release · publish-dockerhub
> # …and publish-develop · publish-ghcr · publish-release · publish-dockerhub
> ```

### Branching — GitFlow (2026-08-16)
Expand All @@ -200,7 +200,7 @@ tagged `v*`. Work goes `feat|fix|chore|docs/*` → PR into `develop`. A release
`release/*` window) **fast-forwarded** into `main`, then tagged. `hotfix/*` is cut from `main` and **must**
be ported back to `develop`.

- Every non-docs push to `develop` republishes the images as `:edge` (`PushEdge`).
- Every non-docs push to `develop` republishes the images as `:develop` (`PushDevelop`).
- `GitHubRelease` **refuses** a tag that is not reachable from `main` or `support/*` — the trunk is
never tagged for release.
- **Never merge a release PR with GitHub's button.** It rewrites the commits, which severs the
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="NSubstitute" Version="6.0.0" />
<!-- FakeTimeProvider — used to prove the ZDF key-discovery rate limit without sleeping (#112). -->
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.10.0" />
<!-- Core ArchUnitNET only: the .xUnit integration package still depends on xunit.assert 2.x,
which conflicts with xunit v3. ArchRuleAssert in Architecture.Tests replaces it. -->
<PackageVersion Include="TngTech.ArchUnitNET" Version="0.13.3" />
Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ Every response carries `<newznab:response offset total/>`, so a client knows whe
stop. Item GUIDs are the stable episode id (`{broadcaster}:{native-id}`), unchanged by a re-crawl —
which is what stops Sonarr grabbing the same episode twice.

Krautwatch can also create that instance for you on first boot — set `SONARR_URL` / `SONARR_API_KEY`
(and the `RADARR_*` pair) in the environment and they are inserted while no instance is configured.
After that the UI owns them and the variables are ignored, so a restart never undoes an edit.

**As a download client** — add a *SABnzbd* client pointing at the same host. Supported modes:
`version`, `get_config`, `addurl`, `addfile`, `queue`, `history`.

Expand Down Expand Up @@ -236,8 +240,13 @@ error: ZDF API rejected our Api-Auth key (401) — it has most likely been rotat
Set Zdf:ApiAuthKey to the current value. ZDF crawling produces nothing until then.
```

The ZDF agent's `/health` also goes **degraded** (amber in the Aspire dashboard; still HTTP 200, so it
won't restart-loop your container). Recovery is a config change and a restart, not a rebuild:
**It usually fixes itself.** ZDF's own web player publishes the current key, so on a rejection
Krautwatch reads it, checks it against the API, and carries on with it — logging loudly, and holding
`/health` at **degraded** until you make it permanent, because the recovered key lives in memory only
and a restart goes back to what is configured. Turn it off with `Zdf__KeyDiscovery__Enabled=false`.

To make it permanent — or if discovery is off or fails — it is a config change and a restart, not a
rebuild:

```
Zdf__ApiAuthKey=<the current value> # environment variable
Expand Down Expand Up @@ -448,7 +457,7 @@ pinned as a local dotnet tool, so run `dotnet tool restore` once on a fresh clon
**The whole CI/CD pipeline is Fallout.** Every workflow under `.github/workflows/` is *generated*
from the `[GitHubActions]` attributes in [`build/Build.CI.GitHubActions.cs`](build/Build.CI.GitHubActions.cs),
and each one only provisions a runner and invokes a target — the gate runs `Test`, the trunk runs
`PushEdge`, a tag runs `PushGhcr` and `GitHubRelease`. Edit the attribute, not the YAML, or your
`PushDevelop`, a tag runs `PushGhcr` and `GitHubRelease`. Edit the attribute, not the YAML, or your
change is overwritten:

```bash
Expand Down Expand Up @@ -500,8 +509,8 @@ gh pr create --base develop --label enhancement # one category label — th

### Running the trunk

Every push to `develop` republishes the images as `:edge` (multi-arch, GHCR). Point an existing
deployment's `.env` at that tag to follow along — it moves under you, migrations included, and
Every push to `develop` republishes the images as `:develop` (multi-arch, GHCR and Docker Hub).
Point an existing deployment's `.env` at that tag to follow along — it moves under you, migrations included, and
downgrading back to a release is not supported, so back up first.

---
Expand Down
15 changes: 8 additions & 7 deletions build/Build.CI.GitHubActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,27 @@
},
InvokedTargets = new[] { nameof(Test) })]

// ── The edge channel (GitFlow's preview channel) ──────────────────────────────
// ── The develop channel (GitFlow's preview channel) ───────────────────────────
//
// Every push to the trunk republishes the six images under `:edge`, so a tester can run the next
// Every push to the trunk republishes the six images under `:develop` — named after the branch
// they are built from — so a tester can run the next
// release before it is a release. Mirrors the extension repo's rolling `preview` VSIX; the shape
// differs only because our artefact is a registry tag rather than a GitHub release asset.
//
// Path exclusions ARE safe here — this is not a required check, so a skipped run blocks nothing,
// and rebuilding six multi-arch images because a markdown file changed is pure waste.
//
// Concurrency QUEUES rather than cancels (ConcurrencyCancelInProgress is left at its default
// false): cancelling a push mid-way can leave `:edge` pointing at a half-written manifest list,
// which is worse than an edge build running a few minutes behind.
// false): cancelling a push mid-way can leave `:develop` pointing at a half-written manifest
// list, which is worse than the channel running a few minutes behind the branch.
[GitHubActions(
"publish-edge",
"publish-develop",
GitHubActionsImage.UbuntuLatest,
FetchDepth = 0,
OnPushBranches = new[] { DevelopBranch },
OnPushExcludePaths = new[] { "**/*.md", "docs/**" },
ConcurrencyGroup = "${{ github.workflow }}",
InvokedTargets = new[] { nameof(PushEdge) },
InvokedTargets = new[] { nameof(PushDevelop) },
EnvironmentName = "ghcr",
ImportSecrets = new[] { nameof(RegistryUser), nameof(RegistryPassword) })]

Expand Down Expand Up @@ -100,7 +101,7 @@
//
// It also refuses to go out from the wrong branch: GitHubRelease asserts the tag is reachable from
// main or a support line (see Build.Release.cs). Under GitFlow the trunk is never tagged for
// release — it ships through the edge channel instead.
// release — it ships through the develop channel instead.
[GitHubActions(
"publish-release",
GitHubActionsImage.UbuntuLatest,
Expand Down
40 changes: 29 additions & 11 deletions build/Build.Publish.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,28 @@ partial class Build
/// <summary>Set by the per-registry targets; falls back to the --registry parameter.</summary>
string _targetRegistry;

/// <summary>Set by the per-registry targets; falls back to --registry-namespace.</summary>
string _targetNamespace;

string EffectiveRegistry => _targetRegistry ?? Registry;

string EffectiveNamespace => _targetNamespace ?? RegistryNamespace;

[Parameter("Registry namespace — the owner or organisation the images live under")]
readonly string RegistryNamespace = "chrison-dev";

/// <summary>
/// The Docker Hub account images are mirrored under.
/// </summary>
/// <remarks>
/// Separate from <see cref="RegistryNamespace"/> because the two registries genuinely disagree: the
/// GitHub org is <c>chrison-dev</c>, and no such namespace exists on Docker Hub (its API answers 404)
/// — pushes there have to go to the account, <c>chrison</c>. Sharing one value meant the mirror could
/// never have worked, which went unnoticed because the channel had no credentials to fail with (#115).
/// </remarks>
[Parameter("Docker Hub namespace — the account images are mirrored under")]
readonly string DockerHubNamespace = "chrison";

[Parameter("Registry username for the push")]
readonly string RegistryUser;

Expand All @@ -56,7 +73,7 @@ partial class Build

AbsolutePath ComposeDirectory => RootDirectory / ".artifacts" / "compose";

/// <summary>Set by <see cref="PushEdge"/>; wins over every other tag resolution.</summary>
/// <summary>Set by <see cref="PushDevelop"/>; wins over every other tag resolution.</summary>
string _tagOverride;

/// <summary>
Expand All @@ -77,7 +94,7 @@ string EffectiveTag
{
get
{
// An edge build runs off a branch, where the resolutions below would answer "dev" —
// A trunk build runs off a branch, where the resolutions below would answer "dev" —
// the point of the channel is that it has its own, stable, name.
if (!string.IsNullOrWhiteSpace(_tagOverride))
return _tagOverride;
Expand Down Expand Up @@ -127,7 +144,7 @@ static readonly (string Service, string Project, string Assembly, bool Ffmpeg)[]
static string LocalImage(string service) => $"krautwatch-{service}";

string RemoteImage(string service) =>
$"{EffectiveRegistry}/{RegistryNamespace}/{LocalImage(service)}";
$"{EffectiveRegistry}/{EffectiveNamespace}/{LocalImage(service)}";

Target Compose => _ => _
.Description("Generate docker-compose.yaml + .env from the Aspire AppHost")
Expand Down Expand Up @@ -191,26 +208,26 @@ string RemoteImage(string service) =>
PushImages();
});

/// <summary>The rolling tag the trunk publishes under.</summary>
/// <summary>The rolling tag the trunk publishes under — named after the branch it comes from.</summary>
/// <remarks>
/// Not "latest": that name is conventionally the newest <i>stable</i> image, and a compose file
/// left on the default tag would silently follow the trunk. Anyone running <c>:edge</c> has
/// typed the word.
/// left on the default tag would silently follow the trunk. Anyone running <c>:develop</c> has
/// typed the name of a branch, which says exactly what they are getting.
/// </remarks>
const string EdgeTag = "edge";
const string DevelopTag = "develop";

Target PushEdge => _ => _
.Description($"Push the rolling ':{EdgeTag}' images to GHCR — CI target for pushes to the trunk")
Target PushDevelop => _ => _
.Description($"Push the rolling ':{DevelopTag}' images to GHCR — CI target for pushes to the trunk")
.DependsOn(Compile)
.Requires(() => RegistryUser)
.Requires(() => RegistryPassword)
.Executes(() =>
{
// The images are the whole channel — there is no separate release artefact to attach,
// as there would be for a versioned release. A tester points their existing .env at
// :edge (docs/releasing.md#the-edge-channel) and pulls.
// :develop (docs/releasing.md#the-develop-channel) and pulls.
_targetRegistry = "ghcr.io";
_tagOverride = EdgeTag;
_tagOverride = DevelopTag;
PushImages();
});

Expand All @@ -222,6 +239,7 @@ string RemoteImage(string service) =>
.Executes(() =>
{
_targetRegistry = "docker.io";
_targetNamespace = DockerHubNamespace;
PushImages();
});

Expand Down
4 changes: 2 additions & 2 deletions build/Build.Release.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
logOutput: false);
probe.WaitForExit();

return probe.ExitCode == 0

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 49 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.
? probe.Output.Select(x => x.Text).FirstOrDefault()?.Trim()
: null;
}
Expand Down Expand Up @@ -110,7 +110,7 @@
/// </summary>
/// <remarks>
/// <para>
/// Under GitFlow the trunk is never tagged for release — <c>develop</c> ships through the edge
/// Under GitFlow the trunk is never tagged for release — <c>develop</c> ships through its own
/// channel, and a release comes from <c>main</c> after a stabilisation window
/// (docs/branching-and-release.md). Without this check that rule is documentation only, and
/// breaking it is silent: a <c>v*</c> tag anywhere publishes real images and a real release
Expand Down Expand Up @@ -149,7 +149,7 @@
Assert.True(releasable,
$"{ReleaseTag} is not reachable from origin/{MainBranch} or a support line — it is on " +
$"[{string.Join(", ", branches)}]. Releases are cut from {MainBranch}; the trunk ships " +
"through the edge channel. See docs/branching-and-release.md.");
"through the develop channel. See docs/branching-and-release.md.");

Log.Information("{Tag} is reachable from {Branches}", ReleaseTag, string.Join(", ", branches));
}
Expand Down Expand Up @@ -334,7 +334,7 @@
/// with "_", so this reverses that rather than duplicating the list of services.
/// </remarks>
static string ServiceForEnvKey(string key) =>
Services.Select(s => s.Service).FirstOrDefault(service =>

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.

Check warning on line 337 in build/Build.Release.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Possible null reference return.
$"{service.ToUpperInvariant().Replace('-', '_')}_IMAGE" == key);

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions docs/branching-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ runs.

| Branch | Purpose | Lifetime | Tagged? |
|---|---|---|---|
| `develop` | **Integration trunk. Default branch.** All finished work lands here first. Every push republishes the [`:edge` images](releasing.md#the-edge-channel). | Permanent | No |
| `develop` | **Integration trunk. Default branch.** All finished work lands here first. Every push republishes the [`:develop` images](releasing.md#the-develop-channel). | Permanent | No |
| `main` | **Production.** Only receives merges from `release/*` and `hotfix/*`, and every one of those is tagged. Never committed to directly. | Permanent | **Yes** |
| `release/*` | **Stabilisation window** for a release being prepared. Cut from `develop`; takes only fixes and release prep. Merges to `main` *and back to* `develop`, then deleted. | Short-lived | No (the merge into `main` is) |
| `hotfix/*` | **Urgent production fix.** Cut from `main`. Merges to `main` *and* `develop`, then deleted. | Short-lived | No (the merge into `main` is) |
Expand Down Expand Up @@ -89,7 +89,7 @@ flowchart TD
A["A change needs to ship"] --> B{"Is a released version<br/>broken right now?"}
B -->|No| C["feat/* or fix/*<br/>off develop"]
C --> D["PR → develop"]
D --> E["Ships on the next release<br/>(and on :edge immediately)"]
D --> E["Ships on the next release<br/>(and on :develop immediately)"]
B -->|Yes| F{"Does it affect the<br/>version main is on?"}
F -->|Yes| G["hotfix/* off main"]
G --> H["PR → main, tag,<br/>then merge back to develop"]
Expand Down
Loading
Loading