1 Commits

Author SHA1 Message Date
mika kuns
9f37b1e21e feat:(workflows) Add Changelogs to Relase 2026-06-04 19:07:05 +02:00
18 changed files with 116 additions and 244 deletions

View File

@@ -5,6 +5,10 @@ on:
tags:
- 'v*'
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
@@ -38,11 +42,52 @@ jobs:
TAG: ${{ steps.ver.outputs.tag }}
run: |
set -euo pipefail
git clone --depth 1 --branch "$TAG" \
# Full clone (with tags) so release notes can diff against the previous tag.
git clone --branch "$TAG" \
"https://oauth2:${TOKEN}@git.kuns.dev/${REPO}.git" \
"$WORK/src"
git -C "$WORK/src" log -1 --oneline
- name: Generate release notes
env:
WORK: ${{ steps.ws.outputs.dir }}
TAG: ${{ steps.ver.outputs.tag }}
run: |
set -euo pipefail
cd "$WORK/src"
PREV="$(git tag --sort=v:refname | grep -E '^v' \
| awk -v t="$TAG" '$0==t{print prev} {prev=$0}')"
if [ -n "$PREV" ]; then
RANGE="${PREV}..${TAG}"
else
RANGE="$TAG"
fi
emit_group() {
# $1 conventional-type, $2 heading
local lines
lines="$(git log "$RANGE" --no-merges --pretty=format:'%s|%h' \
| grep -E "^${1}(\([^)]*\))?(!)?: " || true)"
[ -z "$lines" ] && return 0
printf '### %s\n\n' "$2"
while IFS='|' read -r subject hash; do
printf -- '- %s (%s)\n' "${subject#*: }" "$hash"
done <<< "$lines"
printf '\n'
}
{
emit_group feat "Features"
emit_group fix "Fixes"
emit_group perf "Performance"
emit_group refactor "Refactoring"
emit_group docs "Documentation"
} > RELEASE_NOTES.md
echo "--- release notes ---"
cat RELEASE_NOTES.md
- name: Publish ClaudeDo.App (win-x64, self-contained)
env:
WORK: ${{ steps.ws.outputs.dir }}
@@ -128,7 +173,8 @@ jobs:
BODY=$(jq -n \
--arg tag "$TAG" \
--arg name "$TAG" \
'{tag_name:$tag, name:$name, body:"", draft:false, prerelease:false, target_commitish:"main"}')
--rawfile body "$WORK/src/RELEASE_NOTES.md" \
'{tag_name:$tag, name:$name, body:$body, draft:true, prerelease:false, target_commitish:"main"}')
RESP=$(curl -sS -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
@@ -166,6 +212,32 @@ jobs:
done
echo "All assets uploaded."
- name: Publish release
env:
RELEASE_ID: ${{ steps.release.outputs.release_id }}
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
curl -sS --fail-with-body -X PATCH \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"draft":false}' \
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}" \
> /dev/null
echo "Release ${RELEASE_ID} published."
- name: Delete draft release on failure
if: failure() && steps.release.outputs.release_id != ''
env:
RELEASE_ID: ${{ steps.release.outputs.release_id }}
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
curl -sS -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}" \
> /dev/null || true
echo "Cleaned up draft release ${RELEASE_ID}."
- name: Cleanup workspace
if: always()
env:

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
# Local dev worktrees (created by using-git-worktrees skill)
.worktrees/
# Brainstorming visual companion artifacts
.superpowers/
# .NET build output
bin/
obj/

View File

@@ -7,7 +7,6 @@
<Project Path="src/ClaudeDo.Installer/ClaudeDo.Installer.csproj" />
<Project Path="src/ClaudeDo.Releases/ClaudeDo.Releases.csproj" />
<Project Path="src/ClaudeDo.Localization/ClaudeDo.Localization.csproj" />
<Project Path="src/ClaudeDo.Logging/ClaudeDo.Logging.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj" />

View File

@@ -24,12 +24,10 @@
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
</ItemGroup>
<Import Project="..\ClaudeDo.Localization\Locales.targets" />

View File

@@ -13,8 +13,6 @@ using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.ViewModels.Modals.Settings;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using System;
using System.Globalization;
using System.IO;
@@ -79,12 +77,6 @@ sealed class Program
var sc = new ServiceCollection();
var logRoot = Path.Combine(Path.GetDirectoryName(dbPath)!, "logs");
var serilogLogger = ClaudeDo.Logging.LoggingSetup
.Configure(new LoggerConfiguration(), "app", logRoot)
.CreateLogger();
sc.AddLogging(b => b.AddSerilog(serilogLogger, dispose: true));
// Infrastructure
sc.AddSingleton(settings);
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
@@ -106,9 +98,7 @@ sealed class Program
// Services
sc.AddSingleton<GitService>();
sc.AddSingleton(sp => new WorkerClient(
sp.GetRequiredService<AppSettings>().SignalRUrl,
sp.GetRequiredService<ILogger<WorkerClient>>()));
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
sc.AddSingleton<IWorkerClient>(sp => sp.GetRequiredService<WorkerClient>());
// Release check + installer update

View File

