commit ae8680d275cfe05247e17776a182bad946dcc73a Author: Mohammad Mahdi Date: Tue Nov 3 15:02:50 2026 +0330 `feat: load application and repository configuration from env and TOML` diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88743a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ + +.env +config.toml diff --git a/.idea/.idea.ForgejoReleaseSync/.idea/.gitignore b/.idea/.idea.ForgejoReleaseSync/.idea/.gitignore new file mode 100644 index 0000000..e8260f8 --- /dev/null +++ b/.idea/.idea.ForgejoReleaseSync/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Rider ignored files +/modules.xml +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.ForgejoReleaseSync.iml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.ForgejoReleaseSync/.idea/encodings.xml b/.idea/.idea.ForgejoReleaseSync/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.ForgejoReleaseSync/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.ForgejoReleaseSync/.idea/indexLayout.xml b/.idea/.idea.ForgejoReleaseSync/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.ForgejoReleaseSync/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.ForgejoReleaseSync/.idea/misc.xml b/.idea/.idea.ForgejoReleaseSync/.idea/misc.xml new file mode 100644 index 0000000..294b651 --- /dev/null +++ b/.idea/.idea.ForgejoReleaseSync/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.ForgejoReleaseSync/.idea/vcs.xml b/.idea/.idea.ForgejoReleaseSync/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.ForgejoReleaseSync/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4abfef1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base +USER $APP_UID +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["ForgejoReleaseSync.csproj", "./"] +RUN dotnet restore "ForgejoReleaseSync.csproj" +COPY . . +WORKDIR "/src/" +RUN dotnet build "./ForgejoReleaseSync.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./ForgejoReleaseSync.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ForgejoReleaseSync.dll"] diff --git a/ForgejoReleaseSync.csproj b/ForgejoReleaseSync.csproj new file mode 100644 index 0000000..43ab138 --- /dev/null +++ b/ForgejoReleaseSync.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + true + true + Linux + + + + + + + + + diff --git a/ForgejoReleaseSync.sln b/ForgejoReleaseSync.sln new file mode 100644 index 0000000..0b89c0b --- /dev/null +++ b/ForgejoReleaseSync.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ForgejoReleaseSync", "ForgejoReleaseSync.csproj", "{268E3A9E-52D0-4B81-A677-277555C6D1F8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0E6F637D-3E7A-4DD3-8B01-1C7BFB36586A}" + ProjectSection(SolutionItems) = preProject + compose.yaml = compose.yaml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {268E3A9E-52D0-4B81-A677-277555C6D1F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268E3A9E-52D0-4B81-A677-277555C6D1F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268E3A9E-52D0-4B81-A677-277555C6D1F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268E3A9E-52D0-4B81-A677-277555C6D1F8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..ab0b632 --- /dev/null +++ b/Program.cs @@ -0,0 +1,15 @@ +using ForgejoReleaseSync.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = Host.CreateApplicationBuilder(args); + +var config = AppConfigLoader.LoadConfigFromEnv(); +builder.Services.AddSingleton(config); + +var repositoryConfig = await RepoConfigLoader.LoadConfigAsync( + config.RepositoryConfigFilePath); +builder.Services.AddSingleton(repositoryConfig); + +var app = builder.Build(); +await app.RunAsync(); \ No newline at end of file diff --git a/Services/AppConfig.cs b/Services/AppConfig.cs new file mode 100644 index 0000000..fda9e9c --- /dev/null +++ b/Services/AppConfig.cs @@ -0,0 +1,52 @@ +using DotNetEnv; + +namespace ForgejoReleaseSync.Services; + +public class AppConfig +{ + public required string ForgejoInstacne { get; set; } + public required string ForgejoToken { get; set; } + public required ushort SyncConcurrency { get; set; } + public required string RepositoryConfigFilePath { get; set; } +} + + +public static class AppConfigLoader +{ + public static AppConfig LoadConfigFromEnv() + { + Env.Load(); + + var forgejoInstance = Environment.GetEnvironmentVariable("FORGEJO_INSTANCE"); + var forgejoToken = Environment.GetEnvironmentVariable("FORGEJO_TOKEN"); + var syncConcurrency = Environment.GetEnvironmentVariable("SYNC_CONCURRENCY"); + var repoConfigFilePath = Environment.GetEnvironmentVariable("REPO_CONFIG_FILE"); + + if (string.IsNullOrEmpty(forgejoInstance)) + throw new Exception("FORGEJO_INSTANCE environment variable is required"); + if (!string.IsNullOrEmpty(forgejoToken) && forgejoInstance.EndsWith('/')) + forgejoInstance = forgejoInstance[..^1]; + + if (string.IsNullOrEmpty(forgejoToken)) + throw new Exception("FORGEJO_TOKEN environment variable is required"); + + ushort syncConcurrencyParsed = 3; + if (!string.IsNullOrEmpty(syncConcurrency)) + ushort.TryParse(syncConcurrency, out syncConcurrencyParsed); + if (syncConcurrencyParsed > 10) + throw new ArgumentOutOfRangeException(nameof(syncConcurrencyParsed), + "Sync concurrency cannot be greater than 10"); + + if (string.IsNullOrEmpty(repoConfigFilePath)) + throw new Exception("REPO_CONFIG_FILE environment variable is required"); + + + return new AppConfig + { + ForgejoInstacne = forgejoInstance, + ForgejoToken = forgejoToken, + SyncConcurrency = syncConcurrencyParsed, + RepositoryConfigFilePath = repoConfigFilePath + }; + } +} \ No newline at end of file diff --git a/Services/ForgejoService.cs b/Services/ForgejoService.cs new file mode 100644 index 0000000..00e0723 --- /dev/null +++ b/Services/ForgejoService.cs @@ -0,0 +1,6 @@ +namespace ForgejoReleaseSync.Services; + +public class ForgejoService +{ + +} \ No newline at end of file diff --git a/Services/GithubService.cs b/Services/GithubService.cs new file mode 100644 index 0000000..4c81531 --- /dev/null +++ b/Services/GithubService.cs @@ -0,0 +1,6 @@ +namespace ForgejoReleaseSync.Services; + +public class GithubService +{ + +} \ No newline at end of file diff --git a/Services/RepositoryConfig.cs b/Services/RepositoryConfig.cs new file mode 100644 index 0000000..a42500b --- /dev/null +++ b/Services/RepositoryConfig.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RStein.TOML; + +namespace ForgejoReleaseSync.Services; + +public class RepositoryConfig +{ + public List Repositories { get; set; } = []; +} + +public class RepoSyncConfig +{ + public required string Name { get; set; } + public required string GithubUrl { get; set; } + public required string ForgejoUrl { get; set; } + public required string IncludeRegex { get; set; } + public required string ExcludeRegex { get; set; } +} + +public abstract class RepoConfigLoader() +{ + public static async Task LoadConfigAsync(string configPath,CancellationToken token = default) + { + try + { + var toml = await File.ReadAllTextAsync(configPath, token); + + var tables = await TomlSerializer.DeserializeAsync(toml, token); + + var result = new RepositoryConfig(); + + foreach (var table in tables) + { + if (table.Value is not TomlArray tableValues) + continue; + + foreach (var content in tableValues) + { + if (content is not TomlTable fields) + continue; + + result.Repositories.Add(new RepoSyncConfig + { + Name = table.Key.ToString(), + GithubUrl = GetRequiredString(fields, "GithubUrl"), + ForgejoUrl = GetRequiredString(fields, "ForgejoUrl"), + IncludeRegex = GetRequiredString(fields, "IncludeRegex"), + ExcludeRegex = GetRequiredString(fields, "ExcludeRegex") + }); + } + } + return result; + } + catch (Exception e) + { + throw new InvalidDataException("Failed to load configuration file", e); + } + + } + + private static string GetRequiredString( + TomlTable table, + string key) + { + return (string?)table[key] + ?? throw new InvalidDataException( + $"Missing required configuration property: {key}"); + } +} \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..2f429ee --- /dev/null +++ b/compose.yaml @@ -0,0 +1,7 @@ +services: + forgejoreleasesync: + image: forgejoreleasesync + build: + context: . + dockerfile: Dockerfile + diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..f88f487 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,11 @@ +[[repo1]] +GithubUrl = "https://github.com/owner/repo1" +ForgejoUrl = "https://forgejo.example.com/owner/repo1" +IncludeRegex = ".*" +ExcludeRegex = "^$" + +[[repo2]] +GithubUrl = "https://github.com/owner/repo2" +ForgejoUrl = "https://forgejo.example.com/owner/repo2" +IncludeRegex = "^v.*" +ExcludeRegex = ".*-beta.*" \ No newline at end of file