Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/buildpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Nastavení .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.0.x'
dotnet-version: '10.0.x'

- name: Obnova závislostí (Restore)
run: dotnet restore
Expand Down
83 changes: 42 additions & 41 deletions IoTDeploy/GithubProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,23 @@ public record WorkflowInfo(long Id, string Name, string Path);
public class GithubProvider
{
private static readonly ILogger Logger = Log.ForContext<GithubProvider>();

private GitHubClient gitHubClient;
private string _appId;
private long _installationId;
private string _owner;
private int _workflowTimeoutMinutes;
private string _pemKeyPath;
private readonly AppSettings settings;
private GitHubClient? gitHubClient;
private AccessToken? _installationAccessToken;
private string? _pemKeyPath;

private GitHubClient Client =>
gitHubClient ?? throw new InvalidOperationException("GitHub klient není inicializován. Nejprve zavolejte Init().");

public GithubProvider()
public GithubProvider(AppSettings settings)
{
this.settings = settings;
}

public async Task Init(AppSettings settings)
public async Task Init()
{
_appId = settings.GitHub.AppId;
_installationId = settings.GitHub.InstallationId;
_owner = settings.GitHub.Owner;
_workflowTimeoutMinutes = settings.Runner.WorkflowTimeoutMinutes;
Logger.Information("Inicializuji GitHub App klienta (AppId={AppId}, Owner={Owner})", settings.GitHub.AppId, settings.GitHub.Owner);
_pemKeyPath = FindPemFile(settings.GitHub.PemFilePattern);
Logger.Information("Inicializuji GitHub App klienta (AppId={AppId}, Owner={Owner})", _appId, _owner);
gitHubClient = await CreateInstallationClient();
Logger.Information("GitHub klient úspěšně inicializován");
}
Expand All @@ -65,6 +61,10 @@ private static string FindPemFile(string pattern)
private async Task<GitHubClient> CreateInstallationClient()
{
string pemContent;
if (string.IsNullOrEmpty(_pemKeyPath))
{
throw new InvalidOperationException(Strings.PrivateKeyNotFound);
}
try
{
pemContent = File.ReadAllText(_pemKeyPath);
Expand All @@ -78,28 +78,28 @@ private async Task<GitHubClient> CreateInstallationClient()
{
using var rsa = RSA.Create();
rsa.ImportFromPem(pemContent);
var jwt = GenerateJwt(_appId, rsa);
var jwt = GenerateJwt(settings.GitHub.AppId, rsa);

var jwtClient = new GitHubClient(new ProductHeaderValue("IoTDeploy"))
{
Credentials = new Credentials(jwt, AuthenticationType.Bearer)
};

_installationAccessToken = await jwtClient.GitHubApps.CreateInstallationToken(_installationId);
_installationAccessToken = await jwtClient.GitHubApps.CreateInstallationToken(settings.GitHub.InstallationId);

return new GitHubClient(new ProductHeaderValue("IoTDeploy"))
{
Credentials = new Credentials(_installationAccessToken.Token)
};
}
catch (AuthorizationException)
{
throw new InvalidOperationException(Strings.AuthFailed);
}
catch (NotFoundException)
{
throw new InvalidOperationException(string.Format(Strings.InstallationNotFound, _installationId));
throw new InvalidOperationException(string.Format(Strings.InstallationNotFound, settings.GitHub.InstallationId));
}

return new GitHubClient(new ProductHeaderValue("IoTDeploy"))
{
Credentials = new Credentials(_installationAccessToken.Token)
};
}

private string GenerateJwt(string appId, RSA rsaKey)
Expand All @@ -123,7 +123,7 @@ private string GenerateJwt(string appId, RSA rsaKey)

private async Task EnsureValidTokenAsync()
{
if (_installationAccessToken == null ||
if (gitHubClient == null || _installationAccessToken == null ||
DateTimeOffset.UtcNow >= _installationAccessToken.ExpiresAt.AddMinutes(-5))
{
Logger.Debug("Obnovuji installation access token (vyprší {ExpiresAt})", _installationAccessToken?.ExpiresAt);
Expand All @@ -135,7 +135,7 @@ private async Task EnsureValidTokenAsync()
public async Task<IReadOnlyList<Repository>> GetRepositories()
{
await EnsureValidTokenAsync();
var result = await gitHubClient.GitHubApps.Installation.GetAllRepositoriesForCurrent();
var result = await Client.GitHubApps.Installation.GetAllRepositoriesForCurrent();
return result.Repositories;
}

Expand All @@ -144,7 +144,7 @@ public async Task<IReadOnlyList<Branch>> GetBranches(string repository)
await EnsureValidTokenAsync();
try
{
return await gitHubClient.Repository.Branch.GetAll(_owner, repository);
return (await Client.Repository.Branch.GetAll(settings.GitHub.Owner, repository));
}
catch (NotFoundException)
{
Expand All @@ -155,15 +155,15 @@ public async Task<IReadOnlyList<Branch>> GetBranches(string repository)
public async Task<IReadOnlyList<DeploymentEnvironment>> GetEnvironments(string repository)
{
await EnsureValidTokenAsync();
return (await gitHubClient.Repository.Environment.GetAll(_owner, repository)).Environments;
return (await Client.Repository.Environment.GetAll(settings.GitHub.Owner, repository)).Environments;
}

public async Task<IReadOnlyList<WorkflowInfo>> GetWorkflows(string repository)
{
await EnsureValidTokenAsync();
try
{
var response = await gitHubClient.Actions.Workflows.List(_owner, repository);
var response = await Client.Actions.Workflows.List(settings.GitHub.Owner, repository);
return response.Workflows
.Where(w => string.Equals(w.State.StringValue, "active", StringComparison.OrdinalIgnoreCase))
.Select(w => new WorkflowInfo(w.Id, w.Name, w.Path))
Expand All @@ -188,7 +188,7 @@ public async Task<long> RunWorkflow(string repository, string branchName, long w
Inputs = parameters.ToDictionary(kv => kv.Key, kv => (object)kv.Value)
};

await gitHubClient.Actions.Workflows.CreateDispatch(_owner, repository, workflowId, dispatch);
await Client.Actions.Workflows.CreateDispatch(settings.GitHub.Owner, repository, workflowId, dispatch);
}
catch (NotFoundException)
{
Expand All @@ -211,30 +211,31 @@ public async Task<long> RunWorkflow(string repository, string branchName, long w

private async Task<WorkflowRun> WaitForQueuedRunAsync(string repository, long workflowId, DateTimeOffset createdAfter, IProgress<string> progress, CancellationToken ct = default)
{
var deadline = DateTimeOffset.UtcNow.AddMinutes(_workflowTimeoutMinutes);
var deadline = DateTimeOffset.UtcNow.AddMinutes(settings.Runner.WorkflowTimeoutMinutes);
var attempt = 0;
Logger.Debug("Čekám na workflow run v repo={Repo}, workflow={WorkflowId} (timeout={Timeout}min)", repository, workflowId, _workflowTimeoutMinutes);
Logger.Debug("Čekám na workflow run v repo={Repo}, workflow={WorkflowId} (timeout={Timeout}min)", repository, workflowId, settings.Runner.WorkflowTimeoutMinutes);
while (DateTimeOffset.UtcNow < deadline)
{
await Task.Delay(3000, ct);
attempt++;
progress.Report(string.Format(Strings.WaitingForWorkflow, attempt * 3));
var runs = await gitHubClient.Actions.Workflows.Runs.ListByWorkflow(
_owner, repository, workflowId);
await EnsureValidTokenAsync();
var runs = await Client.Actions.Workflows.Runs.ListByWorkflow(
settings.GitHub.Owner, repository, workflowId);

var run = runs.WorkflowRuns.FirstOrDefault(r =>
r.CreatedAt >= createdAfter &&
r.Status.StringValue != "completed");
if (run != null) return run;
}
Logger.Warning("Timeout při čekání na workflow run (repo={Repo}, workflow={WorkflowId}, timeout={Timeout}min)", repository, workflowId, _workflowTimeoutMinutes);
throw new TimeoutException(string.Format(Strings.WorkflowTimeout, _workflowTimeoutMinutes));
Logger.Warning("Timeout při čekání na workflow run (repo={Repo}, workflow={WorkflowId}, timeout={Timeout}min)", repository, workflowId, settings.Runner.WorkflowTimeoutMinutes);
throw new TimeoutException(string.Format(Strings.WorkflowTimeout, settings.Runner.WorkflowTimeoutMinutes));
}

public async Task<IReadOnlyList<string>> GetQueuedJobLabelsAsync(string repository, long runId, CancellationToken ct = default)
{
await EnsureValidTokenAsync();
var jobs = await gitHubClient.Actions.Workflows.Jobs.List(_owner, repository, runId);
var jobs = await Client.Actions.Workflows.Jobs.List(settings.GitHub.Owner, repository, runId);
var job = jobs.Jobs.FirstOrDefault(j => j.Status.StringValue == "queued")
?? jobs.Jobs.FirstOrDefault();
if (job == null)
Expand All @@ -247,7 +248,7 @@ public async Task<AccessToken> GetTokenForRunner(string repository)
await EnsureValidTokenAsync();
try
{
return await gitHubClient.Actions.SelfHostedRunners.CreateRepositoryRegistrationToken(_owner, repository);
return await Client.Actions.SelfHostedRunners.CreateRepositoryRegistrationToken(settings.GitHub.Owner, repository);
}
catch (NotFoundException)
{
Expand All @@ -271,8 +272,8 @@ public async Task<IReadOnlyList<ArtifactRunInfo>> GetSuccessfulRunsWithArtifacts
try
{
runs = string.IsNullOrEmpty(workflowName)
? await gitHubClient.Actions.Workflows.Runs.List(_owner, repository, request, options)
: await gitHubClient.Actions.Workflows.Runs.ListByWorkflow(_owner, repository, workflowName, request, options);
? await Client.Actions.Workflows.Runs.List(settings.GitHub.Owner, repository, request, options)
: await Client.Actions.Workflows.Runs.ListByWorkflow(settings.GitHub.Owner, repository, workflowName, request, options);
}
catch (NotFoundException)
{
Expand All @@ -288,7 +289,7 @@ public async Task<IReadOnlyList<ArtifactRunInfo>> GetSuccessfulRunsWithArtifacts
ListArtifactsResponse artifacts;
try
{
artifacts = await gitHubClient.Actions.Artifacts.ListWorkflowArtifacts(_owner, repository, run.Id);
artifacts = await Client.Actions.Artifacts.ListWorkflowArtifacts(settings.GitHub.Owner, repository, run.Id);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -321,13 +322,13 @@ public async Task<ArtifactRunInfo> ResolveLatestArtifactAsync(
public async Task<WorkflowProgress?> GetWorkflowProgressAsync(string repository, long runId)
{
await EnsureValidTokenAsync();
var run = await gitHubClient.Actions.Workflows.Runs.Get(_owner, repository, runId);
var run = await Client.Actions.Workflows.Runs.Get(settings.GitHub.Owner, repository, runId);
var isCompleted = run.Status.StringValue == "completed";
var conclusion = isCompleted ? (run.Conclusion?.StringValue ?? "unknown") : null;

try
{
var jobs = await gitHubClient.Actions.Workflows.Jobs.List(_owner, repository, runId);
var jobs = await Client.Actions.Workflows.Jobs.List(settings.GitHub.Owner, repository, runId);
var job = jobs.Jobs.FirstOrDefault(j => j.Status.StringValue == "in_progress")
?? jobs.Jobs.LastOrDefault();

Expand Down
4 changes: 2 additions & 2 deletions IoTDeploy/IoTDeploy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
</ItemGroup>

Expand Down
4 changes: 2 additions & 2 deletions IoTDeploy/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@
cts.Cancel();
};

var githubProvider = new GithubProvider();
var githubProvider = new GithubProvider(settings);
var runner = new Runner(Guid.NewGuid().ToString("N"));
try
{
Console.WriteLine(Strings.ConnectingToGitHub);
await githubProvider.Init(settings);
await githubProvider.Init();

var workflows = await githubProvider.GetWorkflows(cli.Repo);
if (workflows.Count == 0)
Expand Down
1 change: 1 addition & 0 deletions IoTDeploy/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions IoTDeploy/Strings.cs.resx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ Umístěte ho do složky aplikace.</value>
<value>Nenalezen soubor privátního klíče GitHub App (pattern: '{0}').
Umístěte soubor do složky aplikace nebo upravte PemFilePattern v appsettings.json.</value>
</data>
<data name="PrivateKeyNotFound" xml:space="preserve">
<value>Privátní klíč GitHub App nebyl nalezen.</value>
</data>
<data name="CannotReadPrivateKey" xml:space="preserve">
<value>Nelze načíst privátní klíč:
{0}
Expand Down
3 changes: 3 additions & 0 deletions IoTDeploy/Strings.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ Legen Sie sie im Anwendungsordner ab.</value>
<value>Private-Key-Datei der GitHub App nicht gefunden (Muster: '{0}').
Legen Sie die Datei im Anwendungsordner ab oder aktualisieren Sie PemFilePattern in appsettings.json.</value>
</data>
<data name="PrivateKeyNotFound" xml:space="preserve">
<value>Privater Schlüssel der GitHub App nicht gefunden.</value>
</data>
<data name="CannotReadPrivateKey" xml:space="preserve">
<value>Privater Schlüssel kann nicht gelesen werden:
{0}
Expand Down
3 changes: 3 additions & 0 deletions IoTDeploy/Strings.es.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ Colóquelo en la carpeta de la aplicación.</value>
<value>Archivo de clave privada de GitHub App no encontrado (patrón: '{0}').
Coloque el archivo en la carpeta de la aplicación o actualice PemFilePattern en appsettings.json.</value>
</data>
<data name="PrivateKeyNotFound" xml:space="preserve">
<value>Clave privada de GitHub App no encontrada.</value>
</data>
<data name="CannotReadPrivateKey" xml:space="preserve">
<value>No se puede leer la clave privada:
{0}
Expand Down
3 changes: 3 additions & 0 deletions IoTDeploy/Strings.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ Placez-le dans le dossier de l'application.</value>
<value>Fichier de clé privée de GitHub App introuvable (motif : '{0}').
Placez le fichier dans le dossier de l'application ou mettez à jour PemFilePattern dans appsettings.json.</value>
</data>
<data name="PrivateKeyNotFound" xml:space="preserve">
<value>Clé privée de GitHub App introuvable.</value>
</data>
<data name="CannotReadPrivateKey" xml:space="preserve">
<value>Impossible de lire la clé privée :
{0}
Expand Down
3 changes: 3 additions & 0 deletions IoTDeploy/Strings.pl.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ Umieść go w folderze aplikacji.</value>
<value>Nie znaleziono pliku klucza prywatnego GitHub App (wzorzec: '{0}').
Umieść plik w folderze aplikacji lub zaktualizuj PemFilePattern w appsettings.json.</value>
</data>
<data name="PrivateKeyNotFound" xml:space="preserve">
<value>Nie znaleziono klucza prywatnego GitHub App.</value>
</data>
<data name="CannotReadPrivateKey" xml:space="preserve">
<value>Nie można odczytać klucza prywatnego:
{0}
Expand Down
3 changes: 3 additions & 0 deletions IoTDeploy/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ Place it in the application folder.</value>
<value>GitHub App private key file not found (pattern: '{0}').
Place the file in the application folder or update PemFilePattern in appsettings.json.</value>
</data>
<data name="PrivateKeyNotFound" xml:space="preserve">
<value>GitHub App private key not found.</value>
</data>
<data name="CannotReadPrivateKey" xml:space="preserve">
<value>Cannot read private key:
{0}
Expand Down
3 changes: 3 additions & 0 deletions IoTDeploy/Strings.sk.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ Umiestnite ho do priečinka aplikácie.</value>
<value>Súbor privátneho kľúča GitHub App nebol nájdený (vzor: '{0}').
Umiestnite súbor do priečinka aplikácie alebo upravte PemFilePattern v appsettings.json.</value>
</data>
<data name="PrivateKeyNotFound" xml:space="preserve">
<value>Privátny kľúč GitHub App nebol nájdený.</value>
</data>
<data name="CannotReadPrivateKey" xml:space="preserve">
<value>Nie je možné načítať privátny kľúč:
{0}
Expand Down
12 changes: 6 additions & 6 deletions IoTDeployUI/Form1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public Form1(AppSettings settings, string logPath)
InitializeComponent();
this.settings = settings;
_logPath = logPath;
githubProvider = new GithubProvider();
githubProvider = new GithubProvider(settings);
}

private void label1_Click(object sender, EventArgs e)
Expand All @@ -31,9 +31,9 @@ private async void Form1_Load(object sender, EventArgs e)
SetUiBusy(Strings.ConnectingToGitHub);
try
{
await githubProvider.Init(settings);
await githubProvider.Init();
cmbRepository.Items.Clear();
foreach (var repo in await githubProvider.GetRepositories())
foreach (var repo in (await githubProvider.GetRepositories()).OrderBy(k => k.Name))
{
cmbRepository.Items.Add(repo.Name);
}
Expand Down Expand Up @@ -189,22 +189,22 @@ private async void cmbRepository_SelectedIndexChanged(object sender, EventArgs e
try
{
cmbBranch.Items.Clear();
foreach (var branch in await githubProvider.GetBranches(repositoryName))
foreach (var branch in (await githubProvider.GetBranches(repositoryName)).OrderBy(k => k.Name))
{
cmbBranch.Items.Add(branch.Name);
}

cmbWorkflow.Items.Clear();
var workflows = await githubProvider.GetWorkflows(repositoryName);
foreach (var wf in workflows)
foreach (var wf in workflows.OrderBy(k => k.Name))
{
cmbWorkflow.Items.Add(new WorkflowComboItem(wf));
}
if (cmbWorkflow.Items.Count > 0)
cmbWorkflow.SelectedIndex = 0;

cmbEnvironment.Items.Clear();
foreach (var env in await githubProvider.GetEnvironments(repositoryName))
foreach (var env in (await githubProvider.GetEnvironments(repositoryName)).OrderBy(k => k.Name))
{
cmbEnvironment.Items.Add(env.Name);
}
Expand Down
Loading
Loading