0.4.3-ci
ln.build - build0.waldrennach.l--n.de build job pending Details

master
Harald Wolff 2020-12-09 10:05:52 +01:00
parent 18e7a3a229
commit 1b9ed4e7ad
27 changed files with 952 additions and 211 deletions

View File

@ -1,52 +1,17 @@
{
"templates": [
"dotnet"
],
"env": {
"NUGET_SOURCE": "https://nexus.niclas-thobaben.de/repository/l--n.de/",
"CONFIGURATION": "Release"
},
"stages": [
{
"name": "setup",
"env": {
"SOME_ENV_VAR": "Some text",
},
"commands": [
"SH echo Setting up build environment",
"SH set",
"SH rm -Rf .build"
]
},
{
"name": "prepare",
"commands": [
"SH dotnet restore",
"SH dotnet clean"
"dotnet prepare */*.csproj"
]
},
{
"name": "build",
"commands": [
"SH dotnet build -c $CONFIGURATION"
]
},
{
"name": "pack_and_publish",
"commands": [
"SH dotnet pack ln.build -o .build -c $CONFIGURATION",
"SH dotnet pack ln.build.server -o .build -c $CONFIGURATION",
"SH dotnet publish ln.build.server -p:PublishTrimmed=true -p:PublishSingleFile=true -p:PublishReadyToRun=true --self-contained -r linux-x64 -c $CONFIGURATION -o .build/linux-x64",
"SH dotnet publish ln.build.server -p:PublishTrimmed=true -p:PublishSingleFile=true -p:PublishReadyToRun=false --self-contained -r win-x64 -c $CONFIGURATION -o .build/windows-x64"
]
},
{
"name": "push",
"commands": [
"SH for NUPKG in .build/ln.build.*.nupkg; do dotnet nuget push $NUPKG -s $NUGET_SOURCE -k $NUGET_APIKEY; done",
"RELEASE .build/linux-x64/ln.build.server=ln.build.server-linux-amd64",
"RELEASE .build/windows-x64/ln.build.server.exe=ln.build.server-windows-x64.exe"
],
"secrets": {
"NUGET_APIKEY": "https://nexus.niclas-thobaben.de"
}
}
]
}

View File

@ -12,18 +12,41 @@ using Microsoft.VisualBasic;
using ln.application;
using System.IO;
using ln.build.repositories.gitea;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using ln.build.semver;
using ln.build.pipeline;
namespace ln.build.server
{
public enum RunMode {
serv,
versioning,
build
}
class Program
{
static CIService CIService;
[StaticArgument( Option = 'm')]
static RunMode RunMode { get; set; } = RunMode.serv;
[StaticArgument( LongOption = "build")]
public static string BuildPath { get; set; }
static string BuildPath { get; set; }
[StaticArgument( LongOption = "build-secret")]
public static string BuildSecret { get; set; }
static string BuildSecret { get; set; }
[StaticArgument( LongOption = "versioning-provider")]
static string VersioningProviderName { get; set; }
[StaticArgument( LongOption = "versioning-level")]
static SemVerLevels VersioningLevel { get; set;} = SemVerLevels.PATCH;
[StaticArgument( LongOption = "versioning-source")]
static string VersioningSource { get; set;}
static void Main(string[] args)
{
@ -35,19 +58,39 @@ namespace ln.build.server
CIService.Initialize();
if (BuildPath != null)
switch (RunMode)
{
CIJob job = new CIJob(CIService,null, (BuildSecret != null) ? CIService.GetSecretStorage(BuildSecret) : null);
job.WorkingDirectory = BuildPath;
job.RunJob();
} else {
CIService.Initialize();
CIService.AddWebHookHandler("gitea", GiteaRepository.WebHookHandler);
CIService.Start();
case RunMode.serv:
CIService.Initialize();
CIService.AddWebHookHandler("gitea", GiteaRepository.WebHookHandler);
CIService.Start();
break;
case RunMode.build:
CIJob job = new CIJob(CIService,null, (BuildSecret != null) ? CIService.GetSecretStorage(BuildSecret) : null);
job.WorkingDirectory = BuildPath;
job.RunJob();
break;
case RunMode.versioning:
Versioning versioning = new Versioning(VersioningProviderName);
versioning.Sources = new string[]{ VersioningSource };
SemVersion version = versioning.GetCurrentVersion(null);
Logging.Log("INFO: found version {0}", version);
version.Increment(VersioningLevel);
Logging.Log("INFO: write back version {0}", version);
versioning.SetVersion(null, version);
break;
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<ServiceWorker>();
});
}

View File

@ -0,0 +1,16 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace ln.build
{
public class ServiceWorker : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
throw new System.NotImplementedException();
}
}
}

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<Version>0.4.2</Version>
<Version>0.4.3-ci</Version>
<Authors>Harald Wolff-Thobaben</Authors>
<Company>l--n.de</Company>
<Description>A simple build server scheduling builds triggered via web-hooks</Description>
@ -15,11 +15,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ln.application" Version="0.1.1" />
<PackageReference Include="ln.http" Version="0.1.2" />
<PackageReference Include="ln.json" Version="1.0.0" />
<PackageReference Include="ln.logging" Version="1.0.1" />
<PackageReference Include="ln.threading" Version="0.1.0" />
<PackageReference Include="ln.application" Version="0.1.*" />
<PackageReference Include="ln.http" Version="0.1.*" />
<PackageReference Include="ln.json" Version="1.0.*" />
<PackageReference Include="ln.logging" Version="1.0.*" />
<PackageReference Include="ln.threading" Version="0.1.*" />
<PackageReference Include="ln.type" Version="0.1.*" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="5.0.0" />
</ItemGroup>

