Skip to content

Commit

Permalink
Additional GitHub Action feature support.
Browse files Browse the repository at this point in the history
- Added GitHub actions logging (debug|notice|warning|error).
- SVG test badge generation
- `test_result` output
- restructured directories
  • Loading branch information
erichiller committed Feb 7, 2024
1 parent 5962b82 commit 8f007d7
Show file tree
Hide file tree
Showing 12 changed files with 539 additions and 328 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ ENV TZ="America/Chicago"

# Copy everything
COPY src/PlotGitHubAction/*.cs .
COPY src/PlotGitHubAction/Utils/*.cs .
COPY src/PlotGitHubAction/PlotGitHubAction.csproj .

# Restore as distinct layers
Expand Down
18 changes: 16 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,23 @@ inputs:
description: 'Enable debug logging for the action'
required: false
default: 'false'
log_level:
description: 'Log Level: (verbose|debug|notice|warn|error|none)'
required: false
default: 'notice'

# https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-docker-container-and-javascript-actions
outputs:
test_result: # number of outputs
description: '{ "success": (true|false) }'
# tests_success_count:
# description: 'The number of tests that were successful'
# tests_failed_count:
# description: 'The number of tests that failed'
# tests_skipped_count:
# description: 'The number of tests that were skipped'


# TODO: Add output for Failed tests

#outputs:
# random-number:
# description: "Random number"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace PlotGitHubAction;

/*
*
*/

public record ActionConfig(
string OutputDir,
string? SourceScanDir,
Expand Down Expand Up @@ -274,317 +265,4 @@ private static List<GitRepoInfo> scanRepoRoots( string directoryRoot ) {
}
return repo;
}
}

public class RepoNotFoundException : System.Exception { }

[ SuppressMessage( "ReSharper", "NotAccessedPositionalProperty.Global" ) ]
public record GitRepoInfo(
string Name,
string GitHubCommitUrl,
string RootDirPath,
string CommitSha,
string Branch,
string GitHubBranchUrl
) {
[ JsonIgnore ]
public DirectoryInfo RootDir => new DirectoryInfo( RootDirPath );

// , bool useBranch = false
public static GitRepoInfo CreateFromGitDir( DirectoryInfo gitRoot ) {
if ( gitRoot.Name == @".git" ) {
gitRoot = gitRoot.Parent!;
}
var match = Regex.Match(
System.IO.File.ReadAllText( System.IO.Path.Combine( gitRoot.FullName, ".git", "FETCH_HEAD" ) ),
@"(?<CommitSha>[0-9a-f]+).*github.com[/:](?<Name>.+$)"
);
string repoName = match.Groups[ "Name" ].Value.Trim();
string commitSha = match.Groups[ "CommitSha" ].Value;
match = Regex.Match(
System.IO.File.ReadAllText( System.IO.Path.Combine( gitRoot.FullName, ".git", "HEAD" ) ),
@"^ref: refs/heads/(?<Branch>.*)$"
);
string branch = match.Groups[ "Branch" ].Value.Trim();
string gitHubCommitUrlBase =
@"https://github.com/"
+ repoName
+ "/blob/"
+ commitSha
+ "/";
string gitHubBranchUrlBase =
@"https://github.com/"
+ repoName
+ "/tree/"
+ branch
+ "/";
return new GitRepoInfo(
Name: repoName,
GitHubCommitUrl: gitHubCommitUrlBase,
RootDirPath: gitRoot.FullName,
CommitSha: commitSha,
Branch: branch,
GitHubBranchUrl: gitHubBranchUrlBase
);
}
}

/*
*
*/

public readonly record struct CharPosition(
int Line,
int Column
);

readonly record struct SourceText(
string Text,
string Level,
string FilePath,
CharPosition Start,
CharPosition End
) {
public string FormattedFileLocation( ) =>
System.IO.Path.GetFileName( FilePath )
+ " " + Start.Line + ":" + Start.Column;

public string SortableLocationString =>
System.IO.Path.GetFileName( FilePath )
+ "_" + Start.Line.ToString().PadLeft( 6, '0' )
+ ":" + Start.Column.ToString().PadLeft( 6, '0' );

public string MarkdownSafeText( ) =>
Text.Replace( "\n", "<br />" );
}

/*
*
*/