@@ -1,14 +0,0 @@
using System.Diagnostics;
using System.Reflection;
namespace ClaudeDo.Logging;
/// <summary>Runtime build-configuration detection — the replacement for #if DEBUG.
/// Debug builds compile with the JIT optimizer disabled; Release builds enable it.</summary>
public static class BuildConfig
{
public static bool IsDebug { get; } =
Assembly.GetEntryAssembly()
?.GetCustomAttribute<DebuggableAttribute>()
?.IsJITOptimizerDisabled ?? false;
}

View File

@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
</ItemGroup>
</Project>

View File

@@ -1,15 +0,0 @@
using Serilog.Core;
using Serilog.Events;
namespace ClaudeDo.Logging;
/// <summary>Ensures every log event carries a TaskId property (defaulting to "-")
/// so the output template's [{TaskId}] column never renders the raw token.</summary>
public sealed class DefaultTaskIdEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (!logEvent.Properties.ContainsKey("TaskId"))
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TaskId", "-"));
}
}

View File

@@ -1,44 +0,0 @@
using Serilog;
using Serilog.Events;
namespace ClaudeDo.Logging;
public static class LoggingSetup
{
private const string OutputTemplate =
"[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}";
public static LoggerConfiguration Configure(LoggerConfiguration cfg, string processTag, string logRoot)
{
Directory.CreateDirectory(logRoot);
var logFile = Path.Combine(logRoot, "claudedo-.log");
cfg.Enrich.FromLogContext()
.Enrich.WithProperty("Process", processTag)
.Enrich.With(new DefaultTaskIdEnricher());
if (BuildConfig.IsDebug)
{
cfg.MinimumLevel.Debug()
.WriteTo.Console(outputTemplate: OutputTemplate)
.WriteTo.File(
logFile,
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 2,
shared: true,
outputTemplate: OutputTemplate);
}
else
{
cfg.MinimumLevel.Warning()
.WriteTo.File(
logFile,
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 2,
shared: true,
outputTemplate: OutputTemplate);
}
return cfg;
}
}

View File

@@ -11,9 +11,7 @@
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Serilog" Version="4.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,8 +6,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog.Context;
namespace ClaudeDo.Ui.Services;
@@ -32,7 +30,6 @@ sealed class IndefiniteRetryPolicy : IRetryPolicy
public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient
{
private readonly HubConnection _hub;
private readonly ILogger<WorkerClient> _logger;
private CancellationTokenSource? _startCts;
private Task _retryLoopTask = Task.CompletedTask;
private readonly object _startLock = new();
@@ -68,9 +65,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public string? LastMergeAllTarget { get; private set; }
public WorkerClient(string signalRUrl, ILogger<WorkerClient> logger)
public WorkerClient(string signalRUrl)
{
_logger = logger;
_hub = new HubConnectionBuilder()
.WithUrl(signalRUrl)
.WithAutomaticReconnect(new IndefiniteRetryPolicy())
@@ -244,24 +240,20 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
catch { return default; }
}
/// <summary>Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line.</summary>
private async Task InvokeForTaskAsync(string taskId, string method, params object?[] args)
public async Task RunNowAsync(string taskId)
{
using (LogContext.PushProperty("TaskId", taskId))
{
_logger.LogDebug("UI invoking {Method} for task {TaskId}", method, taskId);
await _hub.InvokeCoreAsync(method, args);
}
await _hub.InvokeAsync("RunNow", taskId);
}
public Task RunNowAsync(string taskId)
=> InvokeForTaskAsync(taskId, "RunNow", taskId);
public async Task ContinueTaskAsync(string taskId, string followUpPrompt)
{
await _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt);
}
public Task ContinueTaskAsync(string taskId, string followUpPrompt)
=> InvokeForTaskAsync(taskId, "ContinueTask", taskId, followUpPrompt);
public Task ResetTaskAsync(string taskId)
=> InvokeForTaskAsync(taskId, "ResetTask", taskId);
public async Task ResetTaskAsync(string taskId)
{
await _hub.InvokeAsync("ResetTask", taskId);
}
public async Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage)
{
@@ -272,8 +264,10 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
=> TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);
public Task CancelTaskAsync(string taskId)
=> InvokeForTaskAsync(taskId, "CancelTask", taskId);
public async Task CancelTaskAsync(string taskId)
{
await _hub.InvokeAsync("CancelTask", taskId);
}
public async Task WakeQueueAsync()
{
@@ -392,17 +386,25 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
}
public Task ApproveReviewAsync(string taskId)
=> InvokeForTaskAsync(taskId, "ApproveReview", taskId);
public async Task ApproveReviewAsync(string taskId)
{
await _hub.InvokeAsync("ApproveReview", taskId);
}
public Task RejectReviewToQueueAsync(string taskId, string feedback)
=> InvokeForTaskAsync(taskId, "RejectReviewToQueue", taskId, feedback);
public async Task RejectReviewToQueueAsync(string taskId, string feedback)
{
await _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback);
}
public Task RejectReviewToIdleAsync(string taskId)
=> InvokeForTaskAsync(taskId, "RejectReviewToIdle", taskId);
public async Task RejectReviewToIdleAsync(string taskId)
{
await _hub.InvokeAsync("RejectReviewToIdle", taskId);
}
public Task CancelReviewAsync(string taskId)
=> InvokeForTaskAsync(taskId, "CancelReview", taskId);
public async Task CancelReviewAsync(string taskId)
{
await _hub.InvokeAsync("CancelReview", taskId);
}
public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
=> TryInvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);

