diff --git a/.github/workflows/reusable-compare-disko.yml b/.github/workflows/reusable-compare-disko.yml new file mode 100644 index 00000000..a5ec31bb --- /dev/null +++ b/.github/workflows/reusable-compare-disko.yml @@ -0,0 +1,61 @@ +name: 'Compare Disko NixOS configs' + +on: + # Allow this workflow to be reused by other workflows: + workflow_call: + inputs: + runner: + description: 'JSON-encoded list of runner labels' + default: '["self-hosted"]' + required: false + type: string + secrets: + CACHIX_AUTH_TOKEN: + description: 'Cachix auth token' + required: true + +jobs: + post-initial-comment: + runs-on: ${{ fromJSON(inputs.runner) }} + steps: + - name: 'Post initial disko status comment' + uses: marocchino/sticky-pull-request-comment@v2.9.3 + with: + recreate: true + message: | + Thanks for your Pull Request! + + This comment will be updated automatically with the status of disko on each machine. + compare-disko-configs: + runs-on: ${{ fromJSON(inputs.runner) }} + name: 'Compare disko configs on machines' + + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: metacraft-labs/nixos-modules/.github/install-nix@main + with: + cachix-cache: ${{ vars.CACHIX_CACHE }} + cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} + + - name: Compare disko configurations + env: + CACHIX_CACHE: ${{ vars.CACHIX_CACHE }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + # MCL_BRANCH: ${{ github.repository == 'metacraft-labs/nixos-modules' && github.sha || 'main' }} + MCL_BRANCH: ${{ github.repository == 'metacraft-labs/nixos-modules' && github.sha || 'feat/compare-disko' }} + run: nix run --accept-flake-config github:metacraft-labs/nixos-modules/${{ env.MCL_BRANCH }}#mcl compare_disko + + create-comment: + runs-on: ${{ fromJSON(inputs.runner) }} + name: 'Create comment' + needs: [post-initial-comment, compare-disko-configs] + + steps: + - name: Post Comment + uses: marocchino/sticky-pull-request-comment@v2.9.3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + recreate: true + path: comment.md diff --git a/packages/mcl/src/main.d b/packages/mcl/src/main.d index 4412d7ea..bf90e0e9 100644 --- a/packages/mcl/src/main.d +++ b/packages/mcl/src/main.d @@ -18,6 +18,7 @@ alias supportedCommands = imported!`std.traits`.AliasSeq!( cmds.ci, cmds.machine, cmds.config, + cmds.compare_disko, ); int main(string[] args) diff --git a/packages/mcl/src/src/mcl/commands/compare_disko.d b/packages/mcl/src/src/mcl/commands/compare_disko.d new file mode 100644 index 00000000..bc0d5585 --- /dev/null +++ b/packages/mcl/src/src/mcl/commands/compare_disko.d @@ -0,0 +1,189 @@ +module mcl.commands.compare_disko; + +import mcl.utils.env : optional, parseEnv; +import mcl.utils.nix : nix; +import mcl.utils.path : getTopLevel; +import mcl.utils.process : execute; +import mcl.utils.log : errorAndExit; + +import std.json : JSONValue; +import std.file : write; +import std.logger : infof; +import std.algorithm : uniq; +import std.array; +import std.meta : Filter; + +struct Params +{ + @optional() string baseBranch; + + void setup(){ + if (baseBranch == null){ + baseBranch = "main"; + } + } +} + +enum ChangeStatus{ + Unchanged, + Changed, + Removed, + New +} + +struct MachineChanges{ + string machine; + ChangeStatus _config; + ChangeStatus _create; +} + +enum string[] diskoOptions = [ __traits(allMembers, MachineChanges)[1 .. $] ]; + +string statusToSymbol(ChangeStatus s) { + final switch(s){ + case ChangeStatus.Unchanged: + return "🟩"; + break; + case ChangeStatus.Changed: + return "⚔"; + break; + case ChangeStatus.Removed: + return "šŸ—‘"; + break; + case ChangeStatus.New: + return "🧩"; + break; + } +} + +export void compare_disko(string[] args){ + const params = parseEnv!Params; + + JSONValue configurations = nix.flake!JSONValue("", [], "show"); + + auto machinesNew = appender!(string[])(); + foreach (string k, JSONValue v; configurations["nixosConfigurations"]){ + if (k[$-3 .. $] != "-vm" && + k != "gitlab-runner-container" && + k != "minimal-container" ) + { + machinesNew ~= k; + } + } + + auto attr = constructCommandAttr("./.", machinesNew[]); + auto machineOptionsRootNew = nix.eval!JSONValue("", ["--impure", "--expr", attr]); + + + string gitRoot = getTopLevel(); + string worktreeBaseBranch = gitRoot~"-"~params.baseBranch; + + if (execute("git rev-parse --abbrev-ref HEAD") == params.baseBranch){ + errorAndExit("Trying to compare branch "~params.baseBranch~" with itself. Quitting."); + } + + execute(["git", "worktree" , "add", worktreeBaseBranch, params.baseBranch]); + + configurations = nix.flake!JSONValue(worktreeBaseBranch, [], "show"); + + auto machinesOld = appender!(string[])(); + foreach (string k, JSONValue v; configurations["nixosConfigurations"]){ + if (k[$-3 .. $] != "-vm" && + k != "gitlab-runner-container" && + k != "minimal-container" ) + { + machinesOld ~= k; + } + } + + attr = constructCommandAttr(worktreeBaseBranch, machinesOld[]); + auto machineOptionsRootOld = nix.eval!JSONValue("", ["--impure", "--expr", attr]); + + auto machineChanges = appender!(MachineChanges[])(); + + string[] machines = uniq(machinesOld[] ~ machinesNew[]).array; + + foreach (string m; machines){ + MachineChanges mc; + mc.machine = m; + if (m in machineOptionsRootOld && !(m in machineOptionsRootNew)){ + static foreach (setting; diskoOptions){ + __traits(getMember, mc, setting) = ChangeStatus.Removed; + } + } + else if (m in machineOptionsRootNew && !(m in machineOptionsRootOld)){ + static foreach (setting; diskoOptions){ + __traits(getMember, mc, setting) = ChangeStatus.New; + } + } + else{ + static foreach (setting; diskoOptions){ + if (machineOptionsRootOld[m][setting] == machineOptionsRootNew[m][setting]){ + __traits(getMember, mc, setting) = ChangeStatus.Unchanged; + } + else{ + __traits(getMember, mc, setting) = ChangeStatus.Changed; + } + } + } + machineChanges ~= mc; + } + + create_comment(machineChanges[]); + + execute(["git", "worktree" , "remove", worktreeBaseBranch, "--force"]); +} + +void create_comment(MachineChanges[] machineChanges){ + auto data = appender!string; + data ~= "Thanks for your Pull Request!"; + if (machineChanges.length == 0){ + data ~="\n\nāœ… There have been no changes to disko configs"; + } + else{ + data ~= "\n\nBellow you will find a summary of machines and whether their disko attributes have differences."; + data ~= "\n\n**Legend:**"; + data ~= "\n🟩 = No changes"; + data ~= "\n⚔ = Something is different"; + data ~= "\nšŸ—‘ = Has been removed"; + data ~= "\n🧩 = Has been added"; + + data ~= "\n\n"; + foreach (string field; __traits(allMembers, MachineChanges)){ + data~="| " ~ field ~ " "; + } + data ~= "|\n"; + foreach (string field; __traits(allMembers, MachineChanges)){ + data~="| --- "; + } + data ~= "|\n"; + + foreach(mc; machineChanges){ + foreach (string field; __traits(allMembers, MachineChanges)){ + static if (is(typeof(__traits(getMember, mc, field)) == string)){ + data~="| " ~ __traits(getMember, mc, field) ~ " "; + } + else { + data~="| " ~ statusToSymbol(__traits(getMember, mc, field)) ~ " "; + } + } + data ~= "|\n"; + } + } + write("comment.md", data[]); +} + + +string constructCommandAttr(string flakePath, string[] machines){ + auto ret = appender!string; + ret ~= "let flake = (builtins.getFlake (builtins.toString " ~ flakePath ~ ")); in { "; + foreach (m; machines){ + ret ~= m ~ " = { "; + foreach (option; diskoOptions){ + ret ~= option ~ " = flake.nixosConfigurations." ~ m ~ ".config.disko.devices." ~ option ~ "; "; + } + ret ~= "}; "; + } + ret ~= "}"; + return ret[]; +} diff --git a/packages/mcl/src/src/mcl/commands/package.d b/packages/mcl/src/src/mcl/commands/package.d index 4ab52cce..5074c96b 100644 --- a/packages/mcl/src/src/mcl/commands/package.d +++ b/packages/mcl/src/src/mcl/commands/package.d @@ -8,3 +8,4 @@ public import mcl.commands.ci : ci; public import mcl.commands.host_info : host_info; public import mcl.commands.machine : machine; public import mcl.commands.config : config; +public import mcl.commands.compare_disko: compare_disko; diff --git a/packages/mcl/src/src/mcl/utils/nix.d b/packages/mcl/src/src/mcl/utils/nix.d index e40e410e..b55fb6aa 100644 --- a/packages/mcl/src/src/mcl/utils/nix.d +++ b/packages/mcl/src/src/mcl/utils/nix.d @@ -61,7 +61,7 @@ struct NixCommand template opDispatch(string commandName) { - T opDispatch(T = string)(string path, string[] args = []) + T opDispatch(T = string)(string path, string[] args = [], string subcommand = "") { import std.algorithm : canFind; @@ -80,9 +80,9 @@ struct NixCommand args = ["--json"] ~ args; auto command = [ - "nix", "--experimental-features", "nix-command flakes", + "nix", "--experimental-features", "nix-command flakes pipe-operators", commandName, - ] ~ args ~ path; + ] ~ (subcommand == "" ? [] : [ subcommand ]) ~ args ~ path; auto output = command.execute(true).strip();