diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index a806510..d969adb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ result result-* +.direnv +.pre-commit-config.yaml diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..bd90606 --- /dev/null +++ b/default.nix @@ -0,0 +1,58 @@ +{ + sources ? import ./npins, + pkgs ? import sources.nixpkgs { }, +}: + +let + inherit (pkgs) lib mkShell; + + git-checks = (import sources.git-hooks).run { + src = ./.; + + hooks = { + statix = { + enable = true; + stages = [ "pre-push" ]; + settings.ignore = [ "npins" ]; + }; + + deadnix = { + enable = true; + stages = [ "pre-push" ]; + }; + + nixfmt-rfc-style = { + enable = true; + stages = [ "pre-push" ]; + }; + + commitizen.enable = true; + }; + }; +in + +{ + devShell = mkShell { + name = "nix-actions.dev"; + + inherit (git-checks) shellHook; + }; + + install = + config: + let + project = lib.evalModules { + modules = [ + ./modules + { + config = config // { + _module.args.pkgs = pkgs; + }; + } + ]; + }; + in + { + shellHook = project.config.installationScript; + }; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..747902b --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./workflows.nix + ]; +} diff --git a/modules/workflows.nix b/modules/workflows.nix new file mode 100644 index 0000000..481795a --- /dev/null +++ b/modules/workflows.nix @@ -0,0 +1,136 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + attrNames + concatMapStringsSep + concatStringsSep + getExe + getExe' + mapAttrsToList + mkOption + optionalString + ; + + inherit (lib.types) + attrsOf + bool + enum + str + ; + + cfg = config; + + generate = + name: value: + pkgs.callPackage ( + { + runCommand, + remarshal, + action-validator, + }: + runCommand "${name}.yaml" + { + nativeBuildInputs = [ + action-validator + remarshal + ]; + + value = builtins.toJSON value; + passAsFile = [ "value" ]; + preferLocalBuild = true; + } + '' + json2yaml "$valuePath" "$out" + + # Check that the workflow is valid + action-validator "$out" + '' + ) { }; + + install = + name: value: + let + result = generate name value; + path = ".${cfg.platform}/workflows/${name}.yaml"; + in + '' + if [ ! -f "$GIT_WC/${path}" ] || ! ${getExe' pkgs.diffutils "cmp"} -s "$GIT_WC/${path}" ${result} ; then + # Copy the updated workflow definition + cp --no-preserve=mode,ownership ${result} "$GIT_WC/${path}" && echo "nix-actions: Updated ${name}.yaml" + fi + ''; +in + +{ + options = { + platform = mkOption { + type = enum [ + "forgejo" + "gitea" + "github" + ]; + default = "forgejo"; + description = '' + The platform where the workflows will run. + This will induce the directory in which the yaml files are installed. + ''; + }; + + workflows = mkOption { + type = attrsOf (pkgs.formats.yaml { }).type; + description = '' + Set of workflows to install. + ''; + }; + + installationScript = mkOption { + type = str; + description = '' + A bash snippet that installs the workflows files in the current project. + ''; + readOnly = true; + }; + + removeUnknown = mkOption { + type = bool; + default = true; + description = '' + Whether to remove unknown workflow files. + ''; + }; + }; + + config = { + installationScript = '' + if ! type -t git >/dev/null; then + # This happens in pure shells, including lorri + echo 1>&2 "WARNING: nix-actions: git command not found; skipping installation." + elif ! git rev-parse --git-dir &> /dev/null; then + echo 1>&2 "WARNING: nix-actions: .git not found; skipping installation." + else + GIT_WC=`git rev-parse --show-toplevel` + + # Ensure that the directory exists + mkdir -p "$GIT_WC/.${cfg.platform}/workflows" + + # Install the workflow files + ${concatStringsSep "\n" (mapAttrsToList install cfg.workflows)} + + ${optionalString cfg.removeUnknown '' + # Remove unknown workflow files + for file in $(ls "$GIT_WC/.${cfg.platform}/workflows" | ${getExe pkgs.gnugrep} -v '\(${ + concatMapStringsSep "|" (name: "${name}.yaml") (attrNames cfg.workflows) + }\)'); do + rm "$GIT_WC/.${cfg.platform}/workflows/$file" && echo "nix-actions: Removed $file" + done + ''} + fi + ''; + }; +} diff --git a/npins/default.nix b/npins/default.nix new file mode 100644 index 0000000..af6f9c8 --- /dev/null +++ b/npins/default.nix @@ -0,0 +1,79 @@ +# Generated by npins. Do not modify; will be overwritten regularly +let + data = builtins.fromJSON (builtins.readFile ./sources.json); + version = data.version; + + mkSource = + spec: + assert spec ? type; + let + path = + if spec.type == "Git" then + mkGitSource spec + else if spec.type == "GitRelease" then + mkGitSource spec + else if spec.type == "PyPi" then + mkPyPiSource spec + else if spec.type == "Channel" then + mkChannelSource spec + else + builtins.throw "Unknown source type ${spec.type}"; + in + spec // { outPath = path; }; + + mkGitSource = + { + repository, + revision, + url ? null, + hash, + ... + }: + assert repository ? type; + # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository + # In the latter case, there we will always be an url to the tarball + if url != null then + (builtins.fetchTarball { + inherit url; + sha256 = hash; # FIXME: check nix version & use SRI hashes + }) + else + assert repository.type == "Git"; + let + urlToName = + url: rev: + let + matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url; + + short = builtins.substring 0 7 rev; + + appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; + in + "${if matched == null then "source" else builtins.head matched}${appendShort}"; + name = urlToName repository.url revision; + in + builtins.fetchGit { + url = repository.url; + rev = revision; + inherit name; + # hash = hash; + }; + + mkPyPiSource = + { url, hash, ... }: + builtins.fetchurl { + inherit url; + sha256 = hash; + }; + + mkChannelSource = + { url, hash, ... }: + builtins.fetchTarball { + inherit url; + sha256 = hash; + }; +in +if version == 3 then + builtins.mapAttrs (_: mkSource) data.pins +else + throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" diff --git a/npins/sources.json b/npins/sources.json new file mode 100644 index 0000000..aedf8f4 --- /dev/null +++ b/npins/sources.json @@ -0,0 +1,23 @@ +{ + "pins": { + "git-hooks": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "cachix", + "repo": "git-hooks.nix" + }, + "branch": "master", + "revision": "d70155fdc00df4628446352fc58adc640cd705c2", + "url": "https://github.com/cachix/git-hooks.nix/archive/d70155fdc00df4628446352fc58adc640cd705c2.tar.gz", + "hash": "1s4w7bnign9lfzm8bm9j0zkvqfh5f1x671jp4g61psq42v5cfqvx" + }, + "nixpkgs": { + "type": "Channel", + "name": "nixpkgs-unstable", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre704822.85f7e662eda4/nixexprs.tar.xz", + "hash": "0dqlz0xqd3nn49hnx943y5sfqd7nmj25s6gi1pjm907j3vbgg47k" + } + }, + "version": 3 +} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d6d21cf --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +(import ./. { }).devShell