Compare commits

..

2 Commits

Author SHA1 Message Date
Jörg Thalheim
4035c25b3d don't enable ssh askpass for now if we have a build_host set 2025-07-07 17:21:27 +02:00
Jörg Thalheim
23c1ae031f add ssh askpass implementation 2025-07-07 17:21:27 +02:00
50 changed files with 300 additions and 857 deletions

View File

@@ -1,75 +0,0 @@
#!/usr/bin/env bash
# Shared script for creating pull requests in Gitea workflows
set -euo pipefail
# Required environment variables:
# - CI_BOT_TOKEN: Gitea bot token for authentication
# - PR_BRANCH: Branch name for the pull request
# - PR_TITLE: Title of the pull request
# - PR_BODY: Body/description of the pull request
if [[ -z "${CI_BOT_TOKEN:-}" ]]; then
echo "Error: CI_BOT_TOKEN is not set" >&2
exit 1
fi
if [[ -z "${PR_BRANCH:-}" ]]; then
echo "Error: PR_BRANCH is not set" >&2
exit 1
fi
if [[ -z "${PR_TITLE:-}" ]]; then
echo "Error: PR_TITLE is not set" >&2
exit 1
fi
if [[ -z "${PR_BODY:-}" ]]; then
echo "Error: PR_BODY is not set" >&2
exit 1
fi
# Push the branch
git push origin "+HEAD:${PR_BRANCH}"
# Create pull request
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
-H "Authorization: token $CI_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"head\": \"${PR_BRANCH}\",
\"base\": \"main\",
\"title\": \"${PR_TITLE}\",
\"body\": \"${PR_BODY}\"
}" \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
pr_number=$(echo "$resp" | jq -r '.number')
if [[ "$pr_number" == "null" ]]; then
echo "Error creating pull request:" >&2
echo "$resp" | jq . >&2
exit 1
fi
echo "Created pull request #$pr_number"
# Merge when checks succeed
while true; do
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
-H "Authorization: token $CI_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"Do": "merge",
"merge_when_checks_succeed": true,
"delete_branch_after_merge": true
}' \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
msg=$(echo "$resp" | jq -r '.message')
if [[ "$msg" != "Please try again later" ]]; then
break
fi
echo "Retrying in 2 seconds..."
sleep 2
done
echo "Pull request #$pr_number merge initiated"

View File

@@ -19,10 +19,35 @@ jobs:
run: |
export GIT_AUTHOR_NAME=clan-bot GIT_AUTHOR_EMAIL=clan-bot@clan.lol GIT_COMMITTER_NAME=clan-bot GIT_COMMITTER_EMAIL=clan-bot@clan.lol
git commit -am "Update pinned clan-core for checks"
git push origin +HEAD:update-clan-core-for-checks
set -x
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
-H "Authorization: token $CI_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"head": "update-clan-core-for-checks",
"base": "main",
"title": "Update Clan Core for Checks",
"body": "This PR updates the pinned clan-core flake input that is used for checks."
}' \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
pr_number=$(echo "$resp" | jq -r '.number')
# Use shared PR creation script
export PR_BRANCH="update-clan-core-for-checks"
export PR_TITLE="Update Clan Core for Checks"
export PR_BODY="This PR updates the pinned clan-core flake input that is used for checks."
./.gitea/workflows/create-pr.sh
# Merge when succeed
while true; do
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
-H "Authorization: token $CI_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"Do": "merge",
"merge_when_checks_succeed": true,
"delete_branch_after_merge": true
}' \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
msg=$(echo $resp | jq -r '.message')
if [[ "$msg" != "Please try again later" ]]; then
break
fi
echo "Retrying in 2 seconds..."
sleep 2
done

View File

@@ -1,40 +0,0 @@
name: "Update private flake inputs"
on:
repository_dispatch:
workflow_dispatch:
schedule:
- cron: "0 3 * * *" # Run daily at 3 AM
jobs:
update-private-flake:
runs-on: nix
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Update private flake inputs
run: |
# Update the private flake lock file
cd devFlake/private
nix flake update
cd ../..
# Update the narHash
bash ./devFlake/update-private-narhash
- name: Create pull request
env:
CI_BOT_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
run: |
export GIT_AUTHOR_NAME=clan-bot GIT_AUTHOR_EMAIL=clan-bot@clan.lol GIT_COMMITTER_NAME=clan-bot GIT_COMMITTER_EMAIL=clan-bot@clan.lol
# Check if there are any changes
if ! git diff --quiet; then
git add devFlake/private/flake.lock devFlake/private.narHash
git commit -m "Update dev flake"
# Use shared PR creation script
export PR_BRANCH="update-dev-flake"
export PR_TITLE="Update dev flake"
export PR_BODY="This PR updates the dev flake inputs and corresponding narHash."
else
echo "No changes detected in dev flake inputs"
fi

