Compare commits

..

4 Commits

Author SHA1 Message Date
cesnimda fc356012e6 ci: install .NET via retrying dotnet-install.sh instead of setup-dotnet
CI and Deploy / test (pull_request) Successful in 2m5s
CI and Deploy / deploy (pull_request) Has been skipped
The single self-hosted act_runner intermittently fails actions/setup-dotnet:
a partial extraction sticks in the shared tool-cache (tar: Cannot open: File
exists) or the SDK tarball download corrupts. Install into a clean private
$HOME/.dotnet via dotnet-install.sh with a rm -rf + retry-once, matching the
npm ci and NuGet publish retries already in this pipeline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 13:02:37 +02:00
cesnimda e90835b51e Merge pull request 'fix(deploy): retry publish after clearing NuGet caches on NU3008' (#5) from fix/deploy-nuget-integrity into main
CI and Deploy / test (push) Successful in 2m10s
CI and Deploy / deploy (push) Successful in 2m46s
2026-07-06 01:12:06 +02:00
cesnimda 4b38f7c164 ci: retry npm ci once on the runner's intermittent SIGSEGV
CI and Deploy / test (pull_request) Successful in 2m12s
CI and Deploy / deploy (pull_request) Has been skipped
The frontend deps step occasionally crashes with "Segmentation fault (core
dumped)" (exit 139) during `npm ci` — a memory/native flake on the act_runner,
unrelated to the change under test (it failed the deploy-fix PR whose only change
is the Dockerfile). Retry once with a clean node_modules before failing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 01:08:00 +02:00
cesnimda 63e0300788 fix(deploy): retry publish after clearing NuGet caches on NU3008
CI and Deploy / test (pull_request) Failing after 2m9s
CI and Deploy / deploy (pull_request) Has been skipped
The prod deploy failed restoring a transitive package
(Microsoft.CodeAnalysis.Workspaces.Common) with NU3008 "package integrity check
failed / has changed since it was signed" — a transient corrupted download on the
build host, not a code change. Wrap the backend `dotnet publish` so that on any
failure it clears all NuGet caches and retries once, re-downloading the package
fresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:51:29 +02:00
3 changed files with 36 additions and 22 deletions
+22 -5
View File
@@ -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
+6 -16
View File
@@ -3,7 +3,6 @@ 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;
@@ -19,24 +18,15 @@ 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, IEmailProviderRegistry? providers = null)
public GmailController(IGmailOAuthService gmail, IGmailJobMatchingService matching, JobTrackerContext db, IConfiguration cfg)
{
_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);
@@ -393,7 +383,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 Email.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
var distinctMessageIds = threadMessages
.Where(message => !string.IsNullOrWhiteSpace(message.Id))
.Select(message => message.Id)
@@ -649,7 +639,7 @@ public sealed class GmailController : ControllerBase
_db.JobApplications.Add(job);
await _db.SaveChangesAsync(cancellationToken);
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
var threadMessages = await _gmail.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.
@@ -705,7 +695,7 @@ public sealed class GmailController : ControllerBase
}
}
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
var threadMessages = await _gmail.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))
@@ -803,7 +793,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 Email.SearchAsync(ownerUserId, query, maxResults, cancellationToken);
var items = await _gmail.ListMessagesAsync(ownerUserId, query, maxResults, cancellationToken);
return Ok(items);
}
@@ -907,7 +897,7 @@ public sealed class GmailController : ControllerBase
foreach (var threadId in linkedThreadIds)
{
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
var distinctThreadMessages = threadMessages
.Where(message => !string.IsNullOrWhiteSpace(message.Id))
.GroupBy(message => message.Id, StringComparer.Ordinal)
+8 -1
View File
@@ -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