summaryrefslogtreecommitdiff
path: root/src/marlin.web
diff options
context:
space:
mode:
authorMelodi <62381002+Melodi17@users.noreply.github.com>2026-10-27 23:04:59 +1000
committerMelodi <62381002+Melodi17@users.noreply.github.com>2026-10-27 23:04:59 +1000
commitecd5f5d13936dd097b12766cb95a568ad5b073a9 (patch)
treef360b08e656a98faca4f2b9f10dc44602e6ca289 /src/marlin.web
parent88ee626cb3686dc176cd7911b073c4e65fa54d34 (diff)
feat: lots of stuff
Diffstat (limited to 'src/marlin.web')
-rw-r--r--src/marlin.web/Controllers/Api/DispatchController.cs (renamed from src/marlin.web/Controllers/DispatchController.cs)24
-rw-r--r--src/marlin.web/Controllers/Api/InstanceGrpcService.cs54
-rw-r--r--src/marlin.web/Controllers/JobsController.cs4
-rw-r--r--src/marlin.web/Data/ApplicationDbContext.cs2
-rw-r--r--src/marlin.web/Migrations/20261027095603_AddLogEvent.Designer.cs565
-rw-r--r--src/marlin.web/Migrations/20261027095603_AddLogEvent.cs67
-rw-r--r--src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.Designer.cs565
-rw-r--r--src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.cs78
-rw-r--r--src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs61
-rw-r--r--src/marlin.web/Models/Job.cs13
-rw-r--r--src/marlin.web/Models/LogEventRecord.cs16
-rw-r--r--src/marlin.web/Program.cs8
-rw-r--r--src/marlin.web/Services/VersionControlSyncService.cs2
-rw-r--r--src/marlin.web/Views/Jobs/Details.cshtml25
-rw-r--r--src/marlin.web/marlin.web.csproj2
15 files changed, 1456 insertions, 30 deletions
diff --git a/src/marlin.web/Controllers/DispatchController.cs b/src/marlin.web/Controllers/Api/DispatchController.cs
index a37d5fa..076c4dd 100644
--- a/src/marlin.web/Controllers/DispatchController.cs
+++ b/src/marlin.web/Controllers/Api/DispatchController.cs
@@ -1,15 +1,13 @@
-namespace marlin.web.Controllers;
+namespace marlin.web.Controllers.Api;
-using core.models;
-using core.models.meta;
-using core.util;
-using Data;
+using marlin.core.models;
+using marlin.core.util;
+using marlin.web.Data;
+using marlin.web.Models;
+using marlin.web.Util;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
-using Models;
-using Services;
-using Util;
[Route("api/dispatch")]
[ApiController]
@@ -19,7 +17,7 @@ public class DispatchController(ApplicationDbContext dbContext) : ControllerBase
[HttpPost("handshake")]
public async Task<ActionResult<AgentConfiguration>> Handshake(AgentStatus status, CancellationToken ct)
{
- var agentId = User.RequireAgentId();
+ var agentId = this.User.RequireAgentId();
var agent = await dbContext.Agents
.FirstAsync(x => x.Id == agentId, ct);
@@ -34,11 +32,11 @@ public class DispatchController(ApplicationDbContext dbContext) : ControllerBase
MaxConcurrentJobs: agent.MaxConcurrentJobs
);
}
-
+
[HttpGet("queue")]
public async Task<ActionResult<DispatchJob?>> Queue(CancellationToken ct)
{
- var agentId = User.RequireAgentId();
+ var agentId = this.User.RequireAgentId();
var nextJob = await dbContext
.Jobs
@@ -50,7 +48,7 @@ public class DispatchController(ApplicationDbContext dbContext) : ControllerBase
.FirstOrDefaultAsync(ct);
if (nextJob == null)
- return (DispatchJob?) null;
+ return (DispatchJob?)null;
string claimToken = SecureTokenGenerator.CreateClaimToken();
nextJob.ClaimToken = claimToken;
@@ -66,6 +64,6 @@ public class DispatchController(ApplicationDbContext dbContext) : ControllerBase
Commit: nextJob.Commit.Hash,
Recipe: nextJob.Recipe
);
- }
+ }
}
diff --git a/src/marlin.web/Controllers/Api/InstanceGrpcService.cs b/src/marlin.web/Controllers/Api/InstanceGrpcService.cs
new file mode 100644
index 0000000..fb0af5a
--- /dev/null
+++ b/src/marlin.web/Controllers/Api/InstanceGrpcService.cs
@@ -0,0 +1,54 @@
+namespace marlin.web.Controllers.Api;
+
+using core.contracts;
+using core.models;
+using core.models.plan;
+using Data;
+using Grpc.Core;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Models;
+using ProtoBuf.Grpc;
+using Util;
+
+[Authorize(Policy = "JobScoped")]
+public class InstanceGrpcService(ApplicationDbContext dbContext) : IInstanceService
+{
+ public async Task SendLogEvent(LogEventRequest request, CallContext context = default)
+ {
+ var user = context.ServerCallContext!.GetHttpContext().User;
+ var jobId = user.RequireJobId();
+
+ dbContext.Logs.Add(new LogEventRecord
+ {
+ At = request.At,
+ Message = request.Message,
+ Error = request.Error,
+ StepId = request.StepId,
+ JobId = jobId
+ });
+
+ await dbContext.SaveChangesAsync(context.CancellationToken);
+ }
+
+ public async Task SendStatusUpdate(StatusUpdateRequest request, CallContext context = default)
+ {
+ var user = context.ServerCallContext!.GetHttpContext().User;
+ var jobId = user.RequireJobId();
+
+ var job = await dbContext.Jobs
+ .FirstAsync(x => x.Id == jobId, context.CancellationToken);
+
+ job.Status = request.Status;
+
+ dbContext.JobStatusUpdates.Add(new JobStatusUpdate
+ {
+ At = DateTime.UtcNow,
+ Status = request.Status,
+ Job = job
+ });
+
+ await dbContext.SaveChangesAsync(context.CancellationToken);
+ }
+}
diff --git a/src/marlin.web/Controllers/JobsController.cs b/src/marlin.web/Controllers/JobsController.cs
index 552b56c..9aaf6eb 100644
--- a/src/marlin.web/Controllers/JobsController.cs
+++ b/src/marlin.web/Controllers/JobsController.cs
@@ -95,6 +95,10 @@ public class JobsController(ApplicationDbContext dbContext) : Controller
.Include(x => x.Agent)
.Include(x => x.Source)
.Include(x => x.StatusUpdates)
+ .Include(x => x.Logs)
+ .Include(x => x.Branch)
+ .Include(x => x.Commit)
+ .AsSplitQuery()
.FirstOrDefaultAsync(x => x.Id == id);
if (job == null)
diff --git a/src/marlin.web/Data/ApplicationDbContext.cs b/src/marlin.web/Data/ApplicationDbContext.cs
index cb6b05b..cbff186 100644
--- a/src/marlin.web/Data/ApplicationDbContext.cs
+++ b/src/marlin.web/Data/ApplicationDbContext.cs
@@ -26,8 +26,10 @@ public class ApplicationDbContext : DbContext
public DbSet<VersionControlledBlob> SourceBlobs => this.Set<VersionControlledBlob>();
public DbSet<VersionControlSecret> Secrets => this.Set<VersionControlSecret>();
public DbSet<Job> Jobs => this.Set<Job>();
+ public DbSet<JobStatusUpdate> JobStatusUpdates => this.Set<JobStatusUpdate>();
public DbSet<Agent> Agents => this.Set<Agent>();
public DbSet<WorkflowSnapshot> WorkflowSnapshots => this.Set<WorkflowSnapshot>();
+ public DbSet<LogEventRecord> Logs => this.Set<LogEventRecord>();
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
diff --git a/src/marlin.web/Migrations/20261027095603_AddLogEvent.Designer.cs b/src/marlin.web/Migrations/20261027095603_AddLogEvent.Designer.cs
new file mode 100644
index 0000000..726076f
--- /dev/null
+++ b/src/marlin.web/Migrations/20261027095603_AddLogEvent.Designer.cs
@@ -0,0 +1,565 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using marlin.web.Data;
+
+#nullable disable
+
+namespace marlin.web.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20261027095603_AddLogEvent")]
+ partial class AddLogEvent
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("marlin.web.Models.Agent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("AuthToken")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("auth_token");
+
+ b.Property<string>("Capabilities")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("capabilities");
+
+ b.Property<DateTime>("LastSeen")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_seen");
+
+ b.Property<int>("MaxConcurrentJobs")
+ .HasColumnType("integer")
+ .HasColumnName("max_concurrent_jobs");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("pk_agents");
+
+ b.ToTable("agents", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Job", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AgentId")
+ .HasColumnType("uuid")
+ .HasColumnName("agent_id");
+
+ b.Property<Guid>("BranchId")
+ .HasColumnType("uuid")
+ .HasColumnName("branch_id");
+
+ b.Property<string>("ClaimToken")
+ .HasColumnType("text")
+ .HasColumnName("claim_token");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("CreatedReason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("created_reason");
+
+ b.Property<string>("Recipe")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("recipe");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.Property<int>("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.HasKey("Id")
+ .HasName("pk_jobs");
+
+ b.HasIndex("AgentId")
+ .HasDatabaseName("ix_jobs_agent_id");
+
+ b.HasIndex("BranchId")
+ .HasDatabaseName("ix_jobs_branch_id");
+
+ b.HasIndex("CommitId")
+ .HasDatabaseName("ix_jobs_commit_id");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_jobs_source_id");
+
+ b.ToTable("jobs", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.JobStatusUpdate", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("At")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("at");
+
+ b.Property<Guid>("JobId")
+ .HasColumnType("uuid")
+ .HasColumnName("job_id");
+
+ b.Property<int>("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.HasKey("Id")
+ .HasName("pk_job_status_update");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_job_status_update_job_id");
+
+ b.ToTable("job_status_update", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.LogEventRecord", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("At")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("at");
+
+ b.Property<bool>("Error")
+ .HasColumnType("boolean")
+ .HasColumnName("error");
+
+ b.Property<Guid>("JobId")
+ .HasColumnType("uuid")
+ .HasColumnName("job_id");
+
+ b.Property<string>("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property<string>("StepId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("step_id");
+
+ b.HasKey("Id")
+ .HasName("pk_logs");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_logs_job_id");
+
+ b.ToTable("logs", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Content")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("content");
+
+ b.Property<string>("Kind")
+ .HasColumnType("text")
+ .HasColumnName("kind");
+
+ b.Property<string>("Label")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("label");
+
+ b.Property<bool>("MayReveal")
+ .HasColumnType("boolean")
+ .HasColumnName("may_reveal");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.HasKey("Id")
+ .HasName("pk_secrets");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_secrets_source_id");
+
+ b.ToTable("secrets", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSource", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AccessSecretId")
+ .HasColumnType("uuid")
+ .HasColumnName("access_secret_id");
+
+ b.Property<string>("BranchFilter")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("branch_filter");
+
+ b.Property<string>("MainBranch")
+ .HasColumnType("text")
+ .HasColumnName("main_branch");
+
+ b.Property<int>("MinPollingInterval")
+ .HasColumnType("integer")
+ .HasColumnName("min_polling_interval");
+
+ b.Property<string>("UpstreamUrl")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("upstream_url");
+
+ b.HasKey("Id")
+ .HasName("pk_sources");
+
+ b.ToTable("sources", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBlob", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<byte[]>("Content")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("content");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("path");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.HasKey("Id")
+ .HasName("pk_source_blobs");
+
+ b.HasIndex("CommitId")
+ .HasDatabaseName("ix_source_blobs_commit_id");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_source_blobs_source_id");
+
+ b.ToTable("source_blobs", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBranch", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.HasKey("Id")
+ .HasName("pk_source_branches");
+
+ b.HasIndex("CommitId")
+ .HasDatabaseName("ix_source_branches_commit_id");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_source_branches_source_id");
+
+ b.ToTable("source_branches", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledCommit", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("BlobsFetched")
+ .HasColumnType("boolean")
+ .HasColumnName("blobs_fetched");
+
+ b.Property<string>("Hash")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hash");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.Property<bool>("WorkflowScanned")
+ .HasColumnType("boolean")
+ .HasColumnName("workflow_scanned");
+
+ b.HasKey("Id")
+ .HasName("pk_source_commits");
+
+ b.HasAlternateKey("SourceId", "Hash")
+ .HasName("ak_source_commits_source_id_hash");
+
+ b.ToTable("source_commits", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.WorkflowSnapshot", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<DateTime>("ScannedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("scanned_at");
+
+ b.Property<string>("Workflow")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("workflow");
+
+ b.HasKey("Id")
+ .HasName("pk_workflow_snapshots");
+
+ b.HasIndex("CommitId")
+ .IsUnique()
+ .HasDatabaseName("ix_workflow_snapshots_commit_id");
+
+ b.ToTable("workflow_snapshots", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Job", b =>
+ {
+ b.HasOne("marlin.web.Models.Agent", "Agent")
+ .WithMany("Jobs")
+ .HasForeignKey("AgentId")
+ .HasConstraintName("fk_jobs_agents_agent_id");
+
+ b.HasOne("marlin.web.Models.VersionControlledBranch", "Branch")
+ .WithMany()
+ .HasForeignKey("BranchId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_jobs_source_branches_branch_id");
+
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithMany()
+ .HasForeignKey("CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_jobs_source_commits_commit_id");
+
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_jobs_sources_source_id");
+
+ b.Navigation("Agent");
+
+ b.Navigation("Branch");
+
+ b.Navigation("Commit");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.JobStatusUpdate", b =>
+ {
+ b.HasOne("marlin.web.Models.Job", "Job")
+ .WithMany("StatusUpdates")
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_job_status_update_jobs_job_id");
+
+ b.Navigation("Job");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.LogEventRecord", b =>
+ {
+ b.HasOne("marlin.web.Models.Job", "Job")
+ .WithMany("Logs")
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_logs_jobs_job_id");
+
+ b.Navigation("Job");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_secrets_sources_source_id");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBlob", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithMany("Blobs")
+ .HasForeignKey("CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_blobs_source_commits_commit_id");
+
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_blobs_sources_source_id");
+
+ b.Navigation("Commit");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBranch", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithMany("Branches")
+ .HasForeignKey("CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_branches_source_commits_commit_id");
+
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany("Branches")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_branches_sources_source_id");
+
+ b.Navigation("Commit");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledCommit", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_commits_sources_source_id");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.WorkflowSnapshot", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithOne("WorkflowSnapshot")
+ .HasForeignKey("marlin.web.Models.WorkflowSnapshot", "CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_workflow_snapshots_source_commits_commit_id");
+
+ b.Navigation("Commit");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Agent", b =>
+ {
+ b.Navigation("Jobs");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Job", b =>
+ {
+ b.Navigation("Logs");
+
+ b.Navigation("StatusUpdates");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSource", b =>
+ {
+ b.Navigation("Branches");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledCommit", b =>
+ {
+ b.Navigation("Blobs");
+
+ b.Navigation("Branches");
+
+ b.Navigation("WorkflowSnapshot");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/marlin.web/Migrations/20261027095603_AddLogEvent.cs b/src/marlin.web/Migrations/20261027095603_AddLogEvent.cs
new file mode 100644
index 0000000..b32f68e
--- /dev/null
+++ b/src/marlin.web/Migrations/20261027095603_AddLogEvent.cs
@@ -0,0 +1,67 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace marlin.web.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddLogEvent : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "main_branch",
+ table: "sources",
+ type: "text",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.CreateTable(
+ name: "logs",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ job_id = table.Column<Guid>(type: "uuid", nullable: false),
+ at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ message = table.Column<string>(type: "text", nullable: false),
+ error = table.Column<bool>(type: "boolean", nullable: false),
+ step_id = table.Column<string>(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_logs", x => x.id);
+ table.ForeignKey(
+ name: "fk_logs_jobs_job_id",
+ column: x => x.job_id,
+ principalTable: "jobs",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_logs_job_id",
+ table: "logs",
+ column: "job_id");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "logs");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "main_branch",
+ table: "sources",
+ type: "text",
+ nullable: false,
+ defaultValue: "",
+ oldClrType: typeof(string),
+ oldType: "text",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.Designer.cs b/src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.Designer.cs
new file mode 100644
index 0000000..5a199ba
--- /dev/null
+++ b/src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.Designer.cs
@@ -0,0 +1,565 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using marlin.web.Data;
+
+#nullable disable
+
+namespace marlin.web.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20261027130151_JobStatusUpdatesTable")]
+ partial class JobStatusUpdatesTable
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("marlin.web.Models.Agent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("AuthToken")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("auth_token");
+
+ b.Property<string>("Capabilities")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("capabilities");
+
+ b.Property<DateTime>("LastSeen")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_seen");
+
+ b.Property<int>("MaxConcurrentJobs")
+ .HasColumnType("integer")
+ .HasColumnName("max_concurrent_jobs");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("pk_agents");
+
+ b.ToTable("agents", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Job", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AgentId")
+ .HasColumnType("uuid")
+ .HasColumnName("agent_id");
+
+ b.Property<Guid>("BranchId")
+ .HasColumnType("uuid")
+ .HasColumnName("branch_id");
+
+ b.Property<string>("ClaimToken")
+ .HasColumnType("text")
+ .HasColumnName("claim_token");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("CreatedReason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("created_reason");
+
+ b.Property<string>("Recipe")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("recipe");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.Property<int>("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.HasKey("Id")
+ .HasName("pk_jobs");
+
+ b.HasIndex("AgentId")
+ .HasDatabaseName("ix_jobs_agent_id");
+
+ b.HasIndex("BranchId")
+ .HasDatabaseName("ix_jobs_branch_id");
+
+ b.HasIndex("CommitId")
+ .HasDatabaseName("ix_jobs_commit_id");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_jobs_source_id");
+
+ b.ToTable("jobs", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.JobStatusUpdate", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("At")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("at");
+
+ b.Property<Guid>("JobId")
+ .HasColumnType("uuid")
+ .HasColumnName("job_id");
+
+ b.Property<int>("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.HasKey("Id")
+ .HasName("pk_job_status_updates");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_job_status_updates_job_id");
+
+ b.ToTable("job_status_updates", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.LogEventRecord", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("At")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("at");
+
+ b.Property<bool>("Error")
+ .HasColumnType("boolean")
+ .HasColumnName("error");
+
+ b.Property<Guid>("JobId")
+ .HasColumnType("uuid")
+ .HasColumnName("job_id");
+
+ b.Property<string>("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property<string>("StepId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("step_id");
+
+ b.HasKey("Id")
+ .HasName("pk_logs");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_logs_job_id");
+
+ b.ToTable("logs", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Content")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("content");
+
+ b.Property<string>("Kind")
+ .HasColumnType("text")
+ .HasColumnName("kind");
+
+ b.Property<string>("Label")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("label");
+
+ b.Property<bool>("MayReveal")
+ .HasColumnType("boolean")
+ .HasColumnName("may_reveal");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.HasKey("Id")
+ .HasName("pk_secrets");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_secrets_source_id");
+
+ b.ToTable("secrets", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSource", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AccessSecretId")
+ .HasColumnType("uuid")
+ .HasColumnName("access_secret_id");
+
+ b.Property<string>("BranchFilter")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("branch_filter");
+
+ b.Property<string>("MainBranch")
+ .HasColumnType("text")
+ .HasColumnName("main_branch");
+
+ b.Property<int>("MinPollingInterval")
+ .HasColumnType("integer")
+ .HasColumnName("min_polling_interval");
+
+ b.Property<string>("UpstreamUrl")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("upstream_url");
+
+ b.HasKey("Id")
+ .HasName("pk_sources");
+
+ b.ToTable("sources", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBlob", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<byte[]>("Content")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("content");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("path");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.HasKey("Id")
+ .HasName("pk_source_blobs");
+
+ b.HasIndex("CommitId")
+ .HasDatabaseName("ix_source_blobs_commit_id");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_source_blobs_source_id");
+
+ b.ToTable("source_blobs", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBranch", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.HasKey("Id")
+ .HasName("pk_source_branches");
+
+ b.HasIndex("CommitId")
+ .HasDatabaseName("ix_source_branches_commit_id");
+
+ b.HasIndex("SourceId")
+ .HasDatabaseName("ix_source_branches_source_id");
+
+ b.ToTable("source_branches", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledCommit", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("BlobsFetched")
+ .HasColumnType("boolean")
+ .HasColumnName("blobs_fetched");
+
+ b.Property<string>("Hash")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hash");
+
+ b.Property<Guid>("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.Property<bool>("WorkflowScanned")
+ .HasColumnType("boolean")
+ .HasColumnName("workflow_scanned");
+
+ b.HasKey("Id")
+ .HasName("pk_source_commits");
+
+ b.HasAlternateKey("SourceId", "Hash")
+ .HasName("ak_source_commits_source_id_hash");
+
+ b.ToTable("source_commits", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.WorkflowSnapshot", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid>("CommitId")
+ .HasColumnType("uuid")
+ .HasColumnName("commit_id");
+
+ b.Property<DateTime>("ScannedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("scanned_at");
+
+ b.Property<string>("Workflow")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("workflow");
+
+ b.HasKey("Id")
+ .HasName("pk_workflow_snapshots");
+
+ b.HasIndex("CommitId")
+ .IsUnique()
+ .HasDatabaseName("ix_workflow_snapshots_commit_id");
+
+ b.ToTable("workflow_snapshots", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Job", b =>
+ {
+ b.HasOne("marlin.web.Models.Agent", "Agent")
+ .WithMany("Jobs")
+ .HasForeignKey("AgentId")
+ .HasConstraintName("fk_jobs_agents_agent_id");
+
+ b.HasOne("marlin.web.Models.VersionControlledBranch", "Branch")
+ .WithMany()
+ .HasForeignKey("BranchId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_jobs_source_branches_branch_id");
+
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithMany()
+ .HasForeignKey("CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_jobs_source_commits_commit_id");
+
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_jobs_sources_source_id");
+
+ b.Navigation("Agent");
+
+ b.Navigation("Branch");
+
+ b.Navigation("Commit");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.JobStatusUpdate", b =>
+ {
+ b.HasOne("marlin.web.Models.Job", "Job")
+ .WithMany("StatusUpdates")
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_job_status_updates_jobs_job_id");
+
+ b.Navigation("Job");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.LogEventRecord", b =>
+ {
+ b.HasOne("marlin.web.Models.Job", "Job")
+ .WithMany("Logs")
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_logs_jobs_job_id");
+
+ b.Navigation("Job");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_secrets_sources_source_id");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBlob", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithMany("Blobs")
+ .HasForeignKey("CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_blobs_source_commits_commit_id");
+
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_blobs_sources_source_id");
+
+ b.Navigation("Commit");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledBranch", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithMany("Branches")
+ .HasForeignKey("CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_branches_source_commits_commit_id");
+
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany("Branches")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_branches_sources_source_id");
+
+ b.Navigation("Commit");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledCommit", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlSource", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_source_commits_sources_source_id");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.WorkflowSnapshot", b =>
+ {
+ b.HasOne("marlin.web.Models.VersionControlledCommit", "Commit")
+ .WithOne("WorkflowSnapshot")
+ .HasForeignKey("marlin.web.Models.WorkflowSnapshot", "CommitId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_workflow_snapshots_source_commits_commit_id");
+
+ b.Navigation("Commit");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Agent", b =>
+ {
+ b.Navigation("Jobs");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.Job", b =>
+ {
+ b.Navigation("Logs");
+
+ b.Navigation("StatusUpdates");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlSource", b =>
+ {
+ b.Navigation("Branches");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.VersionControlledCommit", b =>
+ {
+ b.Navigation("Blobs");
+
+ b.Navigation("Branches");
+
+ b.Navigation("WorkflowSnapshot");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.cs b/src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.cs
new file mode 100644
index 0000000..9cf6546
--- /dev/null
+++ b/src/marlin.web/Migrations/20261027130151_JobStatusUpdatesTable.cs
@@ -0,0 +1,78 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace marlin.web.Migrations
+{
+ /// <inheritdoc />
+ public partial class JobStatusUpdatesTable : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_job_status_update_jobs_job_id",
+ table: "job_status_update");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_job_status_update",
+ table: "job_status_update");
+
+ migrationBuilder.RenameTable(
+ name: "job_status_update",
+ newName: "job_status_updates");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_job_status_update_job_id",
+ table: "job_status_updates",
+ newName: "ix_job_status_updates_job_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_job_status_updates",
+ table: "job_status_updates",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_job_status_updates_jobs_job_id",
+ table: "job_status_updates",
+ column: "job_id",
+ principalTable: "jobs",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_job_status_updates_jobs_job_id",
+ table: "job_status_updates");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_job_status_updates",
+ table: "job_status_updates");
+
+ migrationBuilder.RenameTable(
+ name: "job_status_updates",
+ newName: "job_status_update");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_job_status_updates_job_id",
+ table: "job_status_update",
+ newName: "ix_job_status_update_job_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_job_status_update",
+ table: "job_status_update",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_job_status_update_jobs_job_id",
+ table: "job_status_update",
+ column: "job_id",
+ principalTable: "jobs",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs b/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs
index 411706b..da08d88 100644
--- a/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -141,12 +141,50 @@ namespace marlin.web.Migrations
.HasColumnName("status");
b.HasKey("Id")
- .HasName("pk_job_status_update");
+ .HasName("pk_job_status_updates");
b.HasIndex("JobId")
- .HasDatabaseName("ix_job_status_update_job_id");
+ .HasDatabaseName("ix_job_status_updates_job_id");
- b.ToTable("job_status_update", (string)null);
+ b.ToTable("job_status_updates", (string)null);
+ });
+
+ modelBuilder.Entity("marlin.web.Models.LogEventRecord", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("At")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("at");
+
+ b.Property<bool>("Error")
+ .HasColumnType("boolean")
+ .HasColumnName("error");
+
+ b.Property<Guid>("JobId")
+ .HasColumnType("uuid")
+ .HasColumnName("job_id");
+
+ b.Property<string>("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property<string>("StepId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("step_id");
+
+ b.HasKey("Id")
+ .HasName("pk_logs");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_logs_job_id");
+
+ b.ToTable("logs", (string)null);
});
modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b =>
@@ -204,7 +242,6 @@ namespace marlin.web.Migrations
.HasColumnName("branch_filter");
b.Property<string>("MainBranch")
- .IsRequired()
.HasColumnType("text")
.HasColumnName("main_branch");
@@ -399,7 +436,19 @@ namespace marlin.web.Migrations
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
- .HasConstraintName("fk_job_status_update_jobs_job_id");
+ .HasConstraintName("fk_job_status_updates_jobs_job_id");
+
+ b.Navigation("Job");
+ });
+
+ modelBuilder.Entity("marlin.web.Models.LogEventRecord", b =>
+ {
+ b.HasOne("marlin.web.Models.Job", "Job")
+ .WithMany("Logs")
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_logs_jobs_job_id");
b.Navigation("Job");
});
@@ -489,6 +538,8 @@ namespace marlin.web.Migrations
modelBuilder.Entity("marlin.web.Models.Job", b =>
{
+ b.Navigation("Logs");
+
b.Navigation("StatusUpdates");
});
diff --git a/src/marlin.web/Models/Job.cs b/src/marlin.web/Models/Job.cs
index bd60773..f7b6278 100644
--- a/src/marlin.web/Models/Job.cs
+++ b/src/marlin.web/Models/Job.cs
@@ -1,5 +1,7 @@
namespace marlin.web.Models;
+using core.models;
+
public class Job
{
public Guid Id { get; set; }
@@ -25,6 +27,8 @@ public class Job
public string Recipe { get; set; }
// plan
+
+ public ICollection<LogEventRecord> Logs { get; set; }
}
public class JobStatusUpdate
@@ -36,13 +40,4 @@ public class JobStatusUpdate
public DateTime At { get; set; }
public JobStatus Status { get; set; }
-}
-
-public enum JobStatus
-{
- Pending,
- Active,
- Cancelled,
- Success,
- Failed
} \ No newline at end of file
diff --git a/src/marlin.web/Models/LogEventRecord.cs b/src/marlin.web/Models/LogEventRecord.cs
new file mode 100644
index 0000000..1a916d8
--- /dev/null
+++ b/src/marlin.web/Models/LogEventRecord.cs
@@ -0,0 +1,16 @@
+namespace marlin.web.Models;
+
+using core.models;
+
+public class LogEventRecord
+{
+ public Guid Id { get; set; }
+
+ public Guid JobId { get; set; }
+ public Job? Job { get; set; }
+
+ public DateTime At { get; set; }
+ public string Message { get; set; }
+ public bool Error { get; set; }
+ public string StepId { get; set; }
+} \ No newline at end of file
diff --git a/src/marlin.web/Program.cs b/src/marlin.web/Program.cs
index 700e696..8dea3fe 100644
--- a/src/marlin.web/Program.cs
+++ b/src/marlin.web/Program.cs
@@ -1,12 +1,16 @@
+using marlin.web.Controllers.Api;
using marlin.web.Data;
using marlin.web.Models;
using marlin.web.Services;
using Microsoft.EntityFrameworkCore;
+using ProtoBuf.Grpc.Server;
DotNetEnv.Env.NoClobber().TraversePath().Load();
var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddCodeFirstGrpc();
+
builder.Services.AddDbContext<ApplicationDbContext>(options => options
.UseNpgsql(builder.Configuration.GetConnectionString("Master"))
.UseSnakeCaseNamingConvention());
@@ -20,7 +24,7 @@ builder.Services
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Agent", p => p.RequireClaim("agent_id"));
- options.AddPolicy("JobScoped", p => p.RequireClaim("job_id"));
+ options.AddPolicy("JobScoped", p => p.RequireClaim("agent_id").RequireClaim("job_id"));
});
builder.Services.AddHostedService<DatabaseUpgrader>();
@@ -49,6 +53,8 @@ app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
+app.MapGrpcService<InstanceGrpcService>();
+
app.MapStaticAssets();
app
diff --git a/src/marlin.web/Services/VersionControlSyncService.cs b/src/marlin.web/Services/VersionControlSyncService.cs
index 8f2046e..4509745 100644
--- a/src/marlin.web/Services/VersionControlSyncService.cs
+++ b/src/marlin.web/Services/VersionControlSyncService.cs
@@ -115,7 +115,7 @@ public class VersionControlSyncService : BackgroundService
private async Task<string> GetLocalPath(IGitClient client, VersionControlSource source)
{
- var dir = Path.Join(Environment.CurrentDirectory, "run", "sources", source.Id.ToString("N"));
+ var dir = Path.Join(CommonPaths.WebRoot, "sources", source.Id.ToString("N"));
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
diff --git a/src/marlin.web/Views/Jobs/Details.cshtml b/src/marlin.web/Views/Jobs/Details.cshtml
index ebba94d..9b27fe3 100644
--- a/src/marlin.web/Views/Jobs/Details.cshtml
+++ b/src/marlin.web/Views/Jobs/Details.cshtml
@@ -2,4 +2,27 @@
@Model.Id
-<a asp-action="List">Back</a> \ No newline at end of file
+@Model.Branch.Name
+
+<dl>
+ <dt>Id</dt>
+ <dd>@Model.Id</dd>
+
+ <dt>Branch</dt>
+ <dd>@Model.Branch.Name</dd>
+
+ <dt>Commit</dt>
+ <dd>@Model.Commit.Hash</dd>
+
+ <dt>Status</dt>
+ <dd>@Model.Status</dd>
+</dl>
+
+<ul>
+ @foreach (var line in Model.Logs.OrderBy(x=>x.At))
+ {
+ <li style="color: @(line.Error ? "red" : "black")">[@line.At.ToString("HH:mm:ss")] [@line.StepId] @line.Message</li>
+ }
+</ul>
+
+<a asp-action="List">Back</a>
diff --git a/src/marlin.web/marlin.web.csproj b/src/marlin.web/marlin.web.csproj
index 26c8ea3..b931039 100644
--- a/src/marlin.web/marlin.web.csproj
+++ b/src/marlin.web/marlin.web.csproj
@@ -15,6 +15,8 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
+ <PackageReference Include="protobuf-net.Grpc" Version="1.3.14" />
+ <PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.3.14" />
</ItemGroup>
<ItemGroup>