Skip to content

Commit 43b1b84

Browse files
authored
CI sync (#139)
* add Helldivers-2-CI * write parsed files to disk * make sync run daily * fix deprecated actions * update CI workflow
1 parent 8cbe3d3 commit 43b1b84

File tree

8 files changed

+221
-2
lines changed

8 files changed

+221
-2
lines changed

.github/workflows/sync.yml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Validate sync
2+
3+
permissions:
4+
contents: read
5+
pull-requests: write
6+
7+
on:
8+
push:
9+
branches: ["master"]
10+
pull_request:
11+
branches: ["master"]
12+
schedule:
13+
- cron: '0 0 * * *'
14+
15+
jobs:
16+
validate-sync:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup .NET
23+
uses: actions/setup-dotnet@v4
24+
with:
25+
dotnet-version: '9.0.x'
26+
27+
- name: Download JSON submodule
28+
run: git submodule update --init ./src/Helldivers-2-Models/json
29+
30+
- name: Run sync and capture logs
31+
id: run_sync
32+
shell: bash
33+
continue-on-error: true
34+
run: |
35+
set -o pipefail
36+
dotnet build
37+
dotnet run --project ./src/Helldivers-2-CI/Helldivers-2-CI.csproj 2>&1 | tee sync.log
38+
39+
- name: Upload artifacts
40+
if: always()
41+
uses: actions/upload-artifact@v4
42+
with:
43+
name: sync-artifacts
44+
path: |
45+
v1/*.json
46+
v2/*.json
47+
sync.log
48+
49+
- name: Capture error log
50+
id: sync_log
51+
if: ${{ steps.run_sync.outcome == 'failure' && github.event_name == 'pull_request' }}
52+
run: |
53+
# open a multi-line output called "log"
54+
echo "log<<EOF" >> $GITHUB_OUTPUT
55+
cat sync.log >> $GITHUB_OUTPUT
56+
echo "EOF" >> $GITHUB_OUTPUT
57+
58+
- name: Comment on PR on failure
59+
if: ${{ steps.run_sync.outcome == 'failure' && github.event_name == 'pull_request' }}
60+
uses: peter-evans/create-or-update-comment@v4
61+
with:
62+
token: ${{ secrets.GITHUB_TOKEN }}
63+
issue-number: ${{ github.event.pull_request.number }}
64+
body: |
65+
⚠️ **Sync validation failed** (run #${{ github.run_number }} exited with ${{ steps.run_sync.outcome }})
66+
67+
<details>
68+
<summary>Error log</summary>
69+
70+
```text
71+
${{ steps.sync_log.outputs.log }}
72+
```
73+
</details>
74+
75+
**Artifacts** (JSON + log) here:
76+
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
77+
78+
- name: Fail job on error
79+
if: ${{ steps.run_sync.outcome == 'failure' }}
80+
run: exit 1

Helldivers-2.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{
4141
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
4242
.github\workflows\fly.yml = .github\workflows\fly.yml
4343
.github\workflows\pages.yml = .github\workflows\pages.yml
44+
.github\workflows\sync.yml = .github\workflows\sync.yml
4445
EndProjectSection
4546
EndProject
4647
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Templates", "Templates", "{FD5B9284-4689-48BE-B520-633C7269F97C}"
@@ -56,6 +57,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{1A2C
5657
docs\openapi\Helldivers-2-API_arrowhead.json = docs\openapi\Helldivers-2-API_arrowhead.json
5758
EndProjectSection
5859
EndProject
60+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Helldivers-2-CI", "src\Helldivers-2-CI\Helldivers-2-CI.csproj", "{DD942DB1-162A-4915-B10E-C049ED478297}"
61+
EndProject
5962
Global
6063
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6164
Debug|Any CPU = Debug|Any CPU
@@ -82,6 +85,10 @@ Global
8285
{32AE19FB-7D9E-4AC7-A0A5-80DED3680369}.Debug|Any CPU.Build.0 = Debug|Any CPU
8386
{32AE19FB-7D9E-4AC7-A0A5-80DED3680369}.Release|Any CPU.ActiveCfg = Release|Any CPU
8487
{32AE19FB-7D9E-4AC7-A0A5-80DED3680369}.Release|Any CPU.Build.0 = Release|Any CPU
88+
{DD942DB1-162A-4915-B10E-C049ED478297}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
89+
{DD942DB1-162A-4915-B10E-C049ED478297}.Debug|Any CPU.Build.0 = Debug|Any CPU
90+
{DD942DB1-162A-4915-B10E-C049ED478297}.Release|Any CPU.ActiveCfg = Release|Any CPU
91+
{DD942DB1-162A-4915-B10E-C049ED478297}.Release|Any CPU.Build.0 = Release|Any CPU
8592
EndGlobalSection
8693
GlobalSection(SolutionProperties) = preSolution
8794
HideSolutionNode = FALSE
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<RootNamespace>Helldivers_2_CI</RootNamespace>
7+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
12+
<ProjectReference Include="..\Helldivers-2-Sync\Helldivers-2-Sync.csproj" />
13+
</ItemGroup>
14+
15+
</Project>

src/Helldivers-2-CI/Program.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using Helldivers.Core.Contracts.Collections;
2+
using Helldivers.Core.Extensions;
3+
using Helldivers.Models.Domain.Localization;
4+
using Helldivers.Models.V1;
5+
using Helldivers.Models.V2;
6+
using Helldivers.Sync.Configuration;
7+
using Helldivers.Sync.Extensions;
8+
using Helldivers.Sync.Hosted;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Hosting;
11+
using Microsoft.Extensions.Logging;
12+
using System.Diagnostics;
13+
using System.Globalization;
14+
using System.Text.Json;
15+
using Dispatch = Helldivers.Models.V1.Dispatch;
16+
17+
var maxRuntime = new CancellationTokenSource(TimeSpan.FromSeconds(30));
18+
var builder = new HostApplicationBuilder(args);
19+
20+
LocalizedMessage.FallbackCulture = new CultureInfo("en-US");
21+
builder.Services.AddHelldivers();
22+
builder.Services.AddHelldiversSync();
23+
builder.Services.Configure<HelldiversSyncConfiguration>(configuration =>
24+
{
25+
configuration.RunOnce = true;
26+
// Add all languages we want to CI test here.
27+
configuration.Languages =
28+
[
29+
"en-US",
30+
"de-DE",
31+
"es-ES",
32+
"ru-RU",
33+
"fr-FR",
34+
"it-IT",
35+
"pl-PL",
36+
"zh-Hans",
37+
"zh-Hant"
38+
];
39+
});
40+
41+
var stopwatch = new Stopwatch();
42+
var app = builder.Build();
43+
44+
// Run our host, but make it shutdown after *max* 30 seconds.
45+
stopwatch.Start();
46+
var arrowhead = app.Services.GetRequiredService<ArrowHeadSyncService>();
47+
var steam = app.Services.GetRequiredService<SteamSyncService>();
48+
await arrowhead.StartAsync(maxRuntime.Token);
49+
await steam.StartAsync(maxRuntime.Token);
50+
51+
// now we await completion of both.
52+
await Task.WhenAll([
53+
arrowhead.ExecuteTask!,
54+
steam.ExecuteTask!,
55+
]).WaitAsync(maxRuntime.Token);
56+
stopwatch.Stop();
57+
58+
app.Services.GetRequiredService<ILogger<Program>>().LogInformation("Sync succeeded in {}", stopwatch.Elapsed);
59+
60+
// Fetch all information from the stores to write to disk.
61+
var assignments = await app.Services.GetRequiredService<IStore<Assignment, long>>().AllAsync(maxRuntime.Token);
62+
var campaigns = await app.Services.GetRequiredService<IStore<Campaign, int>>().AllAsync(maxRuntime.Token);
63+
var dispatchesv1 = await app.Services.GetRequiredService<IStore<Dispatch, int>>().AllAsync(maxRuntime.Token);
64+
var planets = await app.Services.GetRequiredService<IStore<Planet, int>>().AllAsync(maxRuntime.Token);
65+
var war = await app.Services.GetRequiredService<Helldivers.Core.Contracts.IStore<War>>().Get(maxRuntime.Token);
66+
var dispatchesv2 = await app.Services.GetRequiredService<IStore<Helldivers.Models.V2.Dispatch, int>>().AllAsync(maxRuntime.Token);
67+
var store = await app.Services.GetRequiredService<IStore<SpaceStation, long>>().AllAsync(maxRuntime.Token);
68+
69+
Directory.CreateDirectory("v1");
70+
Directory.CreateDirectory("v2");
71+
72+
var options = new JsonSerializerOptions { WriteIndented = true };
73+
await File.WriteAllBytesAsync("v1/assignments.json", JsonSerializer.SerializeToUtf8Bytes(assignments, options), maxRuntime.Token);
74+
await File.WriteAllBytesAsync("v1/campaigns.json", JsonSerializer.SerializeToUtf8Bytes(campaigns, options), maxRuntime.Token);
75+
await File.WriteAllBytesAsync("v1/dispatches.json", JsonSerializer.SerializeToUtf8Bytes(dispatchesv1, options), maxRuntime.Token);
76+
await File.WriteAllBytesAsync("v1/planets.json", JsonSerializer.SerializeToUtf8Bytes(planets, options), maxRuntime.Token);
77+
await File.WriteAllBytesAsync("v1/war.json", JsonSerializer.SerializeToUtf8Bytes(war, options), maxRuntime.Token);
78+
await File.WriteAllBytesAsync("v2/dispatches.json", JsonSerializer.SerializeToUtf8Bytes(dispatchesv2, options), maxRuntime.Token);
79+
await File.WriteAllBytesAsync("v2/space-stations.json", JsonSerializer.SerializeToUtf8Bytes(store, options), maxRuntime.Token);
80+
81+
return 0;

src/Helldivers-2-SourceGen/Parsers/BaseJsonParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public SourceText Parse(AdditionalText file, CancellationToken cancellationToken
4040
return SourceText.From(output, Encoding.UTF8);
4141
}
4242

43-
return SourceText.From("// Could not read JSON file");
43+
return SourceText.From("// Could not read JSON file", Encoding.UTF8);
4444
}
4545

4646
/// <summary>

src/Helldivers-2-Sync/Configuration/HelldiversSyncConfiguration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@ public sealed class HelldiversSyncConfiguration
2424
/// A list of language codes for which translations will be provided.
2525
/// </summary>
2626
public List<string> Languages { get; set; } = new(0);
27+
28+
/// <summary>
29+
/// Flag to indicate if the application should only run the sync once.
30+
/// This is used in CI testing to validate sync works.
31+
/// </summary>
32+
public bool RunOnce { get; set; } = false;
2733
}

src/Helldivers-2-Sync/Hosted/ArrowHeadSyncService.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,15 @@ StorageFacade storage
4545
private static partial void LogFailedToLoadTranslation(ILogger logger, Exception exception, string language,
4646
string type);
4747

48+
[LoggerMessage(Level = LogLevel.Information, Message = "ArrowHeadSyncService finished processing, shutting down.")]
49+
private static partial void LogRunOnceCompleted(ILogger logger);
50+
4851
#endregion
4952

5053
/// <inheritdoc cref="BackgroundService.ExecuteAsync(CancellationToken)" />
5154
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
5255
{
56+
5357
var delay = TimeSpan.FromSeconds(configuration.Value.IntervalSeconds);
5458

5559
LogRunAtInterval(logger, delay);
@@ -66,6 +70,16 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
6670
catch (Exception exception)
6771
{
6872
LogSyncThrewAnError(logger, exception);
73+
74+
if (configuration.Value.RunOnce)
75+
throw;
76+
}
77+
78+
// If we should only run once, we exit the loop (and thus service) after this.
79+
if (configuration.Value.RunOnce)
80+
{
81+
LogRunOnceCompleted(logger);
82+
return;
6983
}
7084

7185
await Task.Delay(delay, cancellationToken);

src/Helldivers-2-Sync/Hosted/SteamSyncService.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using Helldivers.Core;
2+
using Helldivers.Sync.Configuration;
23
using Helldivers.Sync.Services;
34
using Microsoft.Extensions.DependencyInjection;
45
using Microsoft.Extensions.Hosting;
56
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Options;
68
using Prometheus;
79

810
namespace Helldivers.Sync.Hosted;
@@ -13,7 +15,8 @@ namespace Helldivers.Sync.Hosted;
1315
public sealed partial class SteamSyncService(
1416
ILogger<SteamSyncService> logger,
1517
StorageFacade storage,
16-
IServiceScopeFactory scopeFactory
18+
IServiceScopeFactory scopeFactory,
19+
IOptions<HelldiversSyncConfiguration> configuration
1720
) : BackgroundService
1821
{
1922
/// <summary>
@@ -29,6 +32,9 @@ IServiceScopeFactory scopeFactory
2932
[LoggerMessage(Level = LogLevel.Error, Message = "An exception was thrown when synchronizing from Steam API")]
3033
private static partial void LogSyncThrewAnError(ILogger logger, Exception exception);
3134

35+
[LoggerMessage(Level = LogLevel.Information, Message = "SteamSyncService finished processing, shutting down.")]
36+
private static partial void LogRunOnceCompleted(ILogger logger);
37+
3238
#endregion
3339

3440
/// <inheritdoc />
@@ -52,8 +58,18 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
5258
catch (Exception exception)
5359
{
5460
LogSyncThrewAnError(logger, exception);
61+
62+
if (configuration.Value.RunOnce)
63+
throw;
5564
}
5665

66+
67+
// If we should only run once, we exit the loop (and thus service) after this.
68+
if (configuration.Value.RunOnce)
69+
{
70+
LogRunOnceCompleted(logger);
71+
return;
72+
}
5773
await Task.Delay(delay, cancellationToken);
5874
}
5975
}

0 commit comments

Comments
 (0)