View File

@@ -2,7 +2,6 @@
<ItemGroup>
<ProjectReference Include="..\ClaudeDo.Data\ClaudeDo.Data.csproj" />
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -31,8 +31,13 @@ var builder = WebApplication.CreateBuilder(args);
var logRoot = cfg.LogRoot;
Directory.CreateDirectory(logRoot);
builder.Host.UseSerilog((ctx, lc) =>
ClaudeDo.Logging.LoggingSetup.Configure(lc, "worker", logRoot));
builder.Host.UseSerilog((ctx, lc) => lc
.MinimumLevel.Information()
.WriteTo.File(
System.IO.Path.Combine(logRoot, "worker-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7,
shared: true));
builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt =>
opt.UseSqlite($"Data Source={cfg.DbPath}"));

View File

@@ -1,6 +1,5 @@
using System.Text.Json;
using ClaudeDo.Data;
using Serilog.Context;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
@@ -47,7 +46,6 @@ public sealed class TaskRunner
public async Task RunAsync(TaskEntity task, string slot, CancellationToken ct)
{
using var _taskScope = LogContext.PushProperty("TaskId", task.Id);
string? mcpToken = null;
string? mcpConfigPath = null;
try
@@ -172,7 +170,6 @@ public sealed class TaskRunner
public async Task ContinueAsync(string taskId, string followUpPrompt, string slot, CancellationToken ct)
{
using var _taskScope = LogContext.PushProperty("TaskId", taskId);
TaskEntity task;
TaskRunEntity lastRun;
ListEntity list;

View File

@@ -25,7 +25,6 @@
<ProjectReference Include="..\..\src\ClaudeDo.Worker\ClaudeDo.Worker.csproj" />
<ProjectReference Include="..\..\src\ClaudeDo.Data\ClaudeDo.Data.csproj" />
<ProjectReference Include="..\..\src\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
<ProjectReference Include="..\..\src\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,19 +0,0 @@
using System.Diagnostics;
using System.Reflection;
using ClaudeDo.Logging;
namespace ClaudeDo.Worker.Tests.Logging;
public sealed class BuildConfigTests
{
[Fact]
public void IsDebug_MatchesEntryAssemblyDebuggableAttribute()
{
var entry = Assembly.GetEntryAssembly();
var expected = entry?
.GetCustomAttribute<DebuggableAttribute>()
?.IsJITOptimizerDisabled ?? false;
Assert.Equal(expected, BuildConfig.IsDebug);
}
}

View File

@@ -1,49 +0,0 @@
using ClaudeDo.Logging;
using Serilog;
using Serilog.Context;
using Serilog.Core;
using Serilog.Events;
namespace ClaudeDo.Worker.Tests.Logging;
public sealed class DefaultTaskIdEnricherTests
{
private sealed class CollectingSink : ILogEventSink
{
public List<LogEvent> Events { get; } = new();
public void Emit(LogEvent logEvent) => Events.Add(logEvent);
}
[Fact]
public void AddsDash_WhenNoTaskIdInScope()
{
var sink = new CollectingSink();
using var logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With(new DefaultTaskIdEnricher())
.WriteTo.Sink(sink)
.CreateLogger();
logger.Information("hello");
var prop = Assert.Single(sink.Events).Properties["TaskId"];
Assert.Equal("\"-\"", prop.ToString());
}
[Fact]
public void KeepsPushedTaskId_WhenInScope()
{
var sink = new CollectingSink();
using var logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With(new DefaultTaskIdEnricher())
.WriteTo.Sink(sink)
.CreateLogger();
using (LogContext.PushProperty("TaskId", "task-42"))
logger.Information("hello");
var prop = Assert.Single(sink.Events).Properties["TaskId"];
Assert.Equal("\"task-42\"", prop.ToString());
}
}

View File

@@ -1,30 +0,0 @@
using ClaudeDo.Logging;
using Serilog;
namespace ClaudeDo.Worker.Tests.Logging;
public sealed class LoggingSetupTests
{
[Fact]
public void Configure_WritesSharedLogFile()
{
var logRoot = Path.Combine(Path.GetTempPath(), "claudedo-logtest-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(logRoot);
try
{
var logger = LoggingSetup.Configure(new LoggerConfiguration(), "test", logRoot).CreateLogger();
logger.Warning("marker-{Marker}", "xyz");
logger.Dispose(); // flush + release the file handle
var files = Directory.GetFiles(logRoot, "claudedo-*.log");
var file = Assert.Single(files);
var contents = File.ReadAllText(file);
Assert.Contains("marker-", contents);
Assert.Contains("test/", contents); // {Process} tag in the template
}
finally
{
try { Directory.Delete(logRoot, recursive: true); } catch { /* best effort */ }
}
}
}