diff --git a/build.ln b/build.ln index db60fbc..3364810 100644 --- a/build.ln +++ b/build.ln @@ -39,7 +39,7 @@ { "name": "push", "commands": [ - "SH dotnet nuget push .build/ln.build.*.nupkg -s $NUGET_SOURCE -k $NUGET_APIKEY", + "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" ], "secrets": { diff --git a/ln.build.server/Program.cs b/ln.build.server/Program.cs index 7074d43..03678f5 100644 --- a/ln.build.server/Program.cs +++ b/ln.build.server/Program.cs @@ -22,6 +22,9 @@ namespace ln.build.server [StaticArgument( LongOption = "build")] public static string BuildPath { get; set; } + [StaticArgument( LongOption = "build-secret")] + public static string BuildSecret { get; set; } + static void Main(string[] args) { ArgumentContainer ac = new ArgumentContainer(typeof(Program)); @@ -34,7 +37,7 @@ namespace ln.build.server if (BuildPath != null) { - CIJob job = new CIJob(CIService,null); + CIJob job = new CIJob(CIService,null, (BuildSecret != null) ? CIService.GetSecretStorage(BuildSecret) : null); job.WorkingDirectory = BuildPath; job.RunJob(); } else { diff --git a/ln.build.server/ln.build.server.csproj b/ln.build.server/ln.build.server.csproj index 5302868..186c26b 100644 --- a/ln.build.server/ln.build.server.csproj +++ b/ln.build.server/ln.build.server.csproj @@ -6,7 +6,7 @@ - 0.3.0 + 0.4.0 Harald Wolff-Thobaben l--n.de A simple build server scheduling builds triggered via web-hooks diff --git a/ln.build/CIJob.cs b/ln.build/CIJob.cs index b6d6deb..b7f4d92 100644 --- a/ln.build/CIJob.cs +++ b/ln.build/CIJob.cs @@ -40,6 +40,7 @@ namespace ln.build :this(ciService, repository) { SecretStorage = secretStorage; + Environment.SecretStorage = secretStorage; } public CIJob(CIService ciService, Repository repository) { @@ -52,7 +53,7 @@ namespace ln.build Logger = new Logger(new FileLogger(Path.Combine(ciService.ReportsDirectory, JobID, "build.log"))); Logger.Backends.Add(Logger.ConsoleLogger); - Environment = new CommandEnvironment(); + Environment = new CommandEnvironment(){ CIJob = this }; } public Stream GetLogStream(string name) diff --git a/ln.build/CIService.cs b/ln.build/CIService.cs index 1593a7c..c4e6aa3 100644 --- a/ln.build/CIService.cs +++ b/ln.build/CIService.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.NetworkInformation; using ln.application; using ln.build.repositories; +using ln.build.secrets; using ln.http; using ln.http.router; using ln.json; @@ -131,6 +132,8 @@ namespace ln.build hookRouter.AddSimpleRoute(String.Format("/{0}", name), (HttpRoutingContext context,HttpRequest request) => webHookHandler(this, request)); } + public SecretStorage GetSecretStorage(string secretName) => new SecretStorage(Path.Combine(BaseDirectory,"secrets", String.Format("{0}.json", secretName))); + public void Enqueue(CIJob job) { diff --git a/ln.build/JsonApiClient.cs b/ln.build/JsonApiClient.cs index c2f589b..c8caecc 100644 --- a/ln.build/JsonApiClient.cs +++ b/ln.build/JsonApiClient.cs @@ -47,12 +47,30 @@ namespace ln.build } return httpResponse.StatusCode; } - + + public HttpStatusCode PostContent(HttpContent content, out JSONValue response,params string[] path) + { + HttpResponseMessage httpResponse = HttpClient.PostAsync(string.Format("{0}/{1}",BaseURL, string.Join('/', path)),content).Result; + if (httpResponse.StatusCode == System.Net.HttpStatusCode.Created) + { + response = JSONParser.Parse(httpResponse.Content.ReadAsStringAsync().Result); + } else { + response = null; + Logging.Log(LogLevel.DEBUG, "failed URL: {0}", httpResponse.RequestMessage.RequestUri); + Logging.Log(LogLevel.DEBUG, "httpResponse; {0}", httpResponse); + } + return httpResponse.StatusCode; + } + public void Dispose() { HttpClient?.Dispose(); } + + static JsonApiClient(){ + System.Net.ServicePointManager.Expect100Continue = false; + } } } \ No newline at end of file diff --git a/ln.build/commands/CommandEnvironment.cs b/ln.build/commands/CommandEnvironment.cs index b1aac79..405712c 100644 --- a/ln.build/commands/CommandEnvironment.cs +++ b/ln.build/commands/CommandEnvironment.cs @@ -29,6 +29,9 @@ namespace ln.build.commands SecretStorage secretStorage; public SecretStorage SecretStorage { get => secretStorage ?? Parent?.SecretStorage; set => secretStorage = value; } + + CIJob cIJob; + public CIJob CIJob { get => cIJob ?? Parent?.CIJob ; set => cIJob = value; } public CommandEnvironment(CommandEnvironment parent) : this(parent.Logger) { Parent = parent; WorkingDirectory = parent.WorkingDirectory; } public CommandEnvironment(CommandEnvironment parent, Logger logger) : this(logger) { Parent = parent; WorkingDirectory = parent.WorkingDirectory; } diff --git a/ln.build/ln.build.csproj b/ln.build/ln.build.csproj index 1352265..a0305de 100644 --- a/ln.build/ln.build.csproj +++ b/ln.build/ln.build.csproj @@ -5,7 +5,7 @@ - 0.3.0 + 0.4.0 Harald Wolff-Thobaben l--n.de A simple build server scheduling builds triggered via web-hooks diff --git a/ln.build/pipeline/DefaultPipeLine.cs b/ln.build/pipeline/DefaultPipeLine.cs index 885e6c3..dadcfa5 100644 --- a/ln.build/pipeline/DefaultPipeLine.cs +++ b/ln.build/pipeline/DefaultPipeLine.cs @@ -1,9 +1,4 @@ - - -using System; using System.Collections.Generic; -using System.IO; -using System.Text; using ln.build.commands; using ln.json; using ln.logging; @@ -113,71 +108,4 @@ 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> commandFactories = new Dictionary>(); - - public static StageCommand Create(string cmdline) - { - string[] tokens = cmdline.Split(new char[]{' ','\t'}, 2); - - if (commandFactories.TryGetValue(tokens[0],out Func 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)); - - } - - } - - 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)); - } - } - } \ No newline at end of file diff --git a/ln.build/pipeline/ReleaseCommand.cs b/ln.build/pipeline/ReleaseCommand.cs new file mode 100644 index 0000000..5711171 --- /dev/null +++ b/ln.build/pipeline/ReleaseCommand.cs @@ -0,0 +1,53 @@ + +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 string[] Arguments { get; } + public ReleaseCommand(string args) :base("RELEASE") + { + Arguments = args.Split(); + } + + public override void Run(Stage stage) + { + if (stage.CommandEnvironment.Get("REPO_EVENT","").Equals("release")) + { + Repository repository = stage.CommandEnvironment.CIJob?.Repository; + if (repository != null) + { + Release release = repository.GetRelease(int.Parse(stage.CommandEnvironment.Get("RELEASE_ID"))); + + foreach (string arg in Arguments) + { + string localPath, remoteFileName; + int ie = arg.IndexOf('='); + if (ie == -1) + { + localPath = Path.Combine(stage.CommandEnvironment.WorkingDirectory, arg); + remoteFileName = Path.GetFileName(arg); + } else { + localPath = Path.Combine(stage.CommandEnvironment.WorkingDirectory, arg.Substring(0, ie)); + remoteFileName = arg.Substring(ie + 1); + } + + stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "Adding {0} to release as {1}", localPath, remoteFileName); + release.AddAttachement(localPath, remoteFileName); + } + + } else { + stage.CommandEnvironment.Logger.Log(LogLevel.ERROR, "RELEASE: no repository interface found!"); + throw new Exception("Respository needed!"); + } + } else { + stage.CommandEnvironment.Logger.Log(LogLevel.INFO, "RELEASE: build is no release build. Ignoring."); + } + } + } +} \ No newline at end of file diff --git a/ln.build/pipeline/ShellCommand.cs b/ln.build/pipeline/ShellCommand.cs new file mode 100644 index 0000000..895c320 --- /dev/null +++ b/ln.build/pipeline/ShellCommand.cs @@ -0,0 +1,40 @@ + + +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)); + } + } + +} \ No newline at end of file diff --git a/ln.build/pipeline/StageCommand.cs b/ln.build/pipeline/StageCommand.cs new file mode 100644 index 0000000..d0462d7 --- /dev/null +++ b/ln.build/pipeline/StageCommand.cs @@ -0,0 +1,48 @@ + + +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> commandFactories = new Dictionary>(); + + public static StageCommand Create(string cmdline) + { + string[] tokens = cmdline.Split(new char[]{' ','\t'}, 2); + + if (commandFactories.TryGetValue(tokens[0],out Func 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)); + + } + + } + +} \ No newline at end of file diff --git a/ln.build/repositories/Release.cs b/ln.build/repositories/Release.cs index 6df1e85..4372f1d 100644 --- a/ln.build/repositories/Release.cs +++ b/ln.build/repositories/Release.cs @@ -1,5 +1,6 @@ using System; +using System.Net.Http.Headers; namespace ln.build.repositories { @@ -16,7 +17,8 @@ namespace ln.build.repositories public abstract Repository Repository { get; } public abstract Attachement[] GetAttachements(); - public abstract void AddAttachement(Attachement attachement,string localPath); + + public abstract void AddAttachement(string localPath,string remoteFilename); diff --git a/ln.build/repositories/Repository.cs b/ln.build/repositories/Repository.cs index 0622ea9..91c5259 100644 --- a/ln.build/repositories/Repository.cs +++ b/ln.build/repositories/Repository.cs @@ -12,6 +12,7 @@ namespace ln.build.repositories public abstract Release[] GetReleases(); public abstract Release GetRelease(string tagName); + public abstract Release GetRelease(int id); } diff --git a/ln.build/repositories/gitea/GiteaRelease.cs b/ln.build/repositories/gitea/GiteaRelease.cs index 776d52e..b086a7f 100644 --- a/ln.build/repositories/gitea/GiteaRelease.cs +++ b/ln.build/repositories/gitea/GiteaRelease.cs @@ -1,5 +1,10 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; using ln.json; +using ln.type; namespace ln.build.repositories.gitea { @@ -13,7 +18,7 @@ namespace ln.build.repositories.gitea } public GiteaRelease(GiteaRepository repository,JSONObject jsonRelease) : this(repository) { - Id = (int)jsonRelease["id"].ToNative(); + Id = (int)(long)jsonRelease["id"].ToNative(); Name = jsonRelease["name"].ToNative().ToString(); TagName = jsonRelease["tag_name"].ToNative().ToString(); Body = jsonRelease["body"].ToNative().ToString(); @@ -21,9 +26,18 @@ namespace ln.build.repositories.gitea IsPreRelease = (bool)jsonRelease["prerelease"].ToNative(); } - public override void AddAttachement(Attachement attachement, string localPath) + public override void AddAttachement(string localPath, string remoteFileName) { - throw new System.NotImplementedException(); + using (FileStream fs = new FileStream(localPath,FileMode.Open)) + { + StreamContent attachmentBytes = new StreamContent(fs); + MultipartFormDataContent formDataContent = new MultipartFormDataContent(); + formDataContent.Add(attachmentBytes, "attachment", remoteFileName); + if (HttpStatusCode.Created != GiteaRepository.Client.PostContent(formDataContent,out JSONValue response, "repos", GiteaRepository.Owner, GiteaRepository.Name, "releases", Id.ToString(), String.Format("assets?name={0}", remoteFileName))) + { + throw new Exception(String.Format("could not create attachment to release: {0}", localPath)); + } + } } public override Attachement[] GetAttachements() diff --git a/ln.build/repositories/gitea/GiteaRepository.cs b/ln.build/repositories/gitea/GiteaRepository.cs index 130dccf..408ba1d 100644 --- a/ln.build/repositories/gitea/GiteaRepository.cs +++ b/ln.build/repositories/gitea/GiteaRepository.cs @@ -29,7 +29,7 @@ namespace ln.build.repositories.gitea Owner = owner; Name = name; - Client = new JsonApiClient(baseURL); + Client = new JsonApiClient(string.Format("{0}/api/v1",baseURL)); } public GiteaRepository(string baseURL, string owner, string name, string accessToken) :this(baseURL, owner, name) @@ -49,7 +49,7 @@ namespace ln.build.repositories.gitea if (HttpStatusCode.Created != Client.PostJson( stateObject, out JSONValue response, - "api/v1/repos", job.GetVariable("REPO_OWNER"), job.GetVariable("REPO_NAME"), "statuses", job.GetVariable("REPO_REF") + "repos", job.GetVariable("REPO_OWNER"), job.GetVariable("REPO_NAME"), "statuses", job.GetVariable("REPO_REF") )) { job.Logger.Log(LogLevel.ERROR, "{0}: UpdateBuildState(): could not post state", GetType().Name); @@ -58,14 +58,9 @@ namespace ln.build.repositories.gitea public override Release[] GetReleases() { - string releasesUrl = string.Format("{0}/api/v1/repos/{1}/{2}/releases", - BaseURL, - Owner, - Name - ); if (HttpStatusCode.OK == Client.GetJson( out JSONValue jsonReleases, - "api/v1/repos", Owner, "releases" + "repos", Owner, Name, "releases" )) { List releases = new List(); @@ -86,9 +81,17 @@ namespace ln.build.repositories.gitea { throw new NotImplementedException(); } + public override Release GetRelease(int releaseId) + { + if (HttpStatusCode.OK == Client.GetJson(out JSONValue jsonRelease, "repos", Owner, Name, "releases", releaseId.ToString())) + { + return new GiteaRelease(this, jsonRelease as JSONObject); + } + return null; + } - public bool ContainsFile(string filename,string _ref) => (Client.GetJson(out JSONValue response, "/api/v1/repos", Owner, Name, "contents", string.Format("{0}?ref={1}",filename, _ref)) == HttpStatusCode.OK); + 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); @@ -125,7 +128,7 @@ namespace ln.build.repositories.gitea string giteaBaseUrl = htmlUrl.Remove(htmlUrl.Length - repoPath.Length); - SecretStorage secretStorage = new SecretStorage(Path.Combine(ciService.BaseDirectory,"secrets", String.Format("{0}.json", hookSecret))); + SecretStorage secretStorage = ciService.GetSecretStorage(hookSecret); GiteaRepository giteaRepository = new GiteaRepository(giteaBaseUrl, owner, repoName, secretStorage.GetSecret(giteaBaseUrl)){ SecretStorage = secretStorage, }; @@ -156,10 +159,21 @@ namespace ln.build.repositories.gitea .SetVariable("REPO_OWNER", owner) .SetVariable("REPO_NAME", repoName) .SetVariable("REPO_REF", _ref) - .SetVariable("NOTIFY", message["pusher"]["email"].ToNative().ToString()) +// .SetVariable("NOTIFY", message["pusher"]["email"].ToNative().ToString()) ; ciJob.Environment.SecretStorage = secretStorage; + switch (eventType) + { + case "release": + ciJob.SetVariable("RELEASE_TAGNAME", message["release"]["tag_name"].ToNative().ToString()); + ciJob.SetVariable("RELEASE_ID", message["release"]["id"].ToNative().ToString()); + break; + case "push": + break; + } + + ciJob.UpdateBuildState(BuildState.PENDING); ciService.Enqueue(ciJob); }