Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9febc2b22f | |||
| 35eaef9dee | |||
| fc356012e6 | |||
| e90835b51e | |||
| 4b38f7c164 | |||
| 63e0300788 | |||
| 1badff1437 |
@@ -13,10 +13,22 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
- name: Setup .NET (resilient)
|
||||
shell: bash
|
||||
# actions/setup-dotnet on this single self-hosted runner intermittently
|
||||
# leaves a partial extraction in the shared tool-cache ("tar: Cannot open:
|
||||
# File exists") or corrupts the SDK download. Install into a clean private
|
||||
# dir via dotnet-install.sh and retry once on failure, mirroring the
|
||||
# npm ci / NuGet retries elsewhere in this workflow.
|
||||
run: |
|
||||
install() {
|
||||
curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
|
||||
rm -rf "$HOME/.dotnet"
|
||||
bash /tmp/dotnet-install.sh --channel 9.0 --install-dir "$HOME/.dotnet"
|
||||
}
|
||||
install || ( echo "dotnet install failed ($?) — retrying once..." && install )
|
||||
echo "$HOME/.dotnet" >> "$GITHUB_PATH"
|
||||
"$HOME/.dotnet/dotnet" --info
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -39,7 +51,12 @@ jobs:
|
||||
run: |
|
||||
node -v
|
||||
npm -v
|
||||
npm ci --no-audit --no-fund
|
||||
# npm ci occasionally segfaults on the runner (SIGSEGV/139, a memory/native
|
||||
# flake). Retry once with a clean node_modules before failing the job.
|
||||
npm ci --no-audit --no-fund \
|
||||
|| ( echo "npm ci failed ($?) — cleaning node_modules and retrying once..." \
|
||||
&& rm -rf node_modules \
|
||||
&& npm ci --no-audit --no-fund )
|
||||
|
||||
- name: Test frontend
|
||||
working-directory: job-tracker-ui
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using JobTrackerApi.Services.EmailProviders;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -18,15 +19,24 @@ public sealed class GmailController : ControllerBase
|
||||
private readonly IGmailJobMatchingService _matching;
|
||||
private readonly JobTrackerContext _db;
|
||||
private readonly IConfiguration _cfg;
|
||||
private readonly IEmailProviderRegistry _providers;
|
||||
|
||||
public GmailController(IGmailOAuthService gmail, IGmailJobMatchingService matching, JobTrackerContext db, IConfiguration cfg)
|
||||
public GmailController(IGmailOAuthService gmail, IGmailJobMatchingService matching, JobTrackerContext db, IConfiguration cfg, IEmailProviderRegistry? providers = null)
|
||||
{
|
||||
_gmail = gmail;
|
||||
_matching = matching;
|
||||
_db = db;
|
||||
_cfg = cfg;
|
||||
// Fall back to a single-Gmail registry so direct construction (tests) keeps working.
|
||||
_providers = providers ?? new EmailProviderRegistry(new IEmailProvider[] { new GmailProvider(gmail) });
|
||||
}
|
||||
|
||||
// The email provider backing this controller's read paths (search + thread listing),
|
||||
// via the provider-neutral seam. OAuth and Gmail-specific candidate ranking still use
|
||||
// IGmailOAuthService directly until they are generalised.
|
||||
private IEmailProvider Email => _providers.Get("gmail")
|
||||
?? throw new InvalidOperationException("Gmail email provider is not registered.");
|
||||
|
||||
public sealed record GmailImportResultDto(int Imported, int Skipped, string? ThreadId);
|
||||
public sealed record GmailImportMessageResultDto(int Imported, int Skipped, string MessageId, string? ThreadId, Correspondence? Message);
|
||||
public sealed record ImportGmailMessageRequest(int JobApplicationId, string MessageId);
|
||||
@@ -383,7 +393,7 @@ public sealed class GmailController : ControllerBase
|
||||
.FirstOrDefaultAsync(x => x.Id == request.JobApplicationId.Value, cancellationToken);
|
||||
if (job is null) return NotFound("Job application not found.");
|
||||
|
||||
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
||||
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
||||
var distinctMessageIds = threadMessages
|
||||
.Where(message => !string.IsNullOrWhiteSpace(message.Id))
|
||||
.Select(message => message.Id)
|
||||
@@ -639,7 +649,7 @@ public sealed class GmailController : ControllerBase
|
||||
_db.JobApplications.Add(job);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
||||
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
||||
var distinctMessageIds = threadMessages.Select(message => message.Id).Where(static id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
||||
// Batch the "already imported?" check with a single query instead of one
|
||||
// AnyAsync per message (N+1), mirroring RelinkThread below.
|
||||
@@ -695,7 +705,7 @@ public sealed class GmailController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
||||
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
||||
var distinctMessageIds = threadMessages.Select(message => message.Id).Where(static id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
||||
var existingMessageIds = await _db.Correspondences
|
||||
.Where(message => message.JobApplicationId == job.Id && message.ExternalMessageId != null && distinctMessageIds.Contains(message.ExternalMessageId))
|
||||
@@ -793,7 +803,7 @@ public sealed class GmailController : ControllerBase
|
||||
public async Task<IActionResult> Messages([FromQuery] string? query, [FromQuery] int maxResults = 12, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ownerUserId = GetRequiredOwnerUserId();
|
||||
var items = await _gmail.ListMessagesAsync(ownerUserId, query, maxResults, cancellationToken);
|
||||
var items = await Email.SearchAsync(ownerUserId, query, maxResults, cancellationToken);
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
@@ -897,7 +907,7 @@ public sealed class GmailController : ControllerBase
|
||||
|
||||
foreach (var threadId in linkedThreadIds)
|
||||
{
|
||||
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
||||
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
||||
var distinctThreadMessages = threadMessages
|
||||
.Where(message => !string.IsNullOrWhiteSpace(message.Id))
|
||||
.GroupBy(message => message.Id, StringComparer.Ordinal)
|
||||
|
||||
@@ -9,7 +9,14 @@ COPY Models/ Models/
|
||||
COPY JobTrackerApi/ JobTrackerApi/
|
||||
COPY JobTrackerBackend/ JobTrackerBackend/
|
||||
|
||||
RUN dotnet publish JobTrackerApi/JobTrackerApi.csproj -c Release -o /app/publish /p:UseAppHost=false
|
||||
# Retry once after clearing NuGet caches. Transient download corruption on the
|
||||
# build host can trip NU3008 ("package integrity check failed / has changed since
|
||||
# it was signed") while restoring a transitive package; clearing the caches and
|
||||
# re-downloading resolves it.
|
||||
RUN dotnet publish JobTrackerApi/JobTrackerApi.csproj -c Release -o /app/publish /p:UseAppHost=false \
|
||||
|| ( echo "Publish failed — clearing NuGet caches and retrying once..." \
|
||||
&& dotnet nuget locals all --clear \
|
||||
&& dotnet publish JobTrackerApi/JobTrackerApi.csproj -c Release -o /app/publish /p:UseAppHost=false )
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||
|
||||
|
||||
Reference in New Issue
Block a user