View File

@ -1,20 +1,22 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using ln.build.commands;
using ln.build.pipeline;
using ln.build.repositories;
using ln.build.secrets;
using ln.build.semver;
using ln.json;
using ln.logging;
using ln.threading;
namespace ln.build
{
public delegate void CIJobCompleted(CIJob cIJob);
public class CIJob : PoolJob
{
public event CIJobCompleted OnJobCompleted;
public string JobID { get; } = Guid.NewGuid().ToString("N");
public CIService CIService { get; }
public Repository Repository { get; set; }
@ -115,6 +117,8 @@ namespace ln.build
Logger.Log(LogLevel.ERROR, "CIJob failed at CloneRepository()");
}
OnJobCompleted?.Invoke(this);
Notify();
Cleanup();
}
@ -158,5 +162,30 @@ namespace ln.build
Directory.Delete(WorkingDirectory, true);
}
public void PublishRelease(SemVerLevels releaseLevel)
{
if (BuildState == BuildState.SUCCESS)
{
if (PipeLine.Versioning != null)
{
Versioning versioning = PipeLine.Versioning;
SemVersion version = versioning.GetCurrentVersion(this);
if (version.IsPreRelease)
{
SemVersion releaseVersion = new SemVersion(version);
releaseVersion.PreRelease = null;
versioning.SetVersion(this, releaseVersion);
} else if (version.IsRelease)
{
}
}
}
}
}
}

View File

