From 545ae4d569bd7cde65adb7fc8d2fa6f3e38112cb Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Thu, 25 Dec 2025 22:09:58 +0800 Subject: [PATCH 1/5] fix(deps): upgrade nixpkgs to v25.11, bump flake inputs --- flake.lock | 12 ++++++------ flake.nix | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 6c006c6..34d6f67 100644 --- a/flake.lock +++ b/flake.lock @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1761468971, - "narHash": "sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8=", - "owner": "nixos", + "lastModified": 1766473571, + "narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "78e34d1667d32d8a0ffc3eba4591ff256e80576e", + "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-25.05", + "owner": "NixOS", + "ref": "nixos-25.11", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index d836a3b..7676611 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Haskell Project Template"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; flake-utils.url = "github:numtide/flake-utils"; }; @@ -43,7 +43,8 @@ ## Build inputs for development shell: buildInputs = [ ## Haskell related build inputs: - thisHaskell.apply-refact + ## TODO: Once we are on ghc > 9.10, enable apply-refact again. + # thisHaskell.apply-refact thisHaskell.cabal-fmt thisHaskell.cabal-install thisHaskell.cabal2nix From f8e2ba9b978ae6059a464d54da4f91761a6957aa Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 10 Jan 2026 10:53:40 +0300 Subject: [PATCH 2/5] chore(nix): update flake.nix description --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 7676611..c877463 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Haskell Project Template"; + description = "opsops - SOPS(-Nix) Goodies"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; From a53cd8f3ab07e1110d2d9423c467bc7ac5f2ea47 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 10 Jan 2026 11:07:07 +0300 Subject: [PATCH 3/5] refactor(dev): adopt latest updates to vst/haskell-template-hebele --- .github/workflows/check-static.yaml | 16 ++ .../{check.yaml => check-verify.yaml} | 14 +- .github/workflows/release.yml | 14 +- .gitignore | 3 + .prettierignore | 4 +- .prettierrc.json | 9 +- .stan.toml | 8 + .taplo.toml | 25 ++ README.md | 9 +- build-static.sh | 58 ++--- flake.lock | 46 ++-- flake.nix | 236 ++++++++++-------- nix/cabal-verify/default.nix | 26 ++ nix/cabal-verify/script.sh | 182 ++++++++++++++ nix/dev-test-build.sh | 179 ------------- nix/flake-modules/read-yaml/default.nix | 9 + .../read-yaml/function.nix} | 8 +- stack.yaml | 31 +++ 18 files changed, 514 insertions(+), 363 deletions(-) create mode 100644 .github/workflows/check-static.yaml rename .github/workflows/{check.yaml => check-verify.yaml} (51%) create mode 100644 .stan.toml create mode 100644 .taplo.toml create mode 100644 nix/cabal-verify/default.nix create mode 100644 nix/cabal-verify/script.sh delete mode 100644 nix/dev-test-build.sh create mode 100644 nix/flake-modules/read-yaml/default.nix rename nix/{read-yaml.nix => flake-modules/read-yaml/function.nix} (76%) create mode 100644 stack.yaml diff --git a/.github/workflows/check-static.yaml b/.github/workflows/check-static.yaml new file mode 100644 index 0000000..3570368 --- /dev/null +++ b/.github/workflows/check-static.yaml @@ -0,0 +1,16 @@ +name: "Check Static Build" + +on: + workflow_dispatch: + +jobs: + check: + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout Codebase" + uses: "actions/checkout@v6" + + - name: "Build Static Exectutable" + run: | + bash ./build-static.sh diff --git a/.github/workflows/check.yaml b/.github/workflows/check-verify.yaml similarity index 51% rename from .github/workflows/check.yaml rename to .github/workflows/check-verify.yaml index eb34486..c20daf1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check-verify.yaml @@ -1,4 +1,4 @@ -name: "Check, Test and Build Codebase" +name: "Check, Lint, Test and Build Codebase" on: pull_request: @@ -10,14 +10,18 @@ jobs: steps: - name: "Checkout Codebase" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" - name: "Install Nix" - uses: "DeterminateSystems/nix-installer-action@v20" + uses: "DeterminateSystems/nix-installer-action@v21" - - name: "Check, Test and Build" + - name: "Prepare CI devShell" run: | - nix develop --command bash -c "cabal update --ignore-project && cabal dev-test-build" + nix develop .#ci --command true + + - name: "Verify Codebase" + run: | + nix develop .#ci --command cabal verify - name: "Build Docker Image" run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5abf165..a4f5605 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,20 +20,24 @@ jobs: - name: "Checkout Codebase" if: "${{ steps.release.outputs.release_created }}" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" + with: + fetch-depth: 0 - name: "Install Nix" if: "${{ steps.release.outputs.release_created }}" - uses: "DeterminateSystems/nix-installer-action@v20" + uses: "DeterminateSystems/nix-installer-action@v21" - - name: "Build Statically Compiled Executable" + - name: "Build Static Exectutable" + id: "build_static" if: "${{ steps.release.outputs.release_created }}" run: | - nix develop --command bash build-static.sh + bash ./build-static.sh | tee /tmp/build.log + echo "executable=$(tail -n1 /tmp/build.log)" >> $GITHUB_OUTPUT - name: "Upload Release Artifact" if: "${{ steps.release.outputs.release_created }}" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" run: | - gh release upload "${{ steps.release.outputs.tag_name }}" /tmp/opsops-static-linux-x86_64 + gh release upload "${{ steps.release.outputs.tag_name }}" "${{ steps.build_static.outputs.executable }}" diff --git a/.gitignore b/.gitignore index f12f254..c99cc38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ *.cabal *~ /.direnv +/.envrc +/.stack-work /dist /dist-newstyle /result +/stack.yaml.lock /tmp spec.yaml diff --git a/.prettierignore b/.prettierignore index a8a21b4..2aa6043 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,4 @@ +CHANGELOG.md +LICENSE.md dist-newstyle/ dist/ -nix/ -*.md diff --git a/.prettierrc.json b/.prettierrc.json index aae5213..51edb1d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,9 +1,16 @@ { "tabWidth": 2, + "printWidth": 120, "singleQuote": false, "trailingComma": "es5", - "printWidth": 120, "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 80, + "proseWrap": "always" + } + }, { "files": "package.yaml", "options": { diff --git a/.stan.toml b/.stan.toml new file mode 100644 index 0000000..54246a4 --- /dev/null +++ b/.stan.toml @@ -0,0 +1,8 @@ +# Big tuples +# Using tuples of big size (>= 4) can decrease code readability +# In serveral places Stack uses 4-tuples and in one place Stack uses a +# 5-tuple. +[[check]] +id = "STAN-0302" +scope = "all" +type = "Exclude" diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..93da9dd --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,25 @@ +#:schema taplo://taplo.toml + +include = ["*.toml"] +exclude = [] + +[formatting] + +align_entries = false # Align entries vertically. Entries that have table headers, comments, or blank lines between them are not aligned.(default false) +align_comments = true # Align consecutive comments after entries and items vertically. This applies to comments that are after entries or array items.(default true) +array_trailing_comma = true # Put trailing commas for multiline arrays.(default true) +array_auto_expand = true # Automatically expand arrays to multiple lines (default true) +array_auto_collapse = false # Automatically collapse arrays if they fit in one line.(default true) +compact_arrays = true # Omit whitespace padding inside single-line arrays.(default true) +compact_inline_tables = false # Omit whitespace padding inside inline tables.(default false) +inline_table_expand = true # Expand values (e.g. arrays) inside inline tables.(default true) +compact_entries = false # Omit whitespace around =. (default false) +column_width = 80 # Target maximum column width after which arrays are expanded into new lines.(default 80) +indent_tables = false # Indent subtables if they come in order(default false) +indent_entries = false # Indent entries under tables.(default false) +indent_string = " " # Indentation to use, should be tabs or spaces but technically could be anything. 2 spaces (" ") +trailing_newline = true # Add trailing newline to the source. (default true) +reorder_keys = false # Alphabetically reorder keys that are not separated by blank lines. (default false) +reorder_arrays = false # Alphabetically reorder array values that are not separated by blank lines. (default false) +allowed_blank_lines = 1 # The maximum amount of consecutive blank lines allowed. (default 2) +crlf = false # Use CRLF line endings. (default false) diff --git a/README.md b/README.md index 261adc0..29d3ca0 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,12 @@ hpack && direnv reload && fourmolu -i app/ src/ test/ && prettier --write . && - find . -iname "*.nix" -print0 | xargs --null nixpkgs-fmt && + find . -iname "*.nix" -print0 | xargs --null nixfmt && + statix check && + find . -iname "*.sh" -print0 | xargs --null shfmt -w && + find . -iname "*.sh" -print0 | xargs --null shellcheck && + taplo lint && + taplo format && hlint app/ src/ test/ && cabal build -O0 && cabal run -O0 opsops -- --version && @@ -318,7 +323,7 @@ hpack && To check and build: ```sh -cabal dev-test-build [-c] +cabal verify [-c] ``` ## License diff --git a/build-static.sh b/build-static.sh index f9152f3..f6374f1 100644 --- a/build-static.sh +++ b/build-static.sh @@ -1,22 +1,21 @@ #!/usr/bin/env bash ## NOTE: Things would be much easier if we could use Nix, but we can -## not (or I find it rather tedious). So, we have to use Docker. -## -## Also, `cabal install` does not work with -## `--enable-executable-static` flag. So, we have to use `cabal build` -## instead. Finally, `cabal build` does not work with -## `--enable-executable-stripping`, hence the `strip` command usage. +## not (or I find it rather tedious). So, we have to use Docker and +## Haskell Stack to build our static binary. + +## Executable name: +EXECUTABLE_NAME="$(yq ".executables | keys | .[0]" package.yaml)" + +## Stackage resolver: +STACKAGE_RESOLVER="$(yq ".resolver" stack.yaml)" ## GHC version: -GHC_VERSION="9.8.4" +GHC_VERSION="$(curl -s "https://www.stackage.org/${STACKAGE_RESOLVER}" | grep -oP 'ghc-\K[0-9.]+' | head -n1)" ## Docker image: DOCKER_IMAGE="quay.io/benz0li/ghc-musl:${GHC_VERSION}" -## Executable name: -EXECUTABLE_NAME="opsops" - ## Final executable name: FINAL_EXECUTABLE_NAME="${EXECUTABLE_NAME}-static-$(uname --kernel-name | tr '[:upper:]' '[:lower:]')-$(uname --machine)" @@ -26,43 +25,34 @@ FINAL_EXECUTABLE_PATH="/tmp/${FINAL_EXECUTABLE_NAME}" ## Docker container name: CONTAINER_NAME="static-builder-for-${EXECUTABLE_NAME}" -## Create/update .cabal file: -hpack - -## Cleanup first: -cabal clean -cabal v1-clean - -## First, pin all packages as per Nix: -cabal freeze - ## Run the Docker container: docker run -i --detach -v "$(pwd):/app" --name "${CONTAINER_NAME}" "${DOCKER_IMAGE}" /bin/bash ## Whitelist codebase directory for Git queries: docker exec "${CONTAINER_NAME}" git config --global --add safe.directory /app -## Update cabal database: -docker exec "${CONTAINER_NAME}" cabal update +## Cleanup inside the container: +docker exec -w "/app" "${CONTAINER_NAME}" cabal clean +docker exec -w "/app" "${CONTAINER_NAME}" cabal v1-clean +docker exec -w "/app" "${CONTAINER_NAME}" stack clean --full ## Build the static binary: -docker exec -w "/app" "${CONTAINER_NAME}" cabal build --enable-executable-static +docker exec -w "/app" "${CONTAINER_NAME}" stack build -## Get the path to the executable: -BUILD_PATH="$(docker exec -w "/app" "${CONTAINER_NAME}" cabal list-bin "${EXECUTABLE_NAME}")" +## Install the static binary to our local-bin-path (/tmp): +docker exec -w "/app" "${CONTAINER_NAME}" stack install -## Strip debugging symbols: -docker exec "${CONTAINER_NAME}" strip "${BUILD_PATH}" - -## Copy the binary to the host: -docker cp "${CONTAINER_NAME}:${BUILD_PATH}" "${FINAL_EXECUTABLE_PATH}" +## Install upx: +docker exec -w "/app" "${CONTAINER_NAME}" apk add upx ## Compress the executable: -upx "${FINAL_EXECUTABLE_PATH}" +docker exec -w "/app" "${CONTAINER_NAME}" upx "/tmp/${EXECUTABLE_NAME}" + +## Copy the binary to the host: +docker cp "${CONTAINER_NAME}:/tmp/${EXECUTABLE_NAME}" "${FINAL_EXECUTABLE_PATH}" ## Cleanup: -docker exec -w "/app" "${CONTAINER_NAME}" cabal clean -docker exec -w "/app" "${CONTAINER_NAME}" cabal v1-clean +docker exec -w "/app" "${CONTAINER_NAME}" stack clean --full docker rm -f "${CONTAINER_NAME}" -rm cabal.project.freeze file "${FINAL_EXECUTABLE_PATH}" +find "${FINAL_EXECUTABLE_PATH}" diff --git a/flake.lock b/flake.lock index 34d6f67..b1a714b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,20 +1,20 @@ { "nodes": { - "flake-utils": { + "flake-parts": { "inputs": { - "systems": "systems" + "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1765835352, + "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "a34fae9c08a15ad73f295041fec82323541400a9", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, @@ -34,26 +34,26 @@ "type": "github" } }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { + "nixpkgs-lib": { "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", + "owner": "nix-community", + "repo": "nixpkgs.lib", "type": "github" } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index c877463..39750a5 100644 --- a/flake.nix +++ b/flake.nix @@ -3,132 +3,154 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; - flake-utils.url = "github:numtide/flake-utils"; + flake-parts.url = "github:hercules-ci/flake-parts"; }; - outputs = { self, nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - ## Import nixpkgs: - pkgs = import nixpkgs { inherit system; }; - - ## Load readYAML helper: - readYAML = pkgs.callPackage ./nix/read-yaml.nix { }; - - ## Read package information: - package = readYAML ./package.yaml; - - ## Get our Haskell: - thisHaskell = pkgs.haskellPackages.override { - overrides = self: super: { - ${package.name} = self.callCabal2nix package.name ./. { }; + outputs = + inputs@{ nixpkgs, flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + ./nix/flake-modules/read-yaml + ]; + + systems = nixpkgs.lib.systems.flakeExposed; + + perSystem = + { + config, + self', + inputs', + pkgs, + system, + readYAML, + ... + }: + let + ## Read package information: + package = readYAML ./package.yaml; + + ## Get our Haskell: + thisHaskell = pkgs.haskellPackages.override { + overrides = self: super: { + ${package.name} = self.callCabal2nix package.name ./. { }; + }; }; - }; - - ## Prepare dev-test-build script: - dev-test-build = pkgs.writeShellApplication { - name = "cabal-dev-test-build"; - text = builtins.readFile ./nix/dev-test-build.sh; - runtimeInputs = [ pkgs.bash pkgs.bc pkgs.moreutils ]; - }; - - ## Prepare Nix shell: - thisShell = thisHaskell.shellFor { - ## Define packages for the shell: - packages = p: [ p.${package.name} ]; - - ## Enable Hoogle: - withHoogle = false; - ## Build inputs for development shell: - buildInputs = [ - ## Haskell related build inputs: - ## TODO: Once we are on ghc > 9.10, enable apply-refact again. - # thisHaskell.apply-refact - thisHaskell.cabal-fmt + ## Common build inputs for both development and CI environments: + buildInputsCommon = [ + ## Essential Haskell tools: thisHaskell.cabal-install - thisHaskell.cabal2nix thisHaskell.fourmolu - thisHaskell.haskell-language-server thisHaskell.hlint thisHaskell.hpack + thisHaskell.stan thisHaskell.weeder + ## Other essentials: + pkgs.git + pkgs.nixfmt-rfc-style + pkgs.prettier + pkgs.shellcheck + pkgs.shfmt + pkgs.statix + pkgs.taplo + ## Our development scripts: - dev-test-build + (pkgs.callPackage ./nix/cabal-verify { }) + ]; - ## Other build inputs for various development requirements: + ## Development-only inputs: + buildInputsDevOnly = [ + ## Haskell development tools: + thisHaskell.haskell-language-server + thisHaskell.cabal-fmt + thisHaskell.cabal2nix + + ## Other development tools: pkgs.docker-client - pkgs.git pkgs.nil - pkgs.nixpkgs-fmt - pkgs.nodePackages.prettier - pkgs.upx ]; - }; - thisPackage = pkgs.haskell.lib.justStaticExecutables ( - thisHaskell.${package.name}.overrideAttrs (oldAttrs: { - nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ - pkgs.git - pkgs.installShellFiles - pkgs.makeWrapper - pkgs.ronn - ]; - - postFixup = (oldAttrs.postFixup or "") + '' - ## Create output directories: - mkdir -p $out/{bin} - - ## Wrap program to add PATHs to dependencies: - wrapProgram $out/bin/${package.name} --prefix PATH : ${pkgs.lib.makeBinPath [ - pkgs.bashInteractive ## Added for bash-based CLI option completions - ]} - - ## Install completion scripts: - installShellCompletion --bash --name ${package.name}.bash <($out/bin/${package.name} --bash-completion-script "$out/bin/${package.name}") - installShellCompletion --fish --name ${package.name}.fish <($out/bin/${package.name} --fish-completion-script "$out/bin/${package.name}") - installShellCompletion --zsh --name _${package.name} <($out/bin/${package.name} --zsh-completion-script "$out/bin/${package.name}") - ''; - }) - ); - - thisDocker = pkgs.dockerTools.buildImage { - name = "${package.name}"; - tag = "v${package.version}"; - created = "now"; - - copyToRoot = pkgs.buildEnv { - name = "image-root"; - paths = [ pkgs.cacert ]; - pathsToLink = [ "/etc" ]; + ## Development shell: + devShell = thisHaskell.shellFor { + packages = p: [ p.${package.name} ]; + withHoogle = false; + buildInputs = buildInputsCommon ++ buildInputsDevOnly; }; - runAsRoot = '' - #!${pkgs.runtimeShell} - ${pkgs.dockerTools.shadowSetup} - groupadd -r users - useradd -r -g users patron - ''; - - config = { - User = "patron"; - Entrypoint = [ "${thisPackage}/bin/${package.name}" ]; - Cmd = null; + ## CI shell (minimal, fast): + ciShell = thisHaskell.shellFor { + packages = p: [ p.${package.name} ]; + withHoogle = false; + buildInputs = buildInputsCommon; }; - }; - in - { - ## Project packages output: - packages = { - "${package.name}" = thisPackage; - docker = thisDocker; - default = self.packages.${system}.${package.name}; - }; - ## Project development shell output: - devShells = { - default = thisShell; + thisPackage = pkgs.haskell.lib.justStaticExecutables ( + thisHaskell.${package.name}.overrideAttrs (oldAttrs: { + nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ + pkgs.git + pkgs.installShellFiles + pkgs.makeWrapper + pkgs.ronn + ]; + + postFixup = (oldAttrs.postFixup or "") + '' + ## Create output directories: + mkdir -p $out/{bin} + + ## Wrap program to add PATHs to dependencies: + wrapProgram $out/bin/${package.name} --prefix PATH : ${ + pkgs.lib.makeBinPath [ + pkgs.bashInteractive # Added for bash-based CLI option completions + ] + } + + ## Install completion scripts: + installShellCompletion --bash --name ${package.name}.bash <($out/bin/${package.name} --bash-completion-script "$out/bin/${package.name}") + installShellCompletion --fish --name ${package.name}.fish <($out/bin/${package.name} --fish-completion-script "$out/bin/${package.name}") + installShellCompletion --zsh --name _${package.name} <($out/bin/${package.name} --zsh-completion-script "$out/bin/${package.name}") + ''; + }) + ); + + thisDocker = pkgs.dockerTools.buildImage { + name = "${package.name}"; + tag = "v${package.version}"; + created = "now"; + + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = [ pkgs.cacert ]; + pathsToLink = [ "/etc" ]; + }; + + runAsRoot = '' + #!${pkgs.runtimeShell} + ${pkgs.dockerTools.shadowSetup} + groupadd -r users + useradd -r -g users patron + ''; + + config = { + User = "patron"; + Entrypoint = [ "${thisPackage}/bin/${package.name}" ]; + Cmd = null; + }; + }; + in + { + ## Project packages output: + packages = { + "${package.name}" = thisPackage; + docker = thisDocker; + default = thisPackage; + }; + + ## Project development shells: + devShells = { + default = devShell; + ci = ciShell; + }; }; - }); + }; } diff --git a/nix/cabal-verify/default.nix b/nix/cabal-verify/default.nix new file mode 100644 index 0000000..f385fba --- /dev/null +++ b/nix/cabal-verify/default.nix @@ -0,0 +1,26 @@ +{ + lib, + writeShellApplication, + bash, + coreutils, + moreutils, + yq-go, +}: + +writeShellApplication { + name = "cabal-verify"; + + text = builtins.readFile ./script.sh; + + runtimeInputs = [ + bash + coreutils + moreutils + yq-go + ]; + + meta = with lib; { + description = "Run project verification checks (format, lint, build, test, docs, etc...)"; + platforms = platforms.all; + }; +} diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh new file mode 100644 index 0000000..3e5cdf3 --- /dev/null +++ b/nix/cabal-verify/script.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +############### +## VARIABLES ## +############### + +_clean=false +_cabal="$(command -v cabal)" + +##################### +## TERMINAL COLORS ## +##################### + +if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then + BOLD="$(tput bold)" + RESET="$(tput sgr0)" + RED="$(tput setaf 1)" + GREEN="$(tput setaf 2)" + BLUE="$(tput setaf 4)" +else + BOLD="" + RESET="" + RED="" + GREEN="" + BLUE="" +fi + +####################### +## UTILITY FUNCTIONS ## +####################### + +_usage() { + echo "Usage: ${0} [OPTIONS]" + echo "" + echo " Runs all checks and tests for the project." + echo "" + echo "Options:" + echo " -c, --clean Clean the project before running tests." + echo " -h, --help Show this help message and exit." +} + +_suc() { + printf "%s%s✅ %s%s\n" "${BOLD}" "${GREEN}" "${1}" "${RESET}" +} + +_err() { + printf "%s%s❌ %s%s\n" "${BOLD}" "${RED}" "${1}" "${RESET}" +} + +_get_now_ms() { + date +%s%3N +} + +_diff_ms_to_s() { + local _ms="${1}" + printf '%d.%03d' "$((_ms / 1000))" "$((_ms % 1000))" +} + +_run_check() { + local _title="${1}" + shift + + printf "%s%s🔵 Running %s%s " "${BOLD}" "${BLUE}" "${_title}" "${RESET}" + + local _ms_since + local _ms_until + local _ms_total + local _captured + + _ms_since="$(_get_now_ms)" + + if _captured="$(chronic -- "${@}" 2>&1)"; then + _ms_until="$(_get_now_ms)" + _ms_total=$((_ms_until - _ms_since)) + _suc "$(_diff_ms_to_s "${_ms_total}")s" + else + _ms_until="$(_get_now_ms)" + _ms_total=$((_ms_until - _ms_since)) + _err "$(_diff_ms_to_s "${_ms_total}")s" + >&2 printf "%s\n" "${_captured}" + exit 1 + fi +} + +#################################### +## COMMAND-LINE ARGUMENTS PARSING ## +#################################### + +# cabal external command support +if [[ -n "${CABAL:-}" ]]; then + _cabal="${CABAL}" + shift +fi + +while [[ $# -gt 0 ]]; do + case "${1}" in + --) + shift + break + ;; + -c | --clean) + _clean=true + shift + ;; + -h | --help) + _usage + exit 0 + ;; + -*) + _usage + echo "" + >&2 _err "Invalid option: ${1}" + exit 1 + ;; + *) + break + ;; + esac +done + +############### +## PROCEDURE ## +############### + +_script_start_ms="$(_get_now_ms)" + +${_clean} && _run_check "clean" "${_cabal}" clean && _run_check "v1-clean" "${_cabal}" v1-clean + +_run_check "hpack (v$(hpack --numeric-version))" \ + hpack + +_run_check "nixfmt (v$(nixfmt --numeric-version))" \ + find . -type f -iname "*.nix" -exec nixfmt --check {} + + +_run_check "statix (v$(statix --version | cut -f2 -d" "))" \ + statix check + +_run_check "shfmt (v$(shfmt --version))" \ + find . -type f -iname "*.sh" -exec shfmt --diff {} + + +_run_check "shellcheck (v$(shellcheck --version | grep "^version" | head -n1 | cut -f2 -d" "))" \ + find . -type f -iname "*.sh" -exec shellcheck {} + + +_run_check "prettier (v$(prettier --version))" \ + prettier --check . + +_run_check "taplo lint (v$(taplo --version | cut -f2 -d" "))" \ + taplo lint + +_run_check "taplo format (v$(taplo --version | cut -f2 -d" "))" \ + taplo format --check + +_run_check "fourmolu (v$(fourmolu --version | head -n1 | cut -f2 -d" "))" \ + fourmolu --quiet --mode check app/ src/ test/ + +_run_check "hlint (v$(hlint --numeric-version))" \ + hlint app/ src/ test/ + +_run_check "cabal build (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" build -O0 + +_run_check "cabal run (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" run "$(yq ".executables | keys | .[0]" package.yaml)" -O0 -- --version + +_run_check "cabal test (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" v1-test --ghc-options="-O0" + +_run_check "weeder (v$(weeder --version | head -n1 | cut -f3 -d" "))" \ + weeder + +_run_check "stan ($(stan --version | head -n1 | cut -f2 -d" "))" \ + stan --hiedir ./dist-newstyle + +_run_check "cabal haddock (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" haddock -O0 \ + --haddock-quickjump \ + --haddock-hyperlink-source \ + --haddock-html-location="https://hackage.haskell.org/package/\$pkg-\$version/docs" + +_suc "All checks passed in $(_diff_ms_to_s $(($(_get_now_ms) - _script_start_ms)))s." diff --git a/nix/dev-test-build.sh b/nix/dev-test-build.sh deleted file mode 100644 index 3662f34..0000000 --- a/nix/dev-test-build.sh +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env bash - -## Purpose: This script is used to run all the necessary checks and -## tests for the project. - -## Fail on any error: -set -e - -## Declare default styles: -_sty_bold="" -_sty_underline="" -_sty_standout="" -_sty_normal="" -_sty_black="" -_sty_red="" -_sty_green="" -_sty_yellow="" -_sty_blue="" -_sty_magenta="" -_sty_cyan="" -_sty_white="" - -## Set styles if we are on terminal: -if test -t 1; then - ## Check if the terminal supports colors: - ncolors=$(tput colors) - - ## Defines styles: - if test -n "$ncolors" && test "${ncolors}" -ge 8; then - _sty_bold="$(tput bold)" - _sty_underline="$(tput smul)" - _sty_standout="$(tput smso)" - _sty_normal="$(tput sgr0)" - _sty_black="$(tput setaf 0)" - _sty_red="$(tput setaf 1)" - _sty_green="$(tput setaf 2)" - _sty_yellow="$(tput setaf 3)" - _sty_blue="$(tput setaf 4)" - _sty_magenta="$(tput setaf 5)" - _sty_cyan="$(tput setaf 6)" - _sty_white="$(tput setaf 7)" - fi -fi - -_clean="" - -## Are we being run via cabal command? -if [ -n "${CABAL:-}" ] && [ "${1}" = "dev-test-build" ]; then - shift -fi - -while getopts ":c" opt; do - case ${opt} in - c) - _clean="true" - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done - -_get_now() { - t=${EPOCHREALTIME} # remove the decimal separator (s → µs) - t=${t%???} # remove the last three digits (µs → ms) - echo "${t}" -} - -_get_diff() { - printf "scale=3; %s - %s\n" "${2}" "${1}" | bc -} - -_print_header() { - printf "${_sty_bold}${_sty_blue}🔵 Running %s${_sty_normal}" "${1}" -} - -_print_success() { - _start="${1}" - _until="${2}" - _elapsed=$(_get_diff "${_start}" "${_until}") - printf "${_sty_bold}${_sty_green} ✅ %ss${_sty_normal}\n" "${_elapsed}" -} - -_clean() { - _print_header "clean" - _start=$(_get_now) - chronic -- cabal clean && chronic -- cabal v1-clean - _print_success "${_start}" "$(_get_now)" -} - -_hpack() { - _print_header "hpack (v$(hpack --numeric-version))" - _start=$(_get_now) - chronic -- hpack - _print_success "${_start}" "$(_get_now)" -} - -_fourmolu() { - _print_header "fourmolu (v$(fourmolu --version | head -n1 | cut -d' ' -f2))" - _start=$(_get_now) - chronic -- fourmolu --quiet --mode check app/ src/ test/ - _print_success "${_start}" "$(_get_now)" -} - -_prettier() { - _print_header "prettier (v$(prettier --version))" - _start=$(_get_now) - chronic -- prettier --check . - _print_success "${_start}" "$(_get_now)" -} - -_nixpkgs_fmt() { - _print_header "nixpkgs-fmt (v$(nixpkgs-fmt --version 2>&1 | cut -d' ' -f2))" - _start=$(_get_now) - chronic -- find . -iname "*.nix" -exec nixpkgs-fmt --check {} \; - _print_success "${_start}" "$(_get_now)" -} - -_hlint() { - _print_header "hlint (v$(hlint --numeric-version))" - _start=$(_get_now) - chronic -- hlint app/ src/ test/ - _print_success "${_start}" "$(_get_now)" -} - -_cabal_build() { - _print_header "cabal build (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal build -O0 - _print_success "${_start}" "$(_get_now)" -} - -_cabal_run() { - _print_header "cabal run (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal run -O0 opsops -- --version - _print_success "${_start}" "$(_get_now)" -} - -_cabal_test() { - _print_header "cabal test (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal v1-test --ghc-options=-O0 - _print_success "${_start}" "$(_get_now)" -} - -_weeder() { - _print_header "weeder (v$(weeder --version | head -n1 | cut -d' ' -f3))" - _start=$(_get_now) - chronic -- weeder - _print_success "${_start}" "$(_get_now)" -} - -_cabal_haddock() { - _print_header "cabal haddock (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal haddock -O0 \ - --haddock-quickjump \ - --haddock-hyperlink-source \ - --haddock-html-location="https://hackage.haskell.org/package/\$pkg-\$version/docs" - _print_success "${_start}" "$(_get_now)" -} - -_scr_start=$(_get_now) -if [ -n "${_clean}" ]; then - _clean -fi -_hpack -_fourmolu -_prettier -_nixpkgs_fmt -_hlint -_cabal_build -_cabal_run -_cabal_test -_weeder -_cabal_haddock -printf "Finished all in %ss\n" "$(_get_diff "${_scr_start}" "$(_get_now)")" diff --git a/nix/flake-modules/read-yaml/default.nix b/nix/flake-modules/read-yaml/default.nix new file mode 100644 index 0000000..689b80d --- /dev/null +++ b/nix/flake-modules/read-yaml/default.nix @@ -0,0 +1,9 @@ +_: { + perSystem = + { pkgs, ... }: + { + _module.args = { + readYAML = pkgs.callPackage ./function.nix { }; + }; + }; +} diff --git a/nix/read-yaml.nix b/nix/flake-modules/read-yaml/function.nix similarity index 76% rename from nix/read-yaml.nix rename to nix/flake-modules/read-yaml/function.nix index d89721a..472d172 100644 --- a/nix/read-yaml.nix +++ b/nix/flake-modules/read-yaml/function.nix @@ -18,10 +18,8 @@ path: let - jsonOutputDrv = - runCommand - "from-yaml" - { nativeBuildInputs = [ remarshal ]; } - "remarshal -if yaml -i \"${path}\" -of json -o \"$out\""; + jsonOutputDrv = runCommand "from-yaml" { + nativeBuildInputs = [ remarshal ]; + } "remarshal -if yaml -i \"${path}\" -of json -o \"$out\""; in builtins.fromJSON (builtins.readFile jsonOutputDrv) diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..6687f3d --- /dev/null +++ b/stack.yaml @@ -0,0 +1,31 @@ +## This resolver should correspond to our nixpkgs version (at least baseline): +resolver: lts-24.25 + +## Our local packages: +packages: + - . + +## Use the GHC provided by our Docker build image; never download another one: +system-ghc: true +install-ghc: false + +## Allow running as a different user inside Docker containers: +allow-different-user: true + +## Set the output path for our compiled binaries (stack install): +local-bin-path: /tmp + +## Configure Cabal to build static executables (no shared/dynamic): +configure-options: + "$locals": + - --disable-shared + - --disable-executable-dynamic + - --enable-executable-static + +## Make smaller libs without any profiling: +build: + library-profiling: false + library-stripping: true + +## Extra dependencies pinned to specific commits: +extra-deps: [] From 08479c1f738d9402129e19b332b19e7689d12f4b Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 10 Jan 2026 11:32:00 +0300 Subject: [PATCH 4/5] refactor(format): reformat codebase --- README.md | 43 ++++++++++++++++++++++++------------------- weeder.toml | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 29d3ca0..cd9bbd3 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,15 @@ ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/vst/opsops/main) ![GitHub License](https://img.shields.io/github/license/vst/opsops) -`opsops` is a command-line application to generate clear [SOPS] -secrets from a given specification and generate [sops-nix] snippets -for it. +`opsops` is a command-line application to generate clear [SOPS] secrets from a +given specification and generate [sops-nix] snippets for it. -The specification is a YAML/JSON file representing a tree-like -structure where terminal nodes represent how the clear secrets will be -generated, and internal nodes represent the "path" to the clear -secret. +The specification is a YAML/JSON file representing a tree-like structure where +terminal nodes represent how the clear secrets will be generated, and internal +nodes represent the "path" to the clear secret. -Currently, system processes, scripts and 1password field reference -URIs are supported: +Currently, system processes, scripts and 1password field reference URIs are +supported: ```yaml secrets: @@ -29,7 +27,7 @@ secrets: token: type: "script" value: - content: "printf \"%s\" \"$(gh auth token)\"" + content: 'printf "%s" "$(gh auth token)"' example.com: password: type: "script" @@ -59,6 +57,7 @@ secrets: ``` + - [opsops: SOPS(-Nix) Goodies](#opsops-sops-nix-goodies) - [Installation](#installation) - [Usage](#usage) @@ -68,14 +67,15 @@ secrets: - [Create Snippet for `sops-nix`](#create-snippet-for-sops-nix) - [Development](#development) - [License](#license) + ## Installation > [!WARNING] > -> If 1Password is used, 1Password CLI application (`op`) must be on -> `PATH` when running `opsops`. +> If 1Password is used, 1Password CLI application (`op`) must be on `PATH` when +> running `opsops`. Install `opsops` into your Nix profile: @@ -117,7 +117,7 @@ secrets: token: type: "script" value: - content: "printf \"%s\" \"$(gh auth token)\"" + content: 'printf "%s" "$(gh auth token)"' example.com: password: type: "script" @@ -145,6 +145,7 @@ secrets: account: "IPAEPH0JI3REE8FICHOOVU4CHA" uri: "op://Devops/OokahCuZ4fo8ahphie1aiFa0ei/API Tokens/write-only" ``` + ### See Canonical Specification @@ -210,21 +211,22 @@ secrets: type: process value: arguments: - - --hip - - hop + - --hip + - hop command: zamazingo environment: {} strip: both trailingNewline: crlf ``` + ### Render Clear Secrets > [!WARNING] > -> If 1Password is used, 1Password CLI application (`op`) should be -> authenticated first: +> If 1Password is used, 1Password CLI application (`op`) should be authenticated +> first: > > ```sh > eval $(op signin -f [--account ]) @@ -251,12 +253,13 @@ dockerhub: influxdb: token: mu9aephabeadi7zi8goo9peYo8yae7ge ``` + ### Create Snippet for `sops-nix` -To create snippet for `sops-nix` that can be copied/pasted inside the -`sops-nix` module configuration: +To create snippet for `sops-nix` that can be copied/pasted inside the `sops-nix` +module configuration: ```sh opsops snippet sops-nix --input opsops.yaml @@ -272,6 +275,7 @@ opsops snippet sops-nix --input opsops.yaml "influxdb/token" = {}; "zamazingo/secret" = {}; ``` + ## Development diff --git a/weeder.toml b/weeder.toml index 9256657..5b0ac9d 100644 --- a/weeder.toml +++ b/weeder.toml @@ -6,6 +6,6 @@ roots = [ ## Temporary suspensions: "^Zamazingo.*", - "^Opsops.Spec._testJsonRoundtrip$" + "^Opsops.Spec._testJsonRoundtrip$", ] type-class-roots = true From d1aa85b5cb71e6f4726bab0bcf37b4c6ea40a254 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 10 Jan 2026 12:30:01 +0300 Subject: [PATCH 5/5] chore: fix various spelling issues Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/check-static.yaml | 2 +- .github/workflows/release.yml | 2 +- .stan.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-static.yaml b/.github/workflows/check-static.yaml index 3570368..43db0bf 100644 --- a/.github/workflows/check-static.yaml +++ b/.github/workflows/check-static.yaml @@ -11,6 +11,6 @@ jobs: - name: "Checkout Codebase" uses: "actions/checkout@v6" - - name: "Build Static Exectutable" + - name: "Build Static Executable" run: | bash ./build-static.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4f5605..6e40735 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: if: "${{ steps.release.outputs.release_created }}" uses: "DeterminateSystems/nix-installer-action@v21" - - name: "Build Static Exectutable" + - name: "Build Static Executable" id: "build_static" if: "${{ steps.release.outputs.release_created }}" run: | diff --git a/.stan.toml b/.stan.toml index 54246a4..908d86d 100644 --- a/.stan.toml +++ b/.stan.toml @@ -1,6 +1,6 @@ # Big tuples # Using tuples of big size (>= 4) can decrease code readability -# In serveral places Stack uses 4-tuples and in one place Stack uses a +# In several places Stack uses 4-tuples and in one place Stack uses a # 5-tuple. [[check]] id = "STAN-0302"