View File

@@ -19,11 +19,10 @@
...
}:
let
dependencies =
[
pkgs.stdenv.drvPath
]
++ builtins.map (i: i.outPath) (builtins.attrValues (builtins.removeAttrs self.inputs [ "self" ]));
dependencies = [
self
pkgs.stdenv.drvPath
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in
{

View File

@@ -47,6 +47,14 @@ nixosLib.runTest (
clientone =
{ config, pkgs, ... }:
let
dependencies = [
clan-core
pkgs.stdenv.drvPath
] ++ builtins.map (i: i.outPath) (builtins.attrValues clan-core.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in
{
services.openssh.enable = true;
@@ -57,6 +65,15 @@ nixosLib.runTest (
environment.systemPackages = [ clan-core.packages.${pkgs.system}.clan-cli ];
environment.etc.install-closure.source = "${closureInfo}/store-paths";
nix.settings = {
substituters = pkgs.lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = pkgs.lib.mkForce 3;
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
};
system.extraDependencies = dependencies;
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
};

View File

@@ -41,6 +41,14 @@
clan-core,
...
}:
let
dependencies = [
clan-core
pkgs.stdenv.drvPath
] ++ builtins.map (i: i.outPath) (builtins.attrValues clan-core.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in
{
services.openssh.enable = true;
@@ -51,6 +59,15 @@
environment.systemPackages = [ clan-core.packages.${pkgs.system}.clan-cli ];
environment.etc.install-closure.source = "${closureInfo}/store-paths";
nix.settings = {
substituters = pkgs.lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = pkgs.lib.mkForce 3;
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
};
system.extraDependencies = dependencies;
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
};

View File

@@ -23,13 +23,7 @@ in
unit-test-module = (
self.clanLib.test.flakeModules.makeEvalChecks {
inherit module;
inherit inputs;
fileset = lib.fileset.unions [
# The hello-world service being tested
../../clanServices/hello-world
# Required modules
../../nixosModules/clanCore
];
inherit self inputs;
testName = "hello-world";
tests = ./tests/eval-tests.nix;
# Optional arguments passed to the test

View File

@@ -15,15 +15,7 @@ in
unit-test-module = (
self.clanLib.test.flakeModules.makeEvalChecks {
inherit module;
inherit inputs;
fileset = lib.fileset.unions [
# The zerotier service being tested
../../clanServices/zerotier
# Required modules
../../nixosModules/clanCore
# Dependencies like clan-cli
../../pkgs/clan-cli
];
inherit self inputs;
testName = "zerotier";
tests = ./tests/eval-tests.nix;
testArgs = { };

View File

@@ -1 +0,0 @@
sha256-pFUj3KhQ4FkzZT19t+FHBru8u8Lspax0rS2cv7nXIgM=

View File

@@ -1,165 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"ixx": {
"inputs": {
"flake-utils": [
"nuschtos",
"flake-utils"
],
"nixpkgs": [
"nuschtos",
"nixpkgs"
]
},
"locked": {
"lastModified": 1748294338,
"narHash": "sha256-FVO01jdmUNArzBS7NmaktLdGA5qA3lUMJ4B7a05Iynw=",
"owner": "NuschtOS",
"repo": "ixx",
"rev": "cc5f390f7caf265461d4aab37e98d2292ebbdb85",
"type": "github"
},
"original": {
"owner": "NuschtOS",
"ref": "v0.0.8",
"repo": "ixx",
"type": "github"
}
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1751867001,
"narHash": "sha256-3I49W0s3WVEDBO5S1RxYr74E2LLG7X8Wuvj9AmU0RDk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "73feb5e20ec7259e280ca6f424ba165059b3bb6b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable-small",
"repo": "nixpkgs",
"type": "github"
}
},
"nuschtos": {
"inputs": {
"flake-utils": "flake-utils_2",
"ixx": "ixx",
"nixpkgs": [
"nixpkgs-dev"
]
},
"locked": {
"lastModified": 1749730855,
"narHash": "sha256-L3x2nSlFkXkM6tQPLJP3oCBMIsRifhIDPMQQdHO5xWo=",
"owner": "NuschtOS",
"repo": "search",
"rev": "8dfe5879dd009ff4742b668d9c699bc4b9761742",
"type": "github"
},
"original": {
"owner": "NuschtOS",
"repo": "search",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs-dev": "nixpkgs-dev",
"nuschtos": "nuschtos",
"systems": "systems_2",
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": []
},
"locked": {
"lastModified": 1750931469,
"narHash": "sha256-0IEdQB1nS+uViQw4k3VGUXntjkDp7aAlqcxdewb/hAc=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "ac8e6f32e11e9c7f153823abc3ab007f2a65d3e1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,19 +0,0 @@
{
description = "private dev inputs";
# Dev dependencies
inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-unstable-small";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.flake-utils.inputs.systems.follows = "systems";
inputs.nuschtos.url = "github:NuschtOS/search";
inputs.nuschtos.inputs.nixpkgs.follows = "nixpkgs-dev";
inputs.treefmt-nix.url = "github:numtide/treefmt-nix";
inputs.treefmt-nix.inputs.nixpkgs.follows = "";
inputs.systems.url = "github:nix-systems/default";
outputs = _: { };
}

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env bash
# Used to update the private dev flake hash reference.
set -euo pipefail
cd "$(dirname "$0")"
echo "Updating $PWD/private.narHash" >&2
nix --extra-experimental-features 'flakes nix-command' flake lock ./private
nix --extra-experimental-features 'flakes nix-command' hash path ./private >./private.narHash
echo OK

View File

@@ -1,4 +1,5 @@
{
clan-core,
pkgs,
module-docs,
clan-cli-docs,
@@ -18,17 +19,7 @@ pkgs.stdenv.mkDerivation {
# Points to repository root.
# so that we can access directories outside of docs to include code snippets
src = pkgs.lib.fileset.toSource {
root = ../..;
fileset = pkgs.lib.fileset.unions [
# Docs directory
../../docs
# Icons needed for the build
../../pkgs/clan-app/ui/icons
# Any other directories that might be referenced for code snippets
# Add them here as needed based on what mkdocs actually uses
];
};
src = clan-core;
nativeBuildInputs =
[

View File

@@ -82,9 +82,10 @@
}
''
export CLAN_CORE_PATH=${
inputs.nixpkgs.lib.fileset.toSource {
root = ../..;
fileset = ../../clanModules;
self.filter {
include = [
"clanModules"
];
}
}
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
@@ -125,6 +126,7 @@
});
packages = {
docs = pkgs.python3.pkgs.callPackage ./default.nix {
clan-core = self;
inherit (self'.packages)
clan-cli-docs
docs-options

View File

@@ -1,15 +1,9 @@
{
self,
config,
inputs,
privateInputs ? { },
...
}:
{ self, config, ... }:
{
perSystem =
{
inputs',
lib,
pkgs,
...
}:
let
@@ -163,16 +157,11 @@
};
in
{
packages = lib.optionalAttrs ((privateInputs ? nuschtos) || (inputs ? nuschtos)) {
docs-options =
(privateInputs.nuschtos or inputs.nuschtos)
.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch
{
inherit baseHref;
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
};
packages.docs-options = inputs'.nuschtos.packages.mkMultiSearch {
inherit baseHref;
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
};
};
}

72
flake.lock generated
View File

@@ -67,6 +67,52 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"ixx": {
"inputs": {
"flake-utils": [
"nuschtos",
"flake-utils"
],
"nixpkgs": [
"nuschtos",
"nixpkgs"
]
},
"locked": {
"lastModified": 1748294338,
"narHash": "sha256-FVO01jdmUNArzBS7NmaktLdGA5qA3lUMJ4B7a05Iynw=",
"owner": "NuschtOS",
"repo": "ixx",
"rev": "cc5f390f7caf265461d4aab37e98d2292ebbdb85",
"type": "github"
},
"original": {
"owner": "NuschtOS",
"ref": "v0.0.8",
"repo": "ixx",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
@@ -128,15 +174,41 @@
"url": "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz"
}
},
"nuschtos": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"ixx": "ixx",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1749730855,
"narHash": "sha256-L3x2nSlFkXkM6tQPLJP3oCBMIsRifhIDPMQQdHO5xWo=",
"owner": "NuschtOS",
"repo": "search",
"rev": "8dfe5879dd009ff4742b668d9c699bc4b9761742",
"type": "github"
},
"original": {
"owner": "NuschtOS",
"repo": "search",
"type": "github"
}
},
"root": {
"inputs": {
"data-mesher": "data-mesher",
"disko": "disko",
"flake-parts": "flake-parts",
"flake-utils": "flake-utils",
"nix-darwin": "nix-darwin",
"nix-select": "nix-select",
"nixos-facter-modules": "nixos-facter-modules",
"nixpkgs": "nixpkgs",
"nuschtos": "nuschtos",
"sops-nix": "sops-nix",
"systems": "systems",
"treefmt-nix": "treefmt-nix"

View File

@@ -35,13 +35,19 @@
};
};
# dependencies needed for nuschtos
flake-utils.url = "github:numtide/flake-utils";
flake-utils.inputs.systems.follows = "systems";
nuschtos.url = "github:NuschtOS/search";
nuschtos.inputs.nixpkgs.follows = "nixpkgs";
nuschtos.inputs.flake-utils.follows = "flake-utils";
};
outputs =
inputs@{
flake-parts,
nixpkgs,
systems,
flake-parts,
...
}:
let
@@ -50,25 +56,10 @@
optional
pathExists
;
loadDevFlake =
path:
let
flakeHash = nixpkgs.lib.fileContents "${toString path}.narHash";
flakePath = "path:${toString path}?narHash=${flakeHash}";
in
builtins.getFlake (builtins.unsafeDiscardStringContext flakePath);
devFlake = builtins.tryEval (loadDevFlake ./devFlake/private);
privateInputs = if devFlake.success then devFlake.value.inputs else { };
in
flake-parts.lib.mkFlake { inherit inputs; } (
{ ... }:
{
_module.args = {
inherit privateInputs;
};
clan = {
meta.name = "clan-core";
inventory = {

View File

@@ -4,7 +4,7 @@
perSystem =
{ self', pkgs, ... }:
{
treefmt.projectRootFile = "LICENSE.md";
treefmt.projectRootFile = ".git/config";
treefmt.programs.shellcheck.enable = true;
treefmt.programs.mypy.enable = true;

View File

@@ -37,7 +37,6 @@ lib.fix (
inventory = clanLib.callLib ./modules/inventory { };
modules = clanLib.callLib ./modules/inventory/frontmatter { };
test = clanLib.callLib ./test { };
flake-inputs = clanLib.callLib ./flake-inputs.nix { };
# Custom types
types = clanLib.callLib ./types { };

View File

@@ -1,18 +0,0 @@
{ ... }:
{
/**
Generate nix-unit input overrides for tests
# Example
```nix
inputOverrides = clanLib.flake-inputs.getOverrides inputs;
```
*/
getOverrides =
inputs:
builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (
builtins.filter (name: name != "self") (builtins.attrNames inputs)
)
);
}

View File

@@ -1,6 +1,8 @@
{ self, inputs, ... }:
let
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
inputOverrides = builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
);
in
{
perSystem =

View File

@@ -4,7 +4,9 @@
...
}:
let
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
inputOverrides = builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
);
in
{
imports = [

View File

@@ -1,6 +1,8 @@
{ self, inputs, ... }:
let
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
inputOverrides = builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
);
in
{
perSystem =
@@ -10,23 +12,6 @@ in
system,
...
}:
let
# Common filtered source for inventory tests
inventoryTestsSrc = lib.fileset.toSource {
root = ../../../..;
fileset = lib.fileset.unions [
../../../../flake.nix
../../../../flake.lock
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../../..)
../../../../flakeModules
../../../../lib
../../../../nixosModules/clanCore
../../../../clanModules/borgbackup
../../../../machines
../../../../inventory.json
];
};
in
{
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.<attrName>
legacyPackages.evalTests-distributedServices = import ./tests {
@@ -44,7 +29,7 @@ in
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${inventoryTestsSrc}#legacyPackages.${system}.evalTests-distributedServices
--flake ${self}#legacyPackages.${system}.evalTests-distributedServices
touch $out
'';
@@ -54,7 +39,7 @@ in
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${inventoryTestsSrc}#legacyPackages.${system}.eval-tests-resolve-module
--flake ${self}#legacyPackages.${system}.eval-tests-resolve-module
touch $out
'';

View File

@@ -5,7 +5,9 @@
...
}:
let
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
inputOverrides = builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
);
in
{
imports = [
@@ -68,18 +70,12 @@ in
--show-trace \
${inputOverrides} \
--flake ${
lib.fileset.toSource {
root = ../../..;
fileset = lib.fileset.unions [
../../../flake.nix
../../../flake.lock
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
../../../flakeModules
../../../lib
../../../nixosModules/clanCore
../../../clanModules/borgbackup
../../../machines
../../../inventory.json
self.filter {
include = [
"flakeModules"
"lib"
"clanModules/flake-module.nix"
"clanModules/borgbackup"
];
}
}#legacyPackages.${system}.evalTests-inventory

View File

@@ -16,7 +16,7 @@
*/
makeEvalChecks =
{
fileset,
self,
inputs,
testName,
tests,
@@ -24,7 +24,9 @@
testArgs ? { },
}:
let
inputOverrides = clanLib.flake-inputs.getOverrides inputs;
inputOverrides = builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
);
attrName = "eval-tests-${testName}";
in
{
@@ -39,44 +41,16 @@
}
// testArgs
);
checks.${attrName} =
let
# The root is two directories up from where this file is located
root = ../..;
checks.${attrName} = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
# Combine the user-provided fileset with all flake-module.nix files
# and other essential files
src = lib.fileset.toSource {
inherit root;
fileset = lib.fileset.unions [
# Core flake files
(root + "/flake.nix")
(root + "/flake.lock")
# All flake-module.nix files anywhere in the tree
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") root)
# The flakeModules/clan.nix if it exists
(lib.fileset.maybeMissing (root + "/flakeModules/clan.nix"))
# Core libraries
(root + "/lib")
# User-provided fileset
fileset
];
};
in
pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${src}#legacyPackages.${system}.${attrName}
touch $out
'';
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${self}#legacyPackages.${system}.${attrName}
touch $out
'';
};
}

View File

@@ -1,9 +1,4 @@
{
self,
inputs,
lib,
...
}:
{ self, inputs, ... }:
{
perSystem =
{ ... }:
@@ -15,11 +10,7 @@
test-types-module = (
self.clanLib.test.flakeModules.makeEvalChecks {
module = throw "";
inherit inputs;
fileset = lib.fileset.unions [
# Only lib is needed for type tests
../../lib
];
inherit self inputs;
testName = "types";
tests = ./tests.nix;
# Optional arguments passed to the test

View File

@@ -1,4 +1,4 @@
{ lib, pkgs }:
{ lib, pkgs, ... }:
let
eval =
module:

View File

@@ -5,14 +5,18 @@
...
}:
let
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
inputOverrides = builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
);
in
{
perSystem =
{ system, pkgs, ... }:
{
legacyPackages.evalTests-module-clan-vars = import ./eval-tests {
inherit lib pkgs;
inherit lib;
clan-core = self;
pkgs = inputs.nixpkgs.legacyPackages.${system};
};
checks.eval-module-clan-vars = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
@@ -22,15 +26,11 @@ in
--show-trace \
${inputOverrides} \
--flake ${
lib.fileset.toSource {
root = ../../..;
fileset = lib.fileset.unions [
../../../flake.nix
../../../flake.lock
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
../../../flakeModules/clan.nix
../../../lib
../../../nixosModules/clanCore/vars
self.filter {
include = [
"flakeModules"
"nixosModules"
"lib"
];
}
}#legacyPackages.${system}.evalTests-module-clan-vars

View File

@@ -48,7 +48,7 @@
"jsdom": "^26.1.0",
"knip": "^5.61.2",
"markdown-to-jsx": "^7.7.10",
"playwright": "~1.53.2",
"playwright": "~1.52.0",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
@@ -6189,13 +6189,13 @@
}
},
"node_modules/playwright": {
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.53.2"
"playwright-core": "1.52.0"
},
"bin": {
"playwright": "cli.js"
@@ -6208,9 +6208,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -48,7 +48,7 @@
"jsdom": "^26.1.0",
"knip": "^5.61.2",
"markdown-to-jsx": "^7.7.10",
"playwright": "~1.53.2",
"playwright": "~1.52.0",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",

View File

@@ -40,7 +40,7 @@ export type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
legend: "Signup",
children: (props: FieldProps) => (
fields: (props: FieldProps) => (
<>
<TextInput
{...props}
@@ -90,7 +90,7 @@ export const Error: Story = {
args: {
legend: "Signup",
error: "You must enter a First Name",
children: (props: FieldProps) => (
fields: (props: FieldProps) => (
<>
<TextInput
{...props}

View File

@@ -1,57 +1,41 @@
import "./Fieldset.css";
import { JSX, splitProps } from "solid-js";
import { JSX } from "solid-js";
import cx from "classnames";
import { Typography } from "@/src/components/v2/Typography/Typography";
import { FieldProps } from "./Field";
export type FieldsetFieldProps = Pick<
FieldProps,
"orientation" | "inverted"
> & {
export interface FieldsetProps extends FieldProps {
legend: string;
disabled: boolean;
error?: string;
disabled?: boolean;
};
export interface FieldsetProps
extends Pick<FieldProps, "orientation" | "inverted"> {
legend?: string;
disabled?: boolean;
error?: string;
children: (props: FieldsetFieldProps) => JSX.Element;
fields: (props: FieldProps) => JSX.Element;
}
export const Fieldset = (props: FieldsetProps) => {
const orientation = () => props.orientation || "vertical";
const [fieldProps] = splitProps(props, [
"orientation",
"inverted",
"disabled",
"error",
]);
return (
<fieldset
role="group"
class={cx({ inverted: props.inverted })}
disabled={props.disabled || false}
class={cx(orientation(), { inverted: props.inverted })}
disabled={props.disabled}
>
{props.legend && (
<legend>
<Typography
hierarchy="label"
family="mono"
size="default"
weight="normal"
color="tertiary"
transform="uppercase"
inverted={props.inverted}
>
{props.legend}
</Typography>
</legend>
)}
<div class="fields">{props.children(fieldProps)}</div>
<legend>
<Typography
hierarchy="label"
family="mono"
size="default"
weight="normal"
color="tertiary"
transform="uppercase"
inverted={props.inverted}
>
{props.legend}
</Typography>
</legend>
<div class="fields">
{props.fields({ ...props, orientation: orientation() })}
</div>
{props.error && (
<div class="error" role="alert">
<Typography

View File

@@ -1,24 +0,0 @@
div.modal-content {
@apply max-w-[512px];
@apply rounded-md;
/* todo replace with a theme() color */
box-shadow: 0.1875rem 0.1875rem 0 0 rgba(145, 172, 175, 0.32);
& > div.header {
@apply flex items-center justify-center;
@apply w-full px-2 py-1.5;
@apply bg-def-3;
@apply border border-def-2 rounded-tl-md rounded-tr-md;
@apply border-b-def-3;
& > .title {
@apply mx-auto;
}
}
& > div.body {
@apply p-6 bg-def-1;
@apply border border-def-2 rounded-bl-md rounded-br-md;
}
}

View File

@@ -1,74 +0,0 @@
import { TagProps } from "@/src/components/v2/Tag/Tag";
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { fn } from "storybook/test";
import {
Modal,
ModalContext,
ModalProps,
} from "@/src/components/v2/Modal/Modal";
import { Fieldset } from "@/src/components/v2/Form/Fieldset";
import { TextInput } from "@/src/components/v2/Form/TextInput";
import { TextArea } from "@/src/components/v2/Form/TextArea";
import { Checkbox } from "@/src/components/v2/Form/Checkbox";
import { Button } from "../Button/Button";
const meta: Meta<ModalProps> = {
title: "Components/Modal",
component: Modal,
};
export default meta;
type Story = StoryObj<TagProps>;
export const Default: Story = {
args: {
title: "Example Modal",
onClose: fn(),
children: ({ close }: ModalContext) => (
<form class="flex flex-col gap-5">
<Fieldset legend="General">
{(props) => (
<>
<TextInput
{...props}
label="First Name"
size="s"
required={true}
input={{ placeholder: "Ron" }}
/>
<TextInput
{...props}
label="Last Name"
size="s"
required={true}
input={{ placeholder: "Burgundy" }}
/>
<TextArea
{...props}
label="Bio"
size="s"
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
/>
<Checkbox
{...props}
size="s"
label="Accept Terms"
required={true}
/>
</>
)}
</Fieldset>
<div class="flex w-full items-center justify-end gap-4">
<Button size="s" hierarchy="secondary" onClick={close}>
Cancel
</Button>
<Button size="s" type="submit" hierarchy="primary" onClick={close}>
Save
</Button>
</div>
</form>
),
},
};

View File

@@ -1,40 +0,0 @@
import { createSignal, JSX } from "solid-js";
import { Dialog as KDialog } from "@kobalte/core/dialog";
import "./Modal.css";
import { Typography } from "../Typography/Typography";
import Icon from "../Icon/Icon";
export interface ModalContext {
close(): void;
}
export interface ModalProps {
id?: string;
title: string;
onClose: () => void;
children: (ctx: ModalContext) => JSX.Element;
}
export const Modal = (props: ModalProps) => {
const [open, setOpen] = createSignal(true);
return (
<KDialog id={props.id} open={open()} modal={true}>
<KDialog.Portal>
<KDialog.Content class="modal-content">
<div class="header">
<Typography class="title" hierarchy="label" family="mono" size="xs">
{props.title}
</Typography>
<KDialog.CloseButton onClick={() => setOpen(false)}>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
</div>
<div class="body">
{props.children({ close: () => setOpen(false) })}
</div>
</KDialog.Content>
</KDialog.Portal>
</KDialog>
);
};

View File

@@ -49,21 +49,6 @@ def run_machine_flash(
extra_args: list[str] | None = None,
graphical: bool = False,
) -> None:
"""Flash a machine with the given configuration.
Args:
machine: The Machine instance to flash.
mode: The mode to use for flashing (e.g., "install", "reinstall
disks: List of Disk instances representing the disks to flash.
system_config: SystemConfig instance containing language, keymap, and SSH keys.
dry_run: If True, perform a dry run without making changes.
write_efi_boot_entries: If True, write EFI boot entries.
debug: If True, enable debug mode.
extra_args: Additional arguments to pass to the disko-install command.
graphical: If True, run the command in graphical mode.
Raises:
ClanError: If the language or keymap is invalid, or if there are issues with
reading SSH keys, or if disko-install fails.
"""
devices = [Path(disk.device) for disk in disks]
with pause_automounting(devices, machine, request_graphical=graphical):
if extra_args is None:

View File

@@ -19,12 +19,6 @@ class FlashOptions(TypedDict):
@API.register
def get_flash_options() -> FlashOptions:
"""Retrieve available languages and keymaps for flash configuration.
Returns:
FlashOptions: A dictionary containing lists of available languages and keymaps.
Raises:
ClanError: If the locale file or keymaps directory does not exist.
"""
return {"languages": list_languages(), "keymaps": list_keymaps()}

View File

@@ -429,21 +429,6 @@ def get_generators(
full_closure: bool = False,
include_previous_values: bool = False,
) -> list[Generator]:
"""
Get the list of generators for a machine, optionally with previous values.
If `full_closure` is True, it returns the full closure of generators.
If `include_previous_values` is True, it includes the previous values for prompts.
Args:
machine_name (str): The name of the machine.
base_dir (Path): The base directory of the flake.
full_closure (bool): Whether to return the full closure of generators. If False,
it returns only the generators that are missing or need to be regenerated.
include_previous_values (bool): Whether to include previous values for prompts.
Returns:
list[Generator]: A list of generators for the machine.
"""
from clan_lib.machines.machines import Machine
return get_closure(
@@ -483,20 +468,6 @@ def run_generators(
base_dir: Path,
no_sandbox: bool = False,
) -> bool:
"""Run the specified generators for a machine.
Args:
machine_name (str): The name of the machine.
generators (list[str]): The list of generator names to run.
all_prompt_values (dict[str, dict[str, str]]): A dictionary mapping generator names
to their prompt values.
base_dir (Path): The base directory of the flake.
no_sandbox (bool): Whether to disable sandboxing when executing the generator.
Returns:
bool: True if any variables were generated, False otherwise.
Raises:
ClanError: If the machine or generator is not found, or if there are issues with
executing the generator.
"""
from clan_lib.machines.machines import Machine
machine = Machine(name=machine_name, flake=Flake(str(base_dir)))

View File

@@ -1,8 +1,12 @@
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal
from clan_lib.cmd import RunOpts, run
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from clan_lib.nix import nix_shell
from . import API
@@ -38,6 +42,55 @@ def open_file(file_request: FileRequest) -> list[str] | None:
raise NotImplementedError(msg)
@dataclass
class File:
path: str
file_type: Literal["file", "directory", "symlink"]
@dataclass
class Directory:
path: str
files: list[File] = field(default_factory=list)
@API.register
def get_directory(flake: Flake) -> Directory:
curr_dir = flake.path
directory = Directory(path=str(curr_dir))
if not curr_dir.is_dir():
msg = f"Path {curr_dir} is not a directory"
raise ClanError(msg)
with os.scandir(curr_dir.resolve()) as it:
for entry in it:
if entry.is_symlink():
directory.files.append(
File(
path=str(curr_dir / Path(entry.name)),
file_type="symlink",
)
)
elif entry.is_file():
directory.files.append(
File(
path=str(curr_dir / Path(entry.name)),
file_type="file",
)
)
elif entry.is_dir():
directory.files.append(
File(
path=str(curr_dir / Path(entry.name)),
file_type="directory",
)
)
return directory
@dataclass
class BlkInfo:
name: str

View File

@@ -89,10 +89,6 @@ def parse_avahi_output(output: str) -> DNSInfo:
@API.register
def list_mdns_services() -> DNSInfo:
"""List mDNS/DNS-SD services on the local network.
Returns:
DNSInfo: A dictionary containing discovered mDNS/DNS-SD services.
"""
cmd = nix_shell(
["avahi"],
[

View File

@@ -31,15 +31,6 @@ def git_command(directory: Path, *args: str) -> list[str]:
@API.register
def create_clan(opts: CreateOptions) -> None:
"""Create a new clan repository with the specified template.
Args:
opts: CreateOptions containing the destination path, template name,
source flake, and other options.
Raises:
ClanError: If the source flake is not a valid flake or if the destination
directory already exists.
"""
dest = opts.dest.resolve()
if opts.src_flake is not None:

View File

@@ -7,14 +7,6 @@ from clan_lib.persist.inventory_store import InventoryStore
@API.register
def get_clan_details(flake: Flake) -> InventoryMeta:
"""Retrieve the clan details from the inventory of a given flake.
Args:
flake: The Flake instance representing the clan.
Returns:
InventoryMeta: The meta information from the clan's inventory.
Raises:
ClanError: If the flake does not exist, or if the inventory is invalid (missing the meta attribute).
"""
if flake.is_local and not flake.path.exists():
msg = f"Path {flake} does not exist"
raise ClanError(msg, description="clan directory does not exist")

View File

@@ -15,14 +15,6 @@ class UpdateOptions:
@API.register
def set_clan_details(options: UpdateOptions) -> InventorySnapshot:
"""Update the clan metadata in the inventory of a given flake.
Args:
options: UpdateOptions containing the flake and the new metadata.
Returns:
InventorySnapshot: The updated inventory snapshot after modifying the metadata.
Raises:
ClanError: If the flake does not exist or if the inventory is invalid (missing the meta attribute).
"""
inventory_store = InventoryStore(options.flake)
inventory = inventory_store.read()
set_value_by_path(inventory, "meta", options.meta)

View File

@@ -54,19 +54,6 @@ def list_machines(
@API.register
def get_machine(flake: Flake, name: str) -> InventoryMachine:
"""
Retrieve a machine's inventory details by name from the given flake.
Args:
flake (Flake): The flake object representing the configuration source.
name (str): The name of the machine to retrieve from the inventory.
Returns:
InventoryMachine: An instance representing the machine's inventory details.
Raises:
ClanError: If the machine with the specified name is not found in the inventory.
"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()

View File

@@ -19,13 +19,6 @@ log = logging.getLogger(__name__)
@API.register
def delete_machine(machine: Machine) -> None:
"""Delete a machine from the clan's inventory and remove its associated files.
Args:
machine: The Machine instance to be deleted.
Raises:
ClanError: If the machine does not exist in the inventory or if there are issues with
removing its files.
"""
inventory_store = InventoryStore(machine.flake)
try:
inventory_store.delete(

View File

@@ -40,16 +40,6 @@ class InstallOptions:
@API.register
def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
"""Install a machine using nixos-anywhere.
Args:
opts: InstallOptions containing the machine to install, kexec option, debug mode,
no-reboot option, phases, build-on option, hardware config update, password,
identity file, and use_tor flag.
target_host: Remote object representing the target host for installation.
Raises:
ClanError: If the machine is not found in the inventory or if there are issues with
generating facts or variables.
"""
machine = opts.machine
machine.debug(f"installing {machine.name}")

View File

@@ -52,22 +52,8 @@ def extract_header(c: str) -> str:
return "\n".join(header_lines)
# TODO: Remove this function
# Split out the disko schema extraction into a separate function
# get machine returns the machine already
@API.register
def get_machine_details(machine: Machine) -> MachineDetails:
"""Retrieve detailed information about a machine, including its inventory,
hardware configuration, and disk schema if available.
Args:
machine (Machine): The machine instance for which details are to be retrieved.
Returns:
MachineDetails: An instance containing the machine's inventory, hardware configuration,
and disk schema.
Raises:
ClanError: If the machine's inventory cannot be found or if there are issues with the
hardware configuration or disk schema extraction.
"""
machine_inv = get_machine(machine.flake, machine.name)
hw_config = HardwareConfig.detect_type(machine)

View File

@@ -106,16 +106,6 @@ def upload_sources(machine: Machine, ssh: Remote) -> str:
def run_machine_deploy(
machine: Machine, target_host: Remote, build_host: Remote | None
) -> None:
"""Update an existing machine using nixos-rebuild or darwin-rebuild.
Args:
machine: The Machine instance to deploy.
target_host: Remote object representing the target host for deployment.
build_host: Optional Remote object representing the build host.
Raises:
ClanError: If the machine is not found in the inventory or if there are issues with
generating facts or variables.
"""
with ExitStack() as stack:
target_host = stack.enter_context(target_host.ssh_control_master())

View File

@@ -449,21 +449,6 @@ class CheckResult:
def check_machine_ssh_login(
remote: Remote, opts: ConnectionOptions | None = None
) -> CheckResult:
"""Checks if a remote machine is reachable via SSH by attempting to run a simple command.
Args:
remote (Remote): The remote host to check for SSH login.
opts (ConnectionOptions, optional): Connection options such as timeout and number of retries.
If not provided, default values are used.
Returns:
CheckResult: An object indicating whether the SSH login is successful (`ok=True`) or not (`ok=False`),
and a reason if the check failed.
Usage:
result = check_machine_ssh_login(remote)
if result.ok:
print("SSH login successful")
else:
print(f"SSH login failed: {result.reason}")
"""
if opts is None:
opts = ConnectionOptions()
@@ -490,22 +475,6 @@ def check_machine_ssh_login(
def check_machine_ssh_reachable(
remote: Remote, opts: ConnectionOptions | None = None
) -> CheckResult:
"""
Checks if a remote machine is reachable via SSH by attempting to open a TCP connection
to the specified address and port.
Args:
remote (Remote): The remote host to check for SSH reachability.
opts (ConnectionOptions, optional): Connection options such as timeout and number of retries.
If not provided, default values are used.
Returns:
CheckResult: An object indicating whether the SSH port is reachable (`ok=True`) or not (`ok=False`),
and a reason if the check failed.
Usage:
result = check_machine_ssh_reachable(remote)
if result.ok:
print("SSH port is reachable")
print(f"SSH port is not reachable: {result.reason}")
"""
if opts is None:
opts = ConnectionOptions()

View File

@@ -150,7 +150,7 @@ def main() -> None:
errors.extend(check_res)
if not func_schema.get("description"):
errors.append(
warnings.append(
f"{func_name} doesn't have a description. Python docstring is required for an API function."
)