Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved documentation with badges #57

Merged
merged 6 commits into from
May 1, 2024
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
52 changes: 24 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# WinTuner

[![Documentation badge](https://img.shields.io/badge/Show_Documentation-darkblue?style=for-the-badge)](https://wintuner.app/)
[![PowerShell gallery version][badge_powershell]][link_powershell]
[![Nuget version][badge_nuget]][link_nuget]
[![License][badge_license]][link_license]
[![GitHub issues](https://img.shields.io/github/issues/svrooij/wingetintune?style=for-the-badge)](https://github.com/svrooij/WingetIntune/issues)
[![Github sponsors](https://img.shields.io/github/sponsors/svrooij?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/svrooij)

[Documentation](https://wintuner.app/)
[![WinTuner Mascot](https://wintuner.app/img/wintuner-mascotte-two_100.png)](https://wintuner.app/)

Take any app from WinGet and upload it to Intune in minutes. This app is available as [PowerShell module](#wintuner-powershell-module) and as a [CLI](#wintuner-cli), both run mostly thee same code.

Expand All @@ -19,6 +23,9 @@ Take any app from WinGet and upload it to Intune in minutes. This app is availab

## WinTuner PowerShell Module

[![PowerShell gallery version][badge_powershell]][link_powershell]
[![PowerShell gallery downloads][badge_powershell_downloads]][link_powershell]

This is the PowerShell version of the WinTuner application, requiring PowerShell `7.4` (net8.0). Available in the [PowerShell Gallery](https://www.powershellgallery.com/packages/WinTuner/). Documentation can be found [here](https://wintuner.app/docs/category/wintuner-powershell).

```PowerShell
Expand All @@ -29,48 +36,27 @@ As of April 2024, the main development focus will be on the PowerShell module, s

## WinTuner CLI

This application ~~is Windows only and~~ requires **Dotnet 8** to be installed on your computer. It's a [beta application](#beta-application), so please report any issues you find.
[![Nuget version][badge_nuget]][link_nuget]
[![Nuget downloads][badge_nuget_downloads]][link_nuget]

This application requires **Dotnet 8** to be installed on your computer. It's a [beta application](#beta-application), so please report any issues you find.
Some commands run the `winget` in the background and are thus Windows-only, make sure you have the [App Installer](https://www.microsoft.com/p/app-installer/9nblggh4nns1) installed on your computer if you want to use these commands.

The `package` and `publish` commands are cross-platform, and should work on any platform that supports dotnet 8. These commands no longer use the WinGet executable, which also means any other sources than `winget` are no longer supported.
The `msi` command is still windows only, as it uses the `Microsoft.Deployment.WindowsInstaller` package.

Check out the [documentation](https://wintuner.app/docs/category/wintuner-cli) for more information.

### Installing

This package can be downloaded as a dotnet tool. Make sure you have Dotnet 8 installed on your computer.
I'm working to get a code signing certificate, but for now you might have to configure an exception on your computer to run unsigned code.

```Shell
# Install dotnet 8 sdk (or the way specific for your platform)
winget install --id Microsoft.DotNet.SDK.8 --source winget

# Add the nuget feed, if that is not already done
dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org

# This command will install the tool
dotnet tool install --global Svrooij.Winget-Intune.Cli

# or to update to the latest version
dotnet tool update --global SvRooij.Winget-Intune.Cli

```

## Beta application

This is a beta application, it's not yet ready for production use. I'm still working on it, and I'm looking for feedback.
If you found a bug please create an [issue](https://github.com/svrooij/WingetIntune/issues/new/choose), if you have questions or want to share your feedback, check out the [discussions](https://github.com/svrooij/WingetIntune/discussions) page.

## Library (soon)

I'm planning to release the actual intune specific code as a separate library, so you can use it in your own projects. This will be released as a separate package.

## Contributing

If you want to contribute to this project, please check out the [contributing](https://github.com/svrooij/WingetIntune/blob/main/CONTRIBUTING.md) page and the [Code of Conduct](https://github.com/svrooij/WingetIntune/blob/main/CODE_OF_CONDUCT.md).

## Usefull information
## Useful information

- [WinTuner website](https://wintuner.app/)
- [Blog articles on Intune](https://svrooij.io/tags/intune/)
Expand All @@ -84,4 +70,14 @@ If you want to contribute to this project, please check out the [contributing](h
[link_blog]: https://svrooij.io/
[link_linkedin]: https://www.linkedin.com/in/stephanvanrooij
[link_mastodon]: https://dotnet.social/@svrooij
[link_twitter]: https://twitter.com/svrooij
[link_twitter]: https://twitter.com/svrooij

[badge_license]: https://img.shields.io/github/license/svrooij/WingetIntune?style=for-the-badge
[link_license]: https://github.com/svrooij/WingetIntune/blob/main/LICENSE.txt
[badge_powershell]: https://img.shields.io/powershellgallery/v/WinTuner?style=for-the-badge&logo=powershell&logoColor=white
[badge_powershell_downloads]: https://img.shields.io/powershellgallery/dt/WinTuner?style=for-the-badge&logo=powershell&logoColor=white
[link_powershell]: https://www.powershellgallery.com/packages/WinTuner/

[badge_nuget]: https://img.shields.io/nuget/v/Svrooij.Winget-Intune.Cli?style=for-the-badge&logo=nuget&logoColor=white
[badge_nuget_downloads]: https://img.shields.io/nuget/dt/Svrooij.Winget-Intune.Cli?style=for-the-badge&logo=nuget&logoColor=white
[link_nuget]: https://www.nuget.org/packages/Svrooij.Winget-Intune.Cli/
19 changes: 11 additions & 8 deletions src/Svrooij.WinTuner.CmdLets/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# WinTuner PowerShell module

## Refresh documentation
[![Documentation badge](https://img.shields.io/badge/Show_Documentation-darkblue?style=for-the-badge)](https://wintuner.app/)
[![PowerShell gallery version][badge_powershell]][link_powershell]
[![PowerShell gallery downloads][badge_powershell_downloads]][link_powershell]
[![License][badge_license]][link_license]

```PowerShell
New-MarkdownHelp -Module "Svrooij.WinTuner.CmdLets" -OutputFolder "..\..\..\docs" -WithModulePage -Force
```
Source of WinTuner PowerShell module, available in the [PowerShell Gallery][link_powershell].

## Create a package and deploy to Intune
Documentation can be found [here](https://wintuner.app/docs/category/wintuner-powershell).

```PowerShell
New-WtWingetPackage -PackageId Jandedobbeleer.ohmyposh -PackageFolder C:\tools\packages\ | Deploy-WtWin32App -Username [email protected]
```
[badge_license]: https://img.shields.io/github/license/svrooij/WingetIntune?style=for-the-badge
[link_license]: https://github.com/svrooij/WingetIntune/blob/main/LICENSE.txt
[badge_powershell]: https://img.shields.io/powershellgallery/v/WinTuner?style=for-the-badge&logo=powershell&logoColor=white
[badge_powershell_downloads]: https://img.shields.io/powershellgallery/dt/WinTuner?style=for-the-badge&logo=powershell&logoColor=white
[link_powershell]: https://www.powershellgallery.com/packages/WinTuner/
108 changes: 101 additions & 7 deletions src/WingetIntune.Cli/Commands/GenerateIndexCommand.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Hosting;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using WingetIntune.Cli.Configuration;

namespace WingetIntune.Commands;
Expand All @@ -20,9 +16,19 @@ internal class GenerateIndexCommand : Command
public GenerateIndexCommand() : base(name, description)
{
IsHidden = true;
AddOption(new Option<string>(new string[] { "--output-path", "-o" }, "The path to the output file") { IsRequired = true });
AddOption(new Option<Uri>(new string[] { "--source-uri", "-s" }, () => new Uri(Winget.CommunityRepository.WingetRepository.DefaultIndexUri), "The source URI to use for the index.json file"));
AddOption(new Option<int>(new string[] { "--timeout", "-t" }, () => 600000, "The timeout for the operation in milliseconds"));
AddOption(new Option<string>(["--output-path", "-o"], "The path to the output file") { IsRequired = true });
AddOption(new Option<Uri>(["--source-uri", "-s"], () => new Uri(Winget.CommunityRepository.WingetRepository.DefaultIndexUri), "The source URI to use for the index.json file"));
AddOption(new Option<int>(["--timeout", "-t"], () => 600000, "The timeout for the operation in milliseconds"));
AddOption(new Option<string>(["--update-json"], "Create JSON file with only the updates") { IsHidden = true });
AddOption(new Option<string>(["--update-csv"], "Create CSV file with only the updates") { IsHidden = true });
AddOption(new Option<bool>(["--update-github"], "Create GitHub Action step summary") { IsHidden = true });
AddOption(new Option<Uri?>(["--update-uri"], () =>
{
var uri = Environment.GetEnvironmentVariable("UPDATE_URI");
return string.IsNullOrEmpty(uri) ? null : new Uri(uri);
}, "Post updates to this url")
{ IsHidden = true });

this.Handler = CommandHandler.Create<GenerateIndexCommandOptions, InvocationContext>(HandleCommand);
}

Expand All @@ -39,16 +45,104 @@ private async Task<int> HandleCommand(GenerateIndexCommandOptions options, Invoc
var repo = host.Services.GetRequiredService<Winget.CommunityRepository.WingetRepository>();
repo.UseRespository = true;
var packages = await repo.RefreshPackages(false, combinedCancellation.Token);
if (File.Exists(options.OutputPath) && options.DetectChanges)
{
await HandleChanges(logger, options, packages, combinedCancellation.Token);
}
var json = JsonSerializer.Serialize(packages);
await File.WriteAllTextAsync(Path.GetFullPath(options.OutputPath), json, combinedCancellation.Token);
logger.LogInformation("Generated index.json file at {outputPath}", options.OutputPath);
return 0;
}

private static async Task HandleChanges(ILogger logger, GenerateIndexCommandOptions options, IEnumerable<Winget.CommunityRepository.Models.WingetEntry> packages, CancellationToken cancellationToken)
{
logger.LogInformation("Detecting changes from existing index.json file at {outputPath}", options.OutputPath);
var existingJson = await File.ReadAllTextAsync(Path.GetFullPath(options.OutputPath), cancellationToken);
var existingPackages = JsonSerializer.Deserialize<IEnumerable<Winget.CommunityRepository.Models.WingetEntry>>(existingJson);
if (existingPackages is not null)
{
var updates = packages
.Where(p => !existingPackages.Any(ep => ep.PackageId == p.PackageId && ep.Version == p.Version))
.OrderBy(p => p.PackageId);
if (updates.Any())
{
var lastWriteTime = File.GetLastWriteTimeUtc(Path.GetFullPath(options.OutputPath));
logger.LogInformation("Detected {count} updates since {lastWriteTime:yyyy-MM-dd HH:mm} UTC", updates.Count(), lastWriteTime);
if (!string.IsNullOrEmpty(options.UpdateJson))
{
var updatesJson = JsonSerializer.Serialize(updates);
await File.WriteAllTextAsync(Path.GetFullPath(options.UpdateJson), updatesJson, cancellationToken);
logger.LogInformation("Generated updates.json file at {outputPath}", options.UpdateJson);
}

if (!string.IsNullOrEmpty(options.UpdateCsv))
{
var csv = new StringBuilder();
csv.AppendLine("\"PackageId\",\"Version\"");
foreach (var update in updates)
{
csv.AppendLine($"\"{update.PackageId}\",\"{update.Version}\"");
}
await File.WriteAllTextAsync(Path.GetFullPath(options.UpdateCsv), csv.ToString(), cancellationToken);
logger.LogInformation("Generated updates.csv file at {outputPath}", options.UpdateCsv);
}

if (options.UpdateGithub == true)
{
// Write markdown table with update summary to environment variable GITHUB_STEP_SUMMARY
// get last file write date from the existing file

var markdown = new StringBuilder();
markdown.AppendLine($"Detected **{updates.Count()}** updates since `{lastWriteTime:yyyy-MM-dd HH:mm:ss} UTC`");
markdown.AppendLine("");
markdown.AppendLine("| PackageId | Version |");
markdown.AppendLine("| --- | --- |");
foreach (var update in updates)
{
markdown.AppendLine($"| {update.PackageId} | {update.Version} |");
}
Environment.SetEnvironmentVariable("GITHUB_STEP_SUMMARY", markdown.ToString(), EnvironmentVariableTarget.Process);
logger.LogInformation("Generated GitHub Action step summary");
}

if (options.UpdateUri is not null && options.UpdateUri.IsAbsoluteUri)
{
await PostUpdatesToUri(logger, options.UpdateUri, updates, cancellationToken);
}
}
else
{
logger.LogInformation("No updates detected");
}
}
}

private static async Task PostUpdatesToUri(ILogger logger, Uri uri, IEnumerable<Winget.CommunityRepository.Models.WingetEntry> updates, CancellationToken cancellationToken)
{
try
{
var json = JsonSerializer.Serialize(updates);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var client = new HttpClient();
await client.PostAsync(uri, content, cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to post updates to {host}", uri.Host);
}
}

internal class GenerateIndexCommandOptions
{
public int Timeout { get; set; }
public string OutputPath { get; set; }
public Uri? SourceUri { get; set; }
public string? UpdateCsv { get; set; }
public string? UpdateJson { get; set; }
public bool? UpdateGithub { get; set; }
public Uri? UpdateUri { get; set; }

internal bool DetectChanges => !string.IsNullOrEmpty(UpdateJson) || !string.IsNullOrEmpty(UpdateCsv) || UpdateUri?.IsAbsoluteUri == true || UpdateGithub == true;
}
}
Binary file added static/img/wintuner-mascotte-two_100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.