public interface INamedObject {
public string Name { get; }
}

public class CsProjInfo : INamedObject, IEquatable<CsProjInfo> {
[ SuppressMessage( "ReSharper", "MemberCanBePrivate.Global" ) ]
public GitRepoInfo GitRepo { get; init; }

[ JsonConstructor ]
public CsProjInfo( string filePath, GitRepoInfo gitRepo ) {
GitRepo = gitRepo;
FilePath = filePath;
DirectoryPath = System.IO.Path.GetDirectoryName( filePath ) ?? throw new ArgumentException( $"Unable to determine directory name of {filePath}" );
ProjectName = System.IO.Path.GetFileNameWithoutExtension( filePath );
RepoRelativePath = Path.GetRelativePath( gitRepo.RootDir.FullName, filePath );
RepoRelativeDirectoryPath = Path.GetDirectoryName( this.RepoRelativePath ) ?? throw new NullReferenceException();
}

public CsProjInfo( CsProjInfo toClone ) {
FilePath = toClone.FilePath;
DirectoryPath = toClone.DirectoryPath;
ProjectName = toClone.ProjectName;
GitRepo = toClone.GitRepo;
RepoRelativePath = toClone.RepoRelativePath;
RepoRelativeDirectoryPath = Path.GetDirectoryName( this.RepoRelativePath ) ?? throw new NullReferenceException();
}

public string ProjectName { get; }
public string Name => ProjectName;
public string DirectoryPath { get; }
[ SuppressMessage( "ReSharper", "MemberCanBePrivate.Global" ) ]
public string RepoRelativePath { get; }
public string RepoRelativeDirectoryPath { get; }
public string FilePath { get; }
public string MarkdownId => ProjectName.Replace( '.', '-' );

public bool ContainsFile( string filePath ) =>
filePath.StartsWith( this.DirectoryPath.TrimEnd( Path.DirectorySeparatorChar ) + Path.DirectorySeparatorChar );

public bool Equals( CsProjInfo? other ) {
return other?.FilePath == this.FilePath;
}

public override bool Equals( object? obj ) {
return this.Equals( obj as CsProjInfo );
}

public static bool operator ==( CsProjInfo? left, CsProjInfo? right ) {
return Equals( left, right );
}

public static bool operator !=( CsProjInfo? left, CsProjInfo? right ) {
return !Equals( left, right );
}

public override int GetHashCode( ) {
return this.FilePath.GetHashCode();
}

public override string ToString( ) =>
$"CsProjInfo {{ {nameof(ProjectName)} = {ProjectName} {nameof(FilePath)} = {FilePath} }}";
}

/*
*
*/

