mirror of
https://github.com/Smaug123/gitea-repo-config
synced 2025-10-05 15:38:41 +00:00
Subcommands (#62)
This commit is contained in:
@@ -447,3 +447,12 @@ module GiteaConfig =
|
||||
|
||||
JsonConvert.DeserializeObject<SerialisedGiteaConfig> s
|
||||
|> GiteaConfig.OfSerialised
|
||||
|
||||
let getSchema () : Stream =
|
||||
let resource = "Gitea.Declarative.Lib.GiteaConfig.schema.json"
|
||||
let assembly = System.Reflection.Assembly.GetExecutingAssembly ()
|
||||
let stream = assembly.GetManifestResourceStream resource
|
||||
|
||||
match stream with
|
||||
| null -> failwithf "The resource %s was not found. This is a bug in the tool." resource
|
||||
| stream -> stream
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<Compile Include="Array.fs" />
|
||||
<Compile Include="UserInput.fs" />
|
||||
<Compile Include="Gitea.fs" />
|
||||
<Content Include="GiteaConfig.schema.json" />
|
||||
<EmbeddedResource Include="GiteaConfig.schema.json" />
|
||||
<Content Include="swagger.v1.json" />
|
||||
<EmbeddedResource Include="version.json" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="/" />
|
||||
|
@@ -18,6 +18,8 @@ module TestSchema =
|
||||
let ``Example conforms to schema`` () =
|
||||
let executing = Assembly.GetExecutingAssembly().Location |> FileInfo
|
||||
|
||||
// We choose to refer to the path specifically here, so that the "Update" functionality
|
||||
// below can't be broken by an undetected file rename.
|
||||
let schemaFile =
|
||||
Utils.findFileAbove "Gitea.Declarative.Lib/GiteaConfig.schema.json" executing.Directory
|
||||
|
||||
@@ -37,6 +39,13 @@ module TestSchema =
|
||||
let jsonFile = Utils.findFileAbove "GiteaConfig.json" executing.Directory
|
||||
GiteaConfig.get jsonFile |> ignore
|
||||
|
||||
[<Test>]
|
||||
let ``Schema can be output`` () =
|
||||
use schema = GiteaConfig.getSchema ()
|
||||
let reader = new StreamReader (schema)
|
||||
let schema = reader.ReadToEnd ()
|
||||
schema.Contains "SerialisedGiteaConfig" |> shouldEqual true
|
||||
|
||||
[<Test>]
|
||||
[<Explicit "Run this to regenerate the schema file">]
|
||||
let ``Update schema file`` () =
|
||||
|
20
Gitea.Declarative/ArgsCrate.fs
Normal file
20
Gitea.Declarative/ArgsCrate.fs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Gitea.Declarative
|
||||
|
||||
open Argu
|
||||
|
||||
type ArgsEvaluator<'ret> =
|
||||
abstract Eval<'a, 'b when 'b :> IArgParserTemplate> :
|
||||
(ParseResults<'b> -> Result<'a, ArguParseException>) -> ('a -> Async<int>) -> 'ret
|
||||
|
||||
type ArgsCrate =
|
||||
abstract Apply<'ret> : ArgsEvaluator<'ret> -> 'ret
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module ArgsCrate =
|
||||
let make<'a, 'b when 'b :> IArgParserTemplate>
|
||||
(ofResult : ParseResults<'b> -> Result<'a, ArguParseException>)
|
||||
(run : 'a -> Async<int>)
|
||||
=
|
||||
{ new ArgsCrate with
|
||||
member _.Apply e = e.Eval ofResult run
|
||||
}
|
@@ -16,6 +16,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="ArgsCrate.fs" />
|
||||
<Compile Include="Result.fs" />
|
||||
<Compile Include="Reconcile.fs" />
|
||||
<Compile Include="OutputSchema.fs" />
|
||||
<Compile Include="Verify.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
@@ -27,5 +32,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Argu" Version="6.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NJsonSchema" Version="10.9.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
47
Gitea.Declarative/OutputSchema.fs
Normal file
47
Gitea.Declarative/OutputSchema.fs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Gitea.Declarative
|
||||
|
||||
open System.IO
|
||||
open Argu
|
||||
|
||||
type OutputSchemaArgsFragment =
|
||||
| Output of string
|
||||
|
||||
interface IArgParserTemplate with
|
||||
member s.Usage =
|
||||
match s with
|
||||
| Output _ -> "path to the file to be written (or overwritten, if it already exists), instead of stdout"
|
||||
|
||||
type OutputSchemaArgs =
|
||||
{
|
||||
Output : FileInfo option
|
||||
}
|
||||
|
||||
static member OfParse
|
||||
(parsed : ParseResults<OutputSchemaArgsFragment>)
|
||||
: Result<OutputSchemaArgs, ArguParseException>
|
||||
=
|
||||
try
|
||||
{
|
||||
Output = parsed.TryGetResult OutputSchemaArgsFragment.Output |> Option.map FileInfo
|
||||
}
|
||||
|> Ok
|
||||
with :? ArguParseException as e ->
|
||||
Error e
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module OutputSchema =
|
||||
|
||||
let run (args : OutputSchemaArgs) : Async<int> =
|
||||
async {
|
||||
use stream = GiteaConfig.getSchema ()
|
||||
|
||||
match args.Output with
|
||||
| None ->
|
||||
let reader = new StreamReader (stream)
|
||||
System.Console.WriteLine (reader.ReadToEnd ())
|
||||
| Some output ->
|
||||
use output = output.OpenWrite ()
|
||||
stream.CopyTo output
|
||||
|
||||
return 0
|
||||
}
|
@@ -1,42 +1,7 @@
|
||||
namespace Gitea.Declarative
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
open System.Net.Http
|
||||
open Microsoft.Extensions.Logging
|
||||
open Microsoft.Extensions.Logging.Console
|
||||
open Microsoft.Extensions.Options
|
||||
open Argu
|
||||
|
||||
type ArgsFragments =
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced>] Config_File of string
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced>] Gitea_Host of string
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced ; CustomAppSettings "GITEA_ADMIN_API_TOKEN">] Gitea_Admin_Api_Token of
|
||||
string
|
||||
| [<Unique ; EqualsAssignmentOrSpaced ; CustomAppSettings "GITHUB_API_TOKEN">] GitHub_Api_Token of string
|
||||
| Dry_Run
|
||||
|
||||
interface IArgParserTemplate with
|
||||
member s.Usage =
|
||||
match s with
|
||||
| Config_File _ ->
|
||||
"a config file, JSON, conforming to GiteaConfig.schema.json, specifying the desired Gitea configuration"
|
||||
| Gitea_Host _ -> "the Gitea host, e.g. https://gitea.mydomain.com"
|
||||
| Gitea_Admin_Api_Token _ ->
|
||||
"a Gitea admin user's API token; can be read from the environment variable GITEA_ADMIN_API_TOKEN"
|
||||
| GitHub_Api_Token _ ->
|
||||
"a GitHub API token with read access to every desired sync-from-GitHub repo; can be read from the environment variable GITHUB_API_TOKEN"
|
||||
| Dry_Run _ -> "don't actually perform the reconciliation"
|
||||
|
||||
type Args =
|
||||
{
|
||||
ConfigFile : FileInfo
|
||||
GiteaHost : Uri
|
||||
GiteaAdminApiToken : string
|
||||
GitHubApiToken : string option
|
||||
DryRun : bool
|
||||
}
|
||||
|
||||
module Program =
|
||||
|
||||
let printUserErrors (m : Map<User, AlignmentError<UserInfo>>) =
|
||||
@@ -46,88 +11,78 @@ module Program =
|
||||
m
|
||||
|> Map.iter (fun (User u) errMap -> errMap |> Map.iter (fun (RepoName r) err -> printfn $"%s{u}: %s{r}: {err}"))
|
||||
|
||||
let getUserInput (s : string) : string =
|
||||
Console.Write s
|
||||
Console.ReadLine ()
|
||||
let subcommands =
|
||||
[|
|
||||
"reconcile",
|
||||
("Reconcile a remote Gitea server with a declarative configuration",
|
||||
ArgsCrate.make RunArgs.OfParse Reconcile.run)
|
||||
|
||||
"output-schema",
|
||||
("Output a schema you can use to verify the `reconcile` config file",
|
||||
ArgsCrate.make OutputSchemaArgs.OfParse OutputSchema.run)
|
||||
|
||||
"verify", ("Verify a `reconcile` configuration file", ArgsCrate.make VerifyArgs.OfParse Verify.run)
|
||||
|]
|
||||
|> Map.ofArray
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
let parser = ArgumentParser.Create<ArgsFragments> ()
|
||||
let reader = ConfigurationReader.FromEnvironmentVariables ()
|
||||
// It looks like Argu doesn't really support the combination of subcommands and read-from-env-vars, so we just
|
||||
// roll our own.
|
||||
|
||||
let parsed =
|
||||
try
|
||||
parser.Parse (argv, reader, raiseOnUsage = true) |> Some
|
||||
with :? ArguParseException as e ->
|
||||
eprintfn "%s" e.Message
|
||||
None
|
||||
match Array.tryHead argv with
|
||||
| None
|
||||
| Some "--help" ->
|
||||
subcommands.Keys
|
||||
|> String.concat ","
|
||||
|> eprintfn "Subcommands (try each with `--help`): %s"
|
||||
|
||||
match parsed with
|
||||
| None -> 127
|
||||
| Some parsed ->
|
||||
127
|
||||
|
||||
let args =
|
||||
{
|
||||
ConfigFile = parsed.GetResult ArgsFragments.Config_File |> FileInfo
|
||||
GiteaHost = parsed.GetResult ArgsFragments.Gitea_Host |> Uri
|
||||
GiteaAdminApiToken = parsed.GetResult ArgsFragments.Gitea_Admin_Api_Token
|
||||
GitHubApiToken = parsed.TryGetResult ArgsFragments.GitHub_Api_Token
|
||||
DryRun = parsed.TryGetResult ArgsFragments.Dry_Run |> Option.isSome
|
||||
}
|
||||
| Some commandName ->
|
||||
|
||||
let config = GiteaConfig.get args.ConfigFile
|
||||
match Map.tryFind commandName subcommands with
|
||||
| None ->
|
||||
subcommands.Keys
|
||||
|> String.concat ","
|
||||
|> eprintfn "Unrecognised command '%s'. Subcommands (try each with `--help`): %s" commandName
|
||||
|
||||
let options =
|
||||
let options = ConsoleLoggerOptions ()
|
||||
127
|
||||
|
||||
{ new IOptionsMonitor<ConsoleLoggerOptions> with
|
||||
member _.Get _ = options
|
||||
member _.CurrentValue = options
|
||||
| Some (_help, command) ->
|
||||
|
||||
member _.OnChange _ =
|
||||
{ new IDisposable with
|
||||
member _.Dispose () = ()
|
||||
}
|
||||
}
|
||||
let argv = Array.tail argv
|
||||
let config = ConfigurationReader.FromEnvironmentVariables ()
|
||||
|
||||
use loggerProvider = new ConsoleLoggerProvider (options)
|
||||
let logger = loggerProvider.CreateLogger "Gitea.Declarative"
|
||||
{ new ArgsEvaluator<_> with
|
||||
member _.Eval<'a, 'b when 'b :> IArgParserTemplate>
|
||||
(ofParseResult : ParseResults<'b> -> Result<'a, _>)
|
||||
run
|
||||
=
|
||||
let parser = ArgumentParser.Create<'b> ()
|
||||
|
||||
use client = new HttpClient ()
|
||||
client.BaseAddress <- args.GiteaHost
|
||||
client.DefaultRequestHeaders.Add ("Authorization", $"token {args.GiteaAdminApiToken}")
|
||||
let parsed =
|
||||
try
|
||||
parser.Parse (argv, config, raiseOnUsage = true) |> Some
|
||||
with :? ArguParseException as e ->
|
||||
e.Message.Replace ("Gitea.Declarative ", sprintf "Gitea.Declarative %s " commandName)
|
||||
|> eprintfn "%s"
|
||||
|
||||
let client = Gitea.Client client |> IGiteaClient.fromReal
|
||||
None
|
||||
|
||||
task {
|
||||
logger.LogInformation "Checking users..."
|
||||
let! userErrors = Gitea.checkUsers config client
|
||||
match parsed with
|
||||
| None -> Error 127
|
||||
| Some parsed ->
|
||||
|
||||
match userErrors, args.DryRun with
|
||||
| Ok (), _ -> ()
|
||||
| Error errors, false -> do! Gitea.reconcileUserErrors logger getUserInput client errors
|
||||
| Error errors, true ->
|
||||
logger.LogError (
|
||||
"Differences encountered in user configuration, but not reconciling them due to --dry-run. Errors may occur while checking repo configuration. {UserErrors}",
|
||||
errors
|
||||
)
|
||||
match ofParseResult parsed with
|
||||
| Error e ->
|
||||
e.Message.Replace ("Gitea.Declarative ", sprintf "Gitea.Declarative %s " commandName)
|
||||
|> eprintfn "%s"
|
||||
|
||||
logger.LogInformation "Checking repos..."
|
||||
let! repoErrors = Gitea.checkRepos logger config client
|
||||
Error 127
|
||||
| Ok args ->
|
||||
|
||||
match repoErrors, args.DryRun with
|
||||
| Ok (), _ -> ()
|
||||
| Error errors, false -> do! Gitea.reconcileRepoErrors logger client args.GitHubApiToken errors
|
||||
| Error errors, true ->
|
||||
logger.LogError (
|
||||
"Differences encountered in repo configuration, but not reconciling them due to --dry-run. {RepoErrors}",
|
||||
errors
|
||||
)
|
||||
|
||||
match userErrors, repoErrors with
|
||||
| Ok (), Ok () -> return 0
|
||||
| Ok (), Error _ -> return 1
|
||||
| Error _, Ok () -> return 2
|
||||
| Error _, Error _ -> return 3
|
||||
run args |> Ok
|
||||
}
|
||||
|> fun t -> t.Result
|
||||
|> command.Apply
|
||||
|> Result.cata Async.RunSynchronously id
|
||||
|
116
Gitea.Declarative/Reconcile.fs
Normal file
116
Gitea.Declarative/Reconcile.fs
Normal file
@@ -0,0 +1,116 @@
|
||||
namespace Gitea.Declarative
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
open System.Net.Http
|
||||
open Argu
|
||||
open Microsoft.Extensions.Logging.Console
|
||||
open Microsoft.Extensions.Options
|
||||
open Microsoft.Extensions.Logging
|
||||
|
||||
type RunArgsFragment =
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced>] Config_File of string
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced>] Gitea_Host of string
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced ; CustomAppSettings "GITEA_ADMIN_API_TOKEN">] Gitea_Admin_Api_Token of
|
||||
string
|
||||
| [<Unique ; EqualsAssignmentOrSpaced ; CustomAppSettings "GITHUB_API_TOKEN">] GitHub_Api_Token of string
|
||||
| Dry_Run
|
||||
|
||||
interface IArgParserTemplate with
|
||||
member s.Usage =
|
||||
match s with
|
||||
| Config_File _ ->
|
||||
"a config file, JSON, conforming to GiteaConfig.schema.json, specifying the desired Gitea configuration"
|
||||
| Gitea_Host _ -> "the Gitea host, e.g. https://gitea.mydomain.com"
|
||||
| Gitea_Admin_Api_Token _ ->
|
||||
"a Gitea admin user's API token; can be read from the environment variable GITEA_ADMIN_API_TOKEN"
|
||||
| GitHub_Api_Token _ ->
|
||||
"a GitHub API token with read access to every desired sync-from-GitHub repo; can be read from the environment variable GITHUB_API_TOKEN"
|
||||
| Dry_Run _ -> "don't actually perform the reconciliation"
|
||||
|
||||
type RunArgs =
|
||||
{
|
||||
ConfigFile : FileInfo
|
||||
GiteaHost : Uri
|
||||
GiteaAdminApiToken : string
|
||||
GitHubApiToken : string option
|
||||
DryRun : bool
|
||||
}
|
||||
|
||||
static member OfParse (parsed : ParseResults<RunArgsFragment>) : Result<RunArgs, ArguParseException> =
|
||||
try
|
||||
{
|
||||
ConfigFile = parsed.GetResult RunArgsFragment.Config_File |> FileInfo
|
||||
GiteaHost = parsed.GetResult RunArgsFragment.Gitea_Host |> Uri
|
||||
GiteaAdminApiToken = parsed.GetResult RunArgsFragment.Gitea_Admin_Api_Token
|
||||
GitHubApiToken = parsed.TryGetResult RunArgsFragment.GitHub_Api_Token
|
||||
DryRun = parsed.TryGetResult RunArgsFragment.Dry_Run |> Option.isSome
|
||||
}
|
||||
|> Ok
|
||||
with :? ArguParseException as e ->
|
||||
Error e
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Reconcile =
|
||||
|
||||
let private getUserInput (s : string) : string =
|
||||
Console.Write s
|
||||
Console.ReadLine ()
|
||||
|
||||
let run (args : RunArgs) : Async<int> =
|
||||
|
||||
let config = GiteaConfig.get args.ConfigFile
|
||||
|
||||
let options =
|
||||
let options = ConsoleLoggerOptions ()
|
||||
|
||||
{ new IOptionsMonitor<ConsoleLoggerOptions> with
|
||||
member _.Get _ = options
|
||||
member _.CurrentValue = options
|
||||
|
||||
member _.OnChange _ =
|
||||
{ new IDisposable with
|
||||
member _.Dispose () = ()
|
||||
}
|
||||
}
|
||||
|
||||
async {
|
||||
use loggerProvider = new ConsoleLoggerProvider (options)
|
||||
let logger = loggerProvider.CreateLogger "Gitea.Declarative"
|
||||
|
||||
use client = new HttpClient ()
|
||||
client.BaseAddress <- args.GiteaHost
|
||||
client.DefaultRequestHeaders.Add ("Authorization", $"token {args.GiteaAdminApiToken}")
|
||||
|
||||
let client = Gitea.Client client |> IGiteaClient.fromReal
|
||||
|
||||
logger.LogInformation "Checking users..."
|
||||
let! userErrors = Gitea.checkUsers config client
|
||||
|
||||
match userErrors, args.DryRun with
|
||||
| Ok (), _ -> ()
|
||||
| Error errors, false -> do! Gitea.reconcileUserErrors logger getUserInput client errors
|
||||
| Error errors, true ->
|
||||
logger.LogError (
|
||||
"Differences encountered in user configuration, but not reconciling them due to --dry-run. Errors may occur while checking repo configuration. {UserErrors}",
|
||||
errors
|
||||
)
|
||||
|
||||
logger.LogInformation "Checking repos..."
|
||||
let! repoErrors = Gitea.checkRepos logger config client
|
||||
|
||||
match repoErrors, args.DryRun with
|
||||
| Ok (), _ -> ()
|
||||
| Error errors, false -> do! Gitea.reconcileRepoErrors logger client args.GitHubApiToken errors
|
||||
| Error errors, true ->
|
||||
logger.LogError (
|
||||
"Differences encountered in repo configuration, but not reconciling them due to --dry-run. {RepoErrors}",
|
||||
errors
|
||||
)
|
||||
|
||||
match userErrors, repoErrors with
|
||||
| Ok (), Ok () -> return 0
|
||||
| Ok (), Error _ -> return 1
|
||||
| Error _, Ok () -> return 2
|
||||
| Error _, Error _ -> return 3
|
||||
}
|
9
Gitea.Declarative/Result.fs
Normal file
9
Gitea.Declarative/Result.fs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Gitea.Declarative
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Result =
|
||||
|
||||
let cata<'ok, 'err, 'result> onOk onError (r : Result<'ok, 'err>) : 'result =
|
||||
match r with
|
||||
| Ok ok -> onOk ok
|
||||
| Error e -> onError e
|
62
Gitea.Declarative/Verify.fs
Normal file
62
Gitea.Declarative/Verify.fs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace Gitea.Declarative
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
open Argu
|
||||
open NJsonSchema
|
||||
open NJsonSchema.Validation
|
||||
|
||||
type VerifyArgsFragment =
|
||||
| [<MainCommand>] Input of string
|
||||
|
||||
interface IArgParserTemplate with
|
||||
member s.Usage =
|
||||
match s with
|
||||
| Input _ -> "path to the file to be verified, or the literal '-' to read from stdin"
|
||||
|
||||
type VerifyArgs =
|
||||
| File of FileInfo
|
||||
| Stdin
|
||||
|
||||
static member OfParse (parsed : ParseResults<VerifyArgsFragment>) : Result<VerifyArgs, ArguParseException> =
|
||||
let input =
|
||||
try
|
||||
parsed.GetResult VerifyArgsFragment.Input |> Ok
|
||||
with :? ArguParseException as e ->
|
||||
Error e
|
||||
|
||||
input
|
||||
|> Result.map (fun input ->
|
||||
if input = "-" then
|
||||
VerifyArgs.Stdin
|
||||
else
|
||||
VerifyArgs.File (FileInfo input)
|
||||
)
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Verify =
|
||||
let run (args : VerifyArgs) : Async<int> =
|
||||
async {
|
||||
let validator = JsonSchemaValidator ()
|
||||
use schema = GiteaConfig.getSchema ()
|
||||
let! ct = Async.CancellationToken
|
||||
let! schema = JsonSchema.FromJsonAsync (schema, ct) |> Async.AwaitTask
|
||||
|
||||
use jsonStream =
|
||||
match args with
|
||||
| VerifyArgs.Stdin -> Console.OpenStandardInput ()
|
||||
| VerifyArgs.File f -> f.OpenRead ()
|
||||
|
||||
let reader = new StreamReader (jsonStream)
|
||||
let! json = reader.ReadToEndAsync ct |> Async.AwaitTask
|
||||
|
||||
let errors = validator.Validate (json, schema)
|
||||
|
||||
if errors.Count = 0 then
|
||||
return 0
|
||||
else
|
||||
for error in errors do
|
||||
Console.Error.WriteLine (sprintf "Error: %+A" error)
|
||||
|
||||
return 1
|
||||
}
|
@@ -11,16 +11,18 @@ This is a small project to allow you to specify a [Gitea](https://github.com/go-
|
||||
* Optional branch protection rules
|
||||
* Pull request configuration (e.g. whether rebase-merges are allowed, etc)
|
||||
* Collaborators
|
||||
* Reconciliation of differences between configuration and reality in the above
|
||||
* Deletion of repositories, guarded by the `"deleted": true` configuration
|
||||
|
||||
# Arguments
|
||||
|
||||
Run with the `--help` argument for a full listing.
|
||||
The main argument you provide is a JSON configuration file, which should conform to [the schema](./Gitea.Declarative.Lib/GiteaConfig.schema.json); there is [an example](./Gitea.Declarative.Test/GiteaConfig.json) in the tests.
|
||||
(You can call `dotnet-gitea-declarative verify $CONFIG_PATH` to verify against the schema, or `dotnet-gitea-declarative output-schema` to output the schema for local tooling to consume.)
|
||||
|
||||
# How to build and run
|
||||
|
||||
* With Nix: `nix run github:Smaug123/dotnet-gitea-declarative -- --help`.
|
||||
* With Nix: `nix run github:Smaug123/dotnet-gitea-declarative -- reconcile --help`.
|
||||
* From source: clone the repository, and `dotnet run`.
|
||||
|
||||
# Demos
|
||||
|
Reference in New Issue
Block a user