@ -5,8 +5,11 @@ using System.IO;
using System.Net;
using System.Net.NetworkInformation;
using ln.application;
using ln.build.commands;
using ln.build.pipeline;
using ln.build.repositories;
using ln.build.secrets;
using ln.build.semver;
using ln.http;
using ln.http.router;
using ln.json;
@ -43,9 +46,7 @@ namespace ln.build
TemplateRouter templateRouter;
HTTPServer httpServer;
HashSet<PipeLine> pipelines = new HashSet<PipeLine>();
public IEnumerable<PipeLine> PipeLines => pipelines;
public StageCommandContainer StageCommands { get; } = new StageCommandContainer(null,null);
public CIService()
{
@ -72,6 +73,8 @@ namespace ln.build
Directory.CreateDirectory(ReportsDirectory);
Directory.CreateDirectory(Path.Combine(BaseDirectory, "secrets"));
InitializeStageCommands();
InitializeHttpServer();
}
@ -114,6 +117,21 @@ namespace ln.build
}
}
public void InitializeStageCommands()
{
StageCommands.AddCommand(CoreCommands.ShellCommand, "sh");
StageCommands.AddCommand(DotNetCommand.Prepare, "dotnet", "prepare" );
StageCommands.AddCommand(DotNetCommand.Build, "dotnet", "build" );
StageCommands.AddCommand(DotNetCommand.Test, "dotnet", "test" );
StageCommands.AddCommand(DotNetCommand.Pack, "dotnet", "pack" );
StageCommands.AddCommand(DotNetCommand.Push, "dotnet", "push" );
StageCommands.AddCommand(DotNetCommand.Publish, "dotnet", "publish" );
StageCommands.AddCommand(DeployCommand.Deploy, "deploy" );
StageCommands.AddCommand(DeployCommand.Release, "release" );
}
public string GetJobURL(CIJob job) => string.Format("{0}/builds/{1}", BaseURL, job.JobID);
public void Start()
@ -122,11 +140,6 @@ namespace ln.build
httpServer.Start();
}
public void AddPipeline(PipeLine pipeLine)
{
pipelines.Add(pipeLine);
}
public void AddWebHookHandler(string name, Func<CIService,HttpRequest,HttpResponse> webHookHandler)
{
hookRouter.AddSimpleRoute(String.Format("/{0}", name), (HttpRoutingContext context,HttpRequest request) => webHookHandler(this, request));

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using ln.http.exceptions;
namespace ln.build
{
public static class PathHelper
{
public static IEnumerable<string> ResolvePattern(string pattern) => ResolvePattern(pattern, Environment.CurrentDirectory);
public static IEnumerable<string> ResolvePattern(string pattern, string start)
{
string[] patternTokens = pattern.Split('/', StringSplitOptions.RemoveEmptyEntries);
List<string> matches = new List<string>();
collect(patternTokens, 0, start, matches);
return matches;
}
static void collect(string[] tokens,int depth,string currentPath, List<string> matches)
{
if (depth < tokens.Length-1)
{
foreach (string dirname in Directory.GetDirectories(currentPath, tokens[depth]))
{
collect(tokens, depth + 1, dirname, matches);
}
} else if (depth == (tokens.Length - 1))
{
foreach (string filename in Directory.GetFiles(currentPath, tokens[depth]))
{
matches.Add(filename);
}
} else
throw new Exception("serious bug");
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Jint.Parser.Ast;
using ln.logging;
namespace ln.build.commands
@ -33,9 +34,9 @@ namespace ln.build.commands
TestExitCode = testExitCode;
}
public void AddArgument(Argument argument) => arguments.Add(argument);
public void AddArguments(params Argument[] args) => arguments.AddRange(args);
public CommandRunner AddArgument(Argument argument){ arguments.Add(argument); return this; }
public CommandRunner AddArguments(params Argument[] args) { arguments.AddRange(args); return this; }
public CommandRunner AddArguments(params string[] args) { arguments.AddRange(args.Select((string arg)=>(Argument)arg)); return this; }
public string FindFileInPath(CommandEnvironment environment, string filename)
{
Logger.Log(LogLevel.DEBUG, "Looking up {0} in paths {1}", filename, environment.Get("PATH",""));
@ -116,7 +117,7 @@ namespace ln.build.commands
public class Argument
{
string value;
protected string value;
public bool MaskValue { get; set; }
protected Argument(){ }
@ -142,8 +143,10 @@ namespace ln.build.commands
public Option(string optionArgument, string optionValue) : this(optionArgument, optionValue, false){ }
public Option(string optionArgument, string optionValue, bool maskValue)
{
value = optionValue;
OptionArgument = optionArgument;
GetValue = (e) => optionValue;
GetValue = (e) => value;
MaskValue = maskValue;
}
public Option(string optionArgument, Func<CommandEnvironment,string> getOptionValue) : this(optionArgument, getOptionValue, false) { }

View File

@ -5,7 +5,7 @@
</PropertyGroup>
<PropertyGroup>
<Version>0.4.2</Version>
<Version>0.4.3-ci</Version>
<Authors>Harald Wolff-Thobaben</Authors>
<Company>l--n.de</Company>
<Description>A simple build server scheduling builds triggered via web-hooks</Description>
@ -15,16 +15,18 @@
<ItemGroup>
<None Update="html/**" CopyToOutputDirectory="PreserveNewest" />
<None Update="scripts/**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ln.logging" Version="1.0.1" />
<PackageReference Include="ln.threading" Version="0.1.0" />
<PackageReference Include="ln.json" Version="1.0.0" />
<PackageReference Include="ln.http" Version="0.1.2" />
<PackageReference Include="ln.templates" Version="0.1.1" />
<PackageReference Include="ln.templates.http" Version="0.0.1-test" />
<PackageReference Include="ln.logging" Version="1.0.*" />
<PackageReference Include="ln.threading" Version="0.1.*" />
<PackageReference Include="ln.json" Version="1.0.*" />
<PackageReference Include="ln.http" Version="0.1.*" />
<PackageReference Include="ln.templates" Version="0.1.*" />
<PackageReference Include="ln.templates.http" Version="0.0.*" />
<PackageReference Include="ln.type" Version="0.1.*" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using System;
using System.IO;
using System.Text;
using ln.build.commands;
using ln.logging;
namespace ln.build.pipeline
{
public static class CoreCommands
{
public static void ShellCommand(Stage stage,params string[] arguments)
{
CommandRunner commandRunner = new CommandRunner("/bin/bash", "-c", string.Format("\"{0}\"", string.Join(' ', arguments))) { Throw = CRThrow.NEVER, };
MemoryStream logStream = new MemoryStream();
int result = commandRunner.Run(stage.CommandEnvironment, logStream);
stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "command output:\n{0}", Encoding.UTF8.GetString(logStream.ToArray()));
stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "command exit code: {0}", result);
if (result != 0)
throw new Exception(String.Format("command exited with code {0} [0x{0:x}]", result));
return;
}
}
}

View File

@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using ln.build.commands;
using ln.build.semver;
using ln.json;
using ln.logging;
@ -14,6 +18,11 @@ namespace ln.build.pipeline
List<Stage> stages = new List<Stage>();
public IEnumerable<Stage> Stages => stages;
public Versioning Versioning { get; set; }
List<string> loadedTemplates = new List<string>();
public IEnumerable<string> LoadedTemplates => loadedTemplates;
public DefaultPipeLine(CIService ciService)
{
CIService = ciService;
@ -27,6 +36,29 @@ namespace ln.build.pipeline
public void LoadJson(JSONObject jsonPipeLine)
{
if (jsonPipeLine.ContainsKey("templates"))
{
foreach (JSONString jsonTemplateName in jsonPipeLine["templates"].Children)
{
if (!loadedTemplates.Contains(jsonTemplateName.Value))
{
loadedTemplates.Add(jsonTemplateName.Value);
string repoTemplate = Path.Combine(CommandEnvironment.WorkingDirectory, String.Format("{0}.ln", jsonTemplateName.Value));
string systemTemplate = Path.Combine(CIService.ContextDirectory, "scripts", "pipeline", String.Format("{0}.ln", jsonTemplateName.Value));
if (File.Exists(repoTemplate))
{
LoadJson(JSONParser.ParseFile(repoTemplate) as JSONObject);
} else if (File.Exists(systemTemplate))
{
LoadJson(JSONParser.ParseFile(systemTemplate) as JSONObject);
} else
throw new FileNotFoundException(String.Format("{0}.ln", jsonTemplateName.Value));
}
}
}
if (jsonPipeLine.ContainsKey("env"))
CommandEnvironment.Apply(jsonPipeLine["env"] as JSONObject);
@ -35,12 +67,13 @@ namespace ln.build.pipeline
JSONArray jsonStages = jsonPipeLine["stages"] as JSONArray;
foreach (JSONObject jsonStage in jsonStages.Children)
{
Stage stage = new Stage(this);
Stage stage = GetStage(jsonStage["name"].ToNative().ToString());
stage.LoadJson(jsonStage);
stages.Add(stage);
}
}
if (jsonPipeLine.ContainsKey("versioning"))
Versioning = new Versioning(jsonPipeLine["versioning"] as JSONObject);
}
public void Run()
@ -54,6 +87,19 @@ namespace ln.build.pipeline
}
}
public Stage GetStage(string stageName)
{
foreach (Stage stage in stages)
{
if (stageName.Equals(stage.Name))
return stage;
}
Stage _stage = new Stage(this);
_stage.Name = stageName;
stages.Add(_stage);
return _stage;
}
}
@ -61,11 +107,11 @@ namespace ln.build.pipeline
{
public DefaultPipeLine PipeLine { get; }
public string Name { get; set; }
public int Priority { get; set; }
public CommandEnvironment CommandEnvironment { get; }
List<StageCommand> commands = new List<StageCommand>();
public IEnumerable<StageCommand> Commands => commands;
public List<string> commands = new List<string>();
public Stage(DefaultPipeLine pipeLine)
{
@ -76,6 +122,8 @@ namespace ln.build.pipeline
public void LoadJson(JSONObject jsonStage)
{
Name = jsonStage["name"].ToNative().ToString();
if (jsonStage.ContainsKey("priority"))
Priority = (int)(long)jsonStage["priority"].ToNative();
if (jsonStage.ContainsKey("env"))
{
@ -86,7 +134,8 @@ namespace ln.build.pipeline
JSONArray jsonCommands = jsonStage["commands"] as JSONArray;
foreach (JSONValue jsonValue in jsonCommands.Children)
{
commands.Add(StageCommand.Create(jsonValue.ToNative().ToString()));
Logging.Log(LogLevel.DEBUG, "stage {0} command: {1}", Name, jsonValue.ToNative().ToString());
commands.Add(jsonValue.ToNative().ToString());
}
}
if (jsonStage.ContainsKey("secrets"))
@ -96,15 +145,14 @@ namespace ln.build.pipeline
{
CommandEnvironment.Set(key, CommandEnvironment.SecretStorage?.GetSecret(jsonSecrets[key]?.ToNative()?.ToString()));
}
}
}
public void Run()
{
foreach (StageCommand command in commands)
command.Run(this);
foreach (string command in commands)
PipeLine.CIService.StageCommands.Run(this, command.Split());
}
}