public class UrlMdShortUtils {
private readonly Dictionary<string, string> _urlMap = new (StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _usedUrls = new (StringComparer.OrdinalIgnoreCase);

private readonly Dictionary<string, string> _idStrToGenerated = new (StringComparer.OrdinalIgnoreCase);
private readonly bool _generateIds;
private readonly ActionConfig? _config;

public UrlMdShortUtils( ActionConfig? config = null, bool generateIds = false ) {
_generateIds = generateIds;
_config = config;
}

public string Add( string id, string url, bool isCode = false ) {
string mdModifier = isCode ? "`" : String.Empty;
if ( _generateIds ) {
//
if ( !_idStrToGenerated.TryGetValue( id, out string? generatedId ) ) {
// generatedId = $"{_generatedIdPrefix}{_idSeq++}";
generatedId = toBase36( Utils.GetDeterministicHashCode( url ) );
_urlMap.TryAdd( generatedId, url );
_usedUrls.TryAdd( generatedId, url );
_idStrToGenerated.TryAdd( id, generatedId );
}

return $"{mdModifier}[{id}][{generatedId}]{mdModifier}";
}
_urlMap.TryAdd( id, url );
_usedUrls.TryAdd( id, url );
return $"{mdModifier}[{id}]{mdModifier}";
}

public string GetFormattedLink( string id, bool isCode = false ) {
string mdModifier = isCode ? "`" : String.Empty;
if ( _generateIds ) {
if ( _idStrToGenerated.TryGetValue( id, out string? generatedId ) ) {
return $"{mdModifier}[{id}][{generatedId}]{mdModifier}";
}
} else if ( _urlMap.ContainsKey( id ) ) {
return $"{mdModifier}[{id}]{mdModifier}";
}
return $"{mdModifier}{id}{mdModifier}";
}

public string AddSourceLink( string filePath, CharPosition? start = null, CharPosition? end = null, bool linkToBranch = false ) {
if ( this._config is not { } config ) {
throw new NullReferenceException( nameof(_config) );
}
string linkTitle = config.GetFormattedSourcePosition( filePath, start, end );
if ( config.GetGitHubSourceLink( filePath, start, end, linkToBranch ) is not { } url ) {
if ( !filePath.Contains( "Microsoft.NET.Sdk" ) ) {
Log.Warn( $"Unable to create source link for {linkTitle}" );
}
return linkTitle;
}
// KILL
try {
Log.Debug( $"[{filePath}] [{start}] [{end}]: {new Uri( url ).AbsoluteUri}" );
} catch ( System.UriFormatException e ) {
Log.Error( $"Uri format failed for [{filePath}] [{start}] [{end}]: '{url}': {e.Message}" );
throw;
}
return this.Add( linkTitle, url );
}

public void AddReferencedUrls( StringBuilder sb ) {
foreach ( var (id, url) in _usedUrls.OrderByDescending( kv => kv.Key ) ) {
try {
sb.AppendLine( $"[{id}]: {new Uri( url ).AbsoluteUri}" );
} catch ( System.UriFormatException e ) {
Log.Error( $"Uri format failed for id '{id}': '{url}': {e.Message}" );
throw;
}
}
}


private static string toBase36( uint h ) {
uint b = 36;
string s = String.Empty;
const string chars = "0123456789abcdefghijklmnopqrstuvwxyz";
while ( h > 0 ) {
s += chars[ ( int )( h % b ) ];
h = h / b;
}
return s.PadLeft( 7, '0' );
}
}

/*
*
*/

public static class Log {
// only log if I'm testing in the action's repo
// Note: Could also use Runner debug logging ; https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging
public static bool ShouldLog { get; } =
System.Environment.GetEnvironmentVariable( "GITHUB_ACTION_REPOSITORY" ) is not { Length: > 0 } actionRepo // if no GITHUB_ACTION_REPOSITORY, then assume Debug mode
|| ( System.Environment.GetEnvironmentVariable( "GITHUB_REPOSITORY" ) is { Length: > 0 } repo // or if the current repo is the action repo (eg. for tests)
&& repo == actionRepo )
|| ( System.Environment.GetEnvironmentVariable( "INPUT_DEBUG" )?.Equals( "true", StringComparison.OrdinalIgnoreCase ) ?? false ); // or the user explicitly sets

public static void Debug( object msg ) {
if ( ShouldLog ) {
System.Console.WriteLine( msg );
}
}

public static void Verbose( object msg ) {
if ( ShouldLog ) {
System.Console.WriteLine( msg );
}
}

public static void Info( object msg ) {
System.Console.WriteLine( msg );
}

public static void Warn( object msg ) {
System.Console.WriteLine( $"WARNING: {msg}" );
}

public static void Error( object msg ) {
System.Console.WriteLine( $"ERROR: {msg}" );
}
}

public static class Utils {
public static readonly JsonSerializerOptions SERIALIZER_OPTIONS = new JsonSerializerOptions {
Converters = {
new JsonStringEnumConverter(),
new ScottPlotColorConverter()
},
WriteIndented = true
};

/// <summary>
/// Create a predictable Hash code for input <paramref name="str"/>.
/// This is a Hash code that remains the same across Hardware, OS, and program runs.
/// </summary>
/// <returns><see cref="uint"/> based Hash code</returns>
public static uint GetDeterministicHashCode( string str ) {
unchecked {
uint hash1 = ( 5381 << 16 ) + 5381;
uint hash2 = hash1;

for ( uint i = 0 ; i < str.Length ; i += 2 ) {
hash1 = ( ( hash1 << 5 ) + hash1 ) ^ str[ ( int )i ];
if ( i == str.Length - 1 ) {
break;
}

hash2 = ( ( hash2 << 5 ) + hash2 ) ^ str[ ( int )i + 1 ];
}

return hash1 + ( hash2 * 1566083941 );
}
}
}
Loading

0 comments on commit 8f007d7

Please sign in to comment.