View File

@ -1,22 +1,26 @@
using System;
using System.IO;
using Jint.Native.Function;
using ln.build.repositories;
using ln.logging;
namespace ln.build.pipeline
{
public class ReleaseCommand : StageCommand
public static class DeployCommand
{
public string[] Arguments { get; }
public ReleaseCommand(string args) :base("RELEASE")
public static void Deploy(Stage stage,params string[] arguments)
{
Arguments = args.Split();
stage.CommandEnvironment.Logger.Log(LogLevel.WARNING, "stage command: deploy not yet implemented");
}
public override void Run(Stage stage)
public static void Release(Stage stage,params string[] arguments)
{
stage.CommandEnvironment.Logger.Log(LogLevel.WARNING, "stage command: release not yet implemented");
}
/*
if (stage.CommandEnvironment.Get("REPO_EVENT","").Equals("release"))
{
Repository repository = stage.CommandEnvironment.CIJob?.Repository;
@ -55,6 +59,6 @@ namespace ln.build.pipeline
} else {
stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "RELEASE: build is no release build. Ignoring.");
}
}
*/
}
}

View File

@ -1,26 +0,0 @@
using ln.build.pipeline;
namespace ln.build.commands
{
public class DotNetCommand : StageCommand
{
public DotNetCommand(string arguments) :base("DOTNET"){
}
public void Analyze(Stage stage)
{
}
public override void Run(Stage stage)
{
}
}
}

View File

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using ln.application;
using ln.build.pipeline;
using ln.build.semver;
using ln.build.support.dotnet;
using ln.logging;
using ln.type;
namespace ln.build.commands
{
public static class DotNetCommand
{
public static void Prepare(Stage stage,params string[] arguments)
{
if (Directory.Exists(".build"))
Directory.Delete(".build", true);
List<string> projectFiles = new List<string>();
List<string> slnFiles = new List<string>();
foreach (string argument in arguments)
{
foreach (string filename in PathHelper.ResolvePattern(argument, stage.CommandEnvironment.WorkingDirectory))
{
stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "dotnet prepare: found {0}", filename);
if (filename.EndsWith(".csproj",StringComparison.InvariantCultureIgnoreCase))
projectFiles.Add(filename);
else
slnFiles.Add(filename);
}
}
// ToDo: Parse .sln files for referenced projects
foreach (string projectFileName in projectFiles)
{
stage.PipeLine.CommandEnvironment.Extend("DOTNET_PROJECTS", projectFileName);
CSProjHelper csp = new CSProjHelper(projectFileName);
string projectName = csp.GetName();
SemVersion projectVersion= csp.GetVersion();
string ot = csp.GetOutputType();
stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "dotnet prepare: project {0} version={1} type={2}", projectName, projectVersion, ot);
}
}
public static void Build(Stage stage,params string[] arguments)
{
foreach (string projectFileName in stage.CommandEnvironment.Get("DOTNET_PROJECTS","").Split(':'))
{
new CommandRunner("dotnet","build", projectFileName, new CommandRunner.Option("-c", stage.CommandEnvironment.Get("DOTNET_CONFIGURATION")))
.Run(stage.CommandEnvironment);
}
}
public static void Pack(Stage stage,params string[] arguments)
{
foreach (string projectFileName in stage.CommandEnvironment.Get("DOTNET_PROJECTS","").Split(':'))
{
CSProjHelper csProject = new CSProjHelper(projectFileName);
if (!csProject.GetOutputType().Equals("Library"))
{
stage.CommandEnvironment.Logger.Log(LogLevel.WARNING, "dotnet pack: not packing {0} [{1}]",projectFileName, csProject.GetOutputType());
continue;
}
stage.CommandEnvironment.Logger.Log(LogLevel.WARNING, "dotnet pack: packing now {0} [{1}]",projectFileName, csProject.GetOutputType());
new CommandRunner("dotnet","pack", projectFileName)
.AddArgument(new CommandRunner.Option("-c", stage.CommandEnvironment.Get("DOTNET_CONFIGURATION")))
.AddArguments("-o", ".build/")
.Run(stage.CommandEnvironment);
string artefact = String.Format(".build/{0}.{1}.nupkg",csProject.GetName(), csProject.GetVersion());
if (!File.Exists(artefact))
throw new FileNotFoundException(artefact);
stage.PipeLine.CommandEnvironment.Extend("DOTNET_ARTEFACTS", artefact);
}
}
public static void Publish(Stage stage,params string[] arguments)
{
List<string> projectFileNames = new List<string>();
List<string> artefacts = new List<string>();
if (arguments.Length == 0)
projectFileNames.AddRange(stage.CommandEnvironment.Get("DOTNET_PROJECTS","").Split(':'));
else
projectFileNames.AddRange(arguments);
foreach (string projectFileName in projectFileNames)
{
CSProjHelper csProject = new CSProjHelper(projectFileName);
if (!csProject.GetOutputType().Equals("Exe"))
{
stage.CommandEnvironment.Logger.Log(LogLevel.WARNING, "dotnet publish: not publishing {0} [{1}]",projectFileName, csProject.GetOutputType());
continue;
}
stage.CommandEnvironment.Logger.Log(LogLevel.WARNING, "dotnet publish: publishing now {0} [{1}]",projectFileName, csProject.GetOutputType());
CommandRunner cr = new CommandRunner("dotnet","publish", projectFileName)
.AddArgument(new CommandRunner.Option("-c", stage.CommandEnvironment.Get("DOTNET_CONFIGURATION")))
.AddArguments("-p:PublishTrimmed=true","-p:PublishSingleFile=true","-p:PublishReadyToRun=false");
CommandRunner.Option outputOption = new CommandRunner.Option("-o", ".build/");
cr.AddArgument(outputOption);
string[] rids = stage.CommandEnvironment.Get("DOTNET_RIDS","").Split(':');
stage.CommandEnvironment.Logger.Log(LogLevel.INFO,"dotnet publish: using rids: {0}", string.Join(' ', rids));
if (rids.Length == 0)
{
cr.Run(stage.CommandEnvironment);
} else
{
CommandRunner.Option ridOption = new CommandRunner.Option("-r", "");
cr.AddArgument(ridOption);
cr.AddArgument("--self-contained");
foreach (string rid in rids)
{
ridOption.SetValue(rid);
outputOption.SetValue(String.Format(".build/{0}/", rid));
cr.Run(stage.CommandEnvironment);
string artefact = Directory.GetFiles(String.Format(".build/{0}/", rid)).Where((fn)=>!fn.EndsWith(".pdb")).FirstOrDefault();
if (artefact != null)
{
string ext = Path.GetExtension(artefact);
if (!ext.Equals(".exe"))
ext = "";
string finalName = string.Format(".build/{0}-{1}{2}",csProject.GetName(), rid, ext);
File.Move(artefact, finalName);
artefacts.Add(finalName);
stage.PipeLine.CommandEnvironment.Extend("DOTNET_ARTEFACTS", finalName);
Directory.Delete(String.Format(".build/{0}/", rid), true);
}
}
}
}
stage.CommandEnvironment.Logger.Log(LogLevel.INFO,"dotnet publish: created the following artefacts: {0}", string.Join(' ', artefacts));
}
public static void Test(Stage stage,params string[] arguments)
{
foreach (string projectFileName in stage.CommandEnvironment.Get("DOTNET_PROJECTS","").Split(':'))
{
new CommandRunner("dotnet","test", projectFileName, new CommandRunner.Option("-c", stage.CommandEnvironment.Get("DOTNET_CONFIGURATION")))
.Run(stage.CommandEnvironment);
}
}
public static void Push(Stage stage,params string[] arguments)
{
if (stage.CommandEnvironment.SecretStorage == null)
{
stage.CommandEnvironment.Logger.Log(LogLevel.WARNING,"dotnet push: no SecretStorage available, push operations are going to fail without authorization!");
} else {
stage.CommandEnvironment.Logger.Log(LogLevel.INFO,"dotnet push: using secrets from {0}", stage.CommandEnvironment.SecretStorage.FileName);
}
string[] nupkgList = stage.CommandEnvironment.Get("DOTNET_ARTEFACTS","").Split(':').Where((fn)=>fn.EndsWith(".nupkg")).ToArray();
string[] binaryList = stage.CommandEnvironment.Get("DOTNET_ARTEFACTS","").Split(':').Where((fn)=>!fn.EndsWith(".nupkg")).ToArray();
if (nupkgList.Length > 0)
{
string nugetSource = stage.CommandEnvironment.Get("NUGET_SOURCE");
string nugetApiKey = null;
if (!(stage.CommandEnvironment.SecretStorage?.TryGetSecret(nugetSource, out nugetApiKey) ?? false))
{
URI uriNuget = new URI(nugetSource);
stage.CommandEnvironment.SecretStorage?.TryGetSecret(new URI(uriNuget.Scheme, uriNuget.Authority, "").ToString(), out nugetApiKey);
}
CommandRunner cr = new CommandRunner("dotnet", "nuget", "push");
CommandRunner.Argument argNupkg = new CommandRunner.Argument("");
cr.AddArgument(argNupkg);
CommandRunner.Option optSource = new CommandRunner.Option("-s",nugetSource);
cr.AddArgument(optSource);
CommandRunner.Option optApiKey = new CommandRunner.Option("-k", nugetApiKey);
cr.AddArgument(optApiKey);
foreach (string nupkg in nupkgList)
{
argNupkg.SetValue(nupkg);
cr.Run(stage.CommandEnvironment);
}
}
}
}
}

View File

@ -1,40 +0,0 @@
using System;
using System.IO;
using System.Text;
using ln.build.commands;
using ln.logging;
namespace ln.build.pipeline
{
public class ShellCommand : StageCommand
{
public event CommandExitedDelegate OnCommandExited;
public CommandRunner CommandRunner { get; }
public ShellCommand(string filename,params CommandRunner.Argument[] arguments)
:base("SH")
{
CommandRunner = new CommandRunner("/bin/bash", "-c"){ Throw = CRThrow.NEVER, };
CommandRunner.AddArguments(arguments);
}
public ShellCommand(string cmdline)
:base("SH")
{
CommandRunner = new CommandRunner("/bin/bash", "-c", string.Format("\"{0}\"",cmdline)){ Throw = CRThrow.NEVER, };
}
public override void Run(Stage stage)
{
MemoryStream logStream = new MemoryStream();
int result = CommandRunner.Run(stage.CommandEnvironment, logStream);
stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "command output:\n{0}", Encoding.UTF8.GetString(logStream.ToArray()));
stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "command exit code: {0}", result);
if (result != 0)
throw new Exception(String.Format("command exited with code {0} [0x{0:x}]", result));
}
}
}

View File

@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using ln.build.commands;
namespace ln.build.pipeline
{
public abstract class StageCommand
{
public string Name { get; }
public StageCommand(string name)
{
Name = name;
}
public abstract void Run(Stage stage);
static Dictionary<string,Func<string,StageCommand>> commandFactories = new Dictionary<string, Func<string, StageCommand>>();
public static StageCommand Create(string cmdline)
{
string[] tokens = cmdline.Split(new char[]{' ','\t'}, 2);
if (commandFactories.TryGetValue(tokens[0],out Func<string,StageCommand> factory))
{
return factory(tokens[1]);
}
throw new Exception(string.Format("can't find factory for command keyword '{0}'", tokens[0]));
}
static StageCommand()
{
commandFactories.Add("SH", (args) => new ShellCommand(args));
commandFactories.Add("DOTNET", (args) => new DotNetCommand(args));
commandFactories.Add("GITEA", (args) => new DotNetCommand(args));
commandFactories.Add("RELEASE", (args) => new ReleaseCommand(args));
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using ln.build.commands;
using ln.type;
namespace ln.build.pipeline
{
public class StageCommandContainer
{
public StageCommandContainer Parent { get; }
public string Name { get; }
public string FullName => Parent?.FullName != null ? string.Format("{0} {1}", Parent.FullName, Name) : Name;
Dictionary<string,Action<Stage,string[]>> commands = new Dictionary<string, Action<Stage,string[]>>();
Dictionary<string,StageCommandContainer> children = new Dictionary<string, StageCommandContainer>();
public StageCommandContainer(StageCommandContainer parent, string name)
{
Parent = parent;
Name = name;
}
public void AddCommand(Action<Stage,string[]> commandAction, string commandPath) => AddCommand(commandAction, commandPath.Split());
public void AddCommand(Action<Stage,string[]> commandAction, params string[] commandPath)
{
if (commandPath.Length == 0)
throw new ArgumentException(nameof(commandPath));
if (commandPath.Length > 1)
{
if (!children.TryGetValue(commandPath[0], out StageCommandContainer childContainer))
{
childContainer = new StageCommandContainer(this, commandPath[0]);
children.Add(commandPath[0], childContainer);
}
childContainer.AddCommand(commandAction, commandPath.Slice(1));
} else {
commands.Add(commandPath[0], commandAction);
}
}
public void Run(Stage stage, params string[] arguments)
{
if (commands.TryGetValue(arguments[0], out Action<Stage,string[]> commandAction))
commandAction(stage, arguments.Slice(1));
else if (children.TryGetValue(arguments[0], out StageCommandContainer childContainer))
childContainer.Run(stage, arguments.Slice(1));
else
throw new ArgumentException(String.Format("command not found: {0}", arguments[0]));
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.IO;
using System.Linq;
using ln.build.semver;
using ln.build.semver.provider;
using ln.json;
namespace ln.build.pipeline
{
public class Versioning
{
public string ProviderName { get; set; }
public string[] Sources { get; set; }
public Provider Provider { get; }
public Versioning()
{}
public Versioning(JSONObject jsonVersioning)
{
ProviderName = jsonVersioning["provider"].ToNative().ToString();
if (jsonVersioning.ContainsKey("sources"))
Sources = jsonVersioning["sources"].Children.Select((s) => s.ToNative().ToString()).ToArray();
Provider = Provider.CreateProvider(ProviderName);
}
public Versioning(string providerName)
{
ProviderName = providerName;
Provider = Provider.CreateProvider(ProviderName);
}
public SemVersion GetCurrentVersion(CIJob job) => GetVersion(job, Sources[0]);
public SemVersion GetVersion(CIJob job, string source)
{
string sourceFileName = Path.Combine(job?.WorkingDirectory ?? "", source);
return Provider.GetVersion(sourceFileName);
}
public void SetVersion(CIJob job, SemVersion version)
{
foreach (string source in Sources)
SetVersion(job, source, version);
}
public void SetVersion(CIJob job, string source, SemVersion version)
{
string sourceFileName = Path.Combine(job?.WorkingDirectory ?? "", source);
Provider.SetVersion(sourceFileName, version);
}
}
}

View File

@ -13,6 +13,10 @@ namespace ln.build.repositories
public abstract Release[] GetReleases();
public abstract Release GetRelease(string tagName);
public abstract Release GetRelease(int id);
public abstract void CommitAndPush(string message, string[] addedPaths, string[] modifiedPaths, string[] removedPaths);
}

View File

@ -1,11 +1,17 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using ln.build.commands;
using ln.build.pipeline;
using ln.build.secrets;
using ln.build.semver;
using ln.http;
using ln.json;
using ln.logging;
using ln.threading;
using ln.type;
namespace ln.build.repositories.gitea
{
@ -20,6 +26,8 @@ namespace ln.build.repositories.gitea
public JsonApiClient Client { get; }
string AccessToken { get; set; }
public GiteaRepository(string baseURL, string owner, string name)
:base(string.Format("{0}/{1}/{2}.git",baseURL, owner, name))
{
@ -34,6 +42,7 @@ namespace ln.build.repositories.gitea
public GiteaRepository(string baseURL, string owner, string name, string accessToken)
:this(baseURL, owner, name)
{
AccessToken = accessToken;
if (accessToken != null)
Client.HttpClient.DefaultRequestHeaders.Add("Authorization",String.Format("token {0}", accessToken));
}
@ -93,6 +102,27 @@ namespace ln.build.repositories.gitea
public bool ContainsFile(string filename,string _ref) => (Client.GetJson(out JSONValue response, "repos", Owner, Name, "contents", string.Format("{0}?ref={1}",filename, _ref)) == HttpStatusCode.OK);
public override void CommitAndPush(string message, string[] addedPaths, string[] modifiedPaths, string[] removedPaths)
{
CommandRunner cr = new CommandRunner("git","add");
cr.AddArguments(addedPaths);
cr.AddArguments(modifiedPaths);
cr.Run();
cr = new CommandRunner("git","rm");
cr.AddArguments(removedPaths);
cr.Run();
cr = new CommandRunner("git","commit", "-m", message);
cr.Run();
URI baseUri = new URI(BaseURL);
if (!AccessToken?.Equals(String.Empty) ?? false)
baseUri = baseUri.WithUserInfo(AccessToken);
cr = new CommandRunner("git","push", baseUri.ToString(true));
cr.Run();
}
public static HttpResponse WebHookHandler(CIService ciService, HttpRequest request)
@ -135,6 +165,7 @@ namespace ln.build.repositories.gitea
string eventType = request.GetRequestHeader("X-GITEA-EVENT");
List<string> buildRefs = new List<string>();
JSONObject jsonCommit = null;
switch (eventType)
{
@ -145,8 +176,16 @@ namespace ln.build.repositories.gitea
buildRefs.Add(message["release"]["tag_name"].ToNative().ToString());
break;
case "push":
foreach (JSONObject jsonCommit in (message["commits"] as JSONArray).Children)
buildRefs.Add(jsonCommit["id"].ToNative().ToString());
string currentCommitId = message["after"].ToNative().ToString();
foreach (JSONObject _jsonCommit in (message["commits"] as JSONArray).Children)
{
if (currentCommitId.Equals(_jsonCommit["id"].ToNative().ToString()))
{
buildRefs.Add(_jsonCommit["id"].ToNative().ToString());
jsonCommit = _jsonCommit;
break;
}
}
break;
default:
Logging.Log(LogLevel.WARNING, "received webhook with unsupported event type [{0}]", eventType);
@ -162,6 +201,7 @@ namespace ln.build.repositories.gitea
.SetVariable("REPO_OWNER", owner)
.SetVariable("REPO_NAME", repoName)
.SetVariable("REPO_REF", _ref)
.SetVariable("REPO_BASE", giteaRepository.BaseURL)
// .SetVariable("NOTIFY", message["pusher"]["email"].ToNative().ToString())
;
ciJob.Environment.SecretStorage = secretStorage;
@ -173,6 +213,16 @@ namespace ln.build.repositories.gitea
ciJob.SetVariable("RELEASE_ID", message["release"]["id"].ToNative().ToString());
break;
case "push":
if (jsonCommit != null)
{
string commitMessage = jsonCommit["message"].ToNative().ToString();
if (commitMessage.Contains("#ReleaseMajor"))
ciJob.OnJobCompleted += (job) => job.PublishRelease(SemVerLevels.MAJOR);
else if (commitMessage.Contains("#ReleaseMinor"))
ciJob.OnJobCompleted += (job) => job.PublishRelease(SemVerLevels.MINOR);
else if (commitMessage.Contains("#ReleasePatch"))
ciJob.OnJobCompleted += (job) => job.PublishRelease(SemVerLevels.PATCH);
}
break;
}

View File

@ -0,0 +1,45 @@
{
"env": {
"CONFIGURATION": "Release",
"DOTNET_RIDS": "linux-x64:win-x64:osx-x64"
},
"stages": [
{
"name": "prepare",
"priority": 100,
"commands": [
"dotnet prepare *.sln *.csproj"
]
},
{
"name": "build",
"priority": 300,
"commands": [
"dotnet build"
]
},
{
"name": "test",
"priority": 500,
"commands": [
"dotnet test"
]
},
{
"name": "pack",
"priority": 700,
"commands": [
"dotnet pack",
"dotnet publish"
]
},
{
"name": "push",
"priority": 1000,
"commands": [
"dotnet push",
"deploy"
]
}
]
}

View File

@ -28,6 +28,7 @@ namespace ln.build.secrets
JSONObject secretsObject = JSONParser.ParseFile(FileName) as JSONObject;
foreach (string key in secretsObject.Keys)
{
//Logging.Log(LogLevel.INFO, "loading secret {0}", key);
secrets.Add(key, secretsObject[key].ToNative().ToString());
}
}
@ -36,9 +37,15 @@ namespace ln.build.secrets
public string GetSecret(string key)
{
//Logging.Log(LogLevel.INFO, "trying to fetch secret for: [{0}]", key);
TryGetSecret(key, out string secret);
return secret;
}
public bool TryGetSecret(string key, out string secret) => secrets.TryGetValue(key, out secret);
public bool TryGetSecret(string key, out string secret){
//Logging.Log(LogLevel.INFO, "trying to fetch secret for: [{0}]", key);
bool success = secrets.TryGetValue(key, out secret);
//Logging.Log(LogLevel.INFO, "{1} to fetch secret for: [{0}]", key, success ? "succeded" : "failed");
return success;
}
}
}

View File

@ -0,0 +1,11 @@
namespace ln.build.semver
{
public enum SemVerLevels {
MAJOR,
MINOR,
PATCH,
TAG
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using ln.build.semver;
using Microsoft.VisualBasic;
namespace ln.build.semver
{
public class SemVersion
{
public int Major { get; set; }
public int Minor { get; set; }
public int Patch { get; set; }
public string PreRelease { get; set; } = String.Empty;
public bool IsPreRelease => !PreRelease?.Equals(string.Empty) ?? false;
public bool IsRelease => PreRelease?.Equals(string.Empty) ?? true;
public SemVersion(int major,int minor,int patch,string prerelease)
{
Major = major;
Minor = minor;
Patch = patch;
PreRelease = prerelease;
}
private SemVersion(){}
public SemVersion(SemVersion source)
:this(source.Major, source.Minor, source.Patch, source.PreRelease)
{ }
public void Increment(SemVerLevels level){
switch (level)
{
case SemVerLevels.MAJOR:
Major++;
Minor = 0;
Patch = 0;
break;
case SemVerLevels.MINOR:
Minor++;
Patch = 0;
break;
case SemVerLevels.PATCH:
Patch++;
break;
}
}
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendFormat("{0}.{1}.{2}", Major, Minor, Patch);
if (!PreRelease?.Equals(string.Empty) ?? false)
stringBuilder.AppendFormat("-{0}", PreRelease);
return stringBuilder.ToString();
}
public static Regex rexVersion = new Regex("^(?<major>\\d+).(?<minor>\\d+).(?<patch>\\d+)(-(?<prerelease>.+))?$");
public static SemVersion Parse(string versionString)
{
Match match = rexVersion.Match(versionString);
if (!match.Success)
throw new FormatException(String.Format("{0} is no valid SemVer", versionString));
SemVersion version = new SemVersion();
version.Major = int.Parse(match.Groups["major"].Value);
version.Minor = int.Parse(match.Groups["minor"].Value);
version.Patch = int.Parse(match.Groups["patch"].Value);
if (match.Groups["prerelease"].Success)
version.PreRelease = match.Groups["prerelease"].Value;
return version;
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Xml;
using ln.build.semver;
namespace ln.build.semver.provider
{
public class DotNetProvider : Provider
{
public DotNetProvider(): base("dotnet")
{}
public override SemVersion GetVersion(string source)
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(source);
XmlNode nodeVersion = projectFile.SelectSingleNode("Project/PropertyGroup/Version");
return SemVersion.Parse(nodeVersion.InnerText);
}
public override void SetVersion(string source, SemVersion version)
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(source);
XmlNode nodeVersion = projectFile.SelectSingleNode("Project/PropertyGroup/Version");
nodeVersion.InnerText = version.ToString();
projectFile.Save(source);
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Diagnostics.Contracts;
using System.IO;
namespace ln.build.semver.provider
{
public abstract class Provider
{
public String Name { get; }
public Provider(string providerName)
{
Name = providerName;
}
public abstract SemVersion GetVersion(string source);
public abstract void SetVersion(string source, SemVersion version);
public static Provider CreateProvider(string providerName)
{
switch (providerName)
{
case "dotnet":
return new DotNetProvider();
default:
throw new FileNotFoundException();
}
}
}
}

View File

@ -0,0 +1,66 @@
using System.IO;
using System.Xml;
using ln.build.semver;
namespace ln.build.support.dotnet
{
public class CSProjHelper
{
public string FileName { get; set; }
public CSProjHelper(string filename)
{
FileName = filename;
}
public string GetName()
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(FileName);
XmlNode nodeVersion = projectFile.SelectSingleNode("Project/PropertyGroup/AssemblyName");
return nodeVersion?.InnerText ?? Path.GetFileNameWithoutExtension(FileName);
}
public SemVersion GetVersion()
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(FileName);
XmlNode nodeVersion = projectFile.SelectSingleNode("Project/PropertyGroup/Version");
return SemVersion.Parse(nodeVersion.InnerText);
}
public void SetVersion(SemVersion version)
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(FileName);
XmlNode nodeVersion = projectFile.SelectSingleNode("Project/PropertyGroup/Version");
nodeVersion.InnerText = version.ToString();
projectFile.Save(FileName);
}
public bool IsPackable()
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(FileName);
XmlNode nodePackable = projectFile.SelectSingleNode("Project/PropertyGroup/IsPackable");
return bool.Parse(nodePackable?.InnerText ?? "true");
}
public string GetOutputType()
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(FileName);
XmlNode nodeVersion = projectFile.SelectSingleNode("Project/PropertyGroup/OutputType");
return nodeVersion?.InnerText ?? "Library";
}
}
}