mirror of
https://github.com/DeterminateSystems/flake-checker.git
synced 2024-08-16 09:30:26 +03:00
Merge pull request #124 from DeterminateSystems/cel-expressions
This commit is contained in:
commit
6ba8ec538e
15
.github/workflows/ci.yaml
vendored
15
.github/workflows/ci.yaml
vendored
@ -36,6 +36,21 @@ jobs:
|
|||||||
- name: cargo test
|
- name: cargo test
|
||||||
run: nix develop -c cargo test
|
run: nix develop -c cargo test
|
||||||
|
|
||||||
|
check-flake-cel-condition:
|
||||||
|
name: Check flake.lock test (CEL condition)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- name: Check flake.lock
|
||||||
|
run: |
|
||||||
|
nix develop -c \
|
||||||
|
cargo run -- \
|
||||||
|
--condition "supportedRefs.contains(gitRef) && numDaysOld < 30 && owner == 'NixOS'" \
|
||||||
|
./tests/flake.cel.0.lock
|
||||||
|
|
||||||
check-flake-dirty:
|
check-flake-dirty:
|
||||||
name: Check flake.lock test (dirty 😈)
|
name: Check flake.lock test (dirty 😈)
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
879
Cargo.lock
generated
879
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@ -1,9 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flake-checker"
|
name = "flake-checker"
|
||||||
version = "0.1.20"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
members = [".", "parse-flake-lock"]
|
members = [".", "parse-flake-lock"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@ -13,11 +14,8 @@ serde_json = { version = "1.0.100", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
thiserror = { version = "1.0.40", default-features = false }
|
thiserror = { version = "1.0.40", default-features = false }
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
allowed-refs = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
cel-interpreter = { version = "0.7.1", default-features = false }
|
||||||
chrono = { version = "0.4.25", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4.25", default-features = false, features = ["clock"] }
|
||||||
clap = { version = "4.3.0", default-features = false, features = [
|
clap = { version = "4.3.0", default-features = false, features = [
|
||||||
"derive",
|
"derive",
|
||||||
@ -27,8 +25,7 @@ clap = { version = "4.3.0", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
handlebars = { version = "4.3.7", default-features = false }
|
handlebars = { version = "4.3.7", default-features = false }
|
||||||
is_ci = "1.1.1"
|
is_ci = "1.1.1"
|
||||||
once_cell = { version = "1.19.0", default-features = false }
|
parse-flake-lock = { path = "./parse-flake-lock" }
|
||||||
parse-flake-lock = { path = "parse-flake-lock" }
|
|
||||||
reqwest = { version = "0.11.18", default-features = false, features = [
|
reqwest = { version = "0.11.18", default-features = false, features = [
|
||||||
"blocking",
|
"blocking",
|
||||||
"json",
|
"json",
|
||||||
@ -38,3 +35,7 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sha2 = { version = "0.10.6", default-features = false }
|
sha2 = { version = "0.10.6", default-features = false }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
allowed-refs = []
|
||||||
|
83
README.md
83
README.md
@ -14,21 +14,83 @@ nix run github:DeterminateSystems/flake-checker
|
|||||||
nix run github:DeterminateSystems/flake-checker /path/to/flake.lock
|
nix run github:DeterminateSystems/flake-checker /path/to/flake.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
Nix Flake Checker looks at your `flake.lock`'s root-level [Nixpkgs] inputs and checks that:
|
Nix Flake Checker looks at your `flake.lock`'s root-level [Nixpkgs] inputs.
|
||||||
|
There are two ways to express flake policies:
|
||||||
|
|
||||||
- Any explicit Nixpkgs Git refs are in this list:
|
* Via [config parameters](#parameters).
|
||||||
- `nixos-23.11`
|
* Via [policy conditions](#policy-conditions) using [Common Expression Language][cel] (CEL).
|
||||||
- `nixos-23.11-small`
|
|
||||||
- `nixos-unstable`
|
|
||||||
- `nixos-unstable-small`
|
|
||||||
- `nixpkgs-23.11-darwin`
|
|
||||||
- `nixpkgs-unstable`
|
|
||||||
- Any Nixpkgs dependencies are less than 30 days old
|
|
||||||
- Any Nixpkgs dependencies have the [`NixOS`][nixos-org] org as the GitHub owner (and thus that the dependency isn't a fork or non-upstream variant)
|
|
||||||
|
|
||||||
If you're running it locally, Nix Flake Checker reports any issues via text output in your terminal.
|
If you're running it locally, Nix Flake Checker reports any issues via text output in your terminal.
|
||||||
But you can also use Nix Flake Checker [in CI](#the-flake-checker-action).
|
But you can also use Nix Flake Checker [in CI](#the-flake-checker-action).
|
||||||
|
|
||||||
|
## Supported branches
|
||||||
|
|
||||||
|
At any given time, [Nixpkgs] has a bounded set of branches that are considered *supported*.
|
||||||
|
The current list:
|
||||||
|
|
||||||
|
* `nixos-23.11`
|
||||||
|
* `nixos-23.11-small`
|
||||||
|
* `nixos-24.05`
|
||||||
|
* `nixos-24.05-small`
|
||||||
|
* `nixos-unstable`
|
||||||
|
* `nixos-unstable-small`
|
||||||
|
* `nixpkgs-23.11-darwin`
|
||||||
|
* `nixpkgs-24.05-darwin`
|
||||||
|
* `nixpkgs-unstable`
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
By default, Flake Checker verifies that:
|
||||||
|
|
||||||
|
- Any explicit Nixpkgs Git refs are in the [supported list](#supported-branches).
|
||||||
|
- Any Nixpkgs dependencies are less than 30 days old.
|
||||||
|
- Any Nixpkgs dependencies have the [`NixOS`][nixos-org] org as the GitHub owner (and thus that the dependency isn't a fork or non-upstream variant).
|
||||||
|
|
||||||
|
You can adjust this behavior via configuration (all are enabled by default but you can disable them):
|
||||||
|
|
||||||
|
Flag | Environment variable | Action | Default
|
||||||
|
:----|:---------------------|:-------|:-------
|
||||||
|
`--check-outdated` | `NIX_FLAKE_CHECKER_CHECK_OUTDATED` | Check for outdated Nixpkgs inputs | `true`
|
||||||
|
`--check-owner` | `NIX_FLAKE_CHECKER_CHECK_OWNER` | Check that Nixpkgs inputs have `NixOS` as the GitHub owner | `true`
|
||||||
|
`--check-supported` | `NIX_FLAKE_CHECKER_CHECK_SUPPORTED` | Check that Git refs for Nixpkgs inputs are supported | `true`
|
||||||
|
|
||||||
|
## Policy conditions
|
||||||
|
|
||||||
|
You can apply a CEL condition to your flake using the `--condition` flag.
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
flake-checker --condition "has(numDaysOld) && numDaysOld < 365"
|
||||||
|
```
|
||||||
|
|
||||||
|
This would check that each Nixpkgs input in your `flake.lock` is less than 365 days old.
|
||||||
|
These variables are available in each condition:
|
||||||
|
|
||||||
|
Variable | Description
|
||||||
|
:--------|:-----------
|
||||||
|
`gitRef` | The Git reference of the input.
|
||||||
|
`numDaysOld` | The number of days old the input is.
|
||||||
|
`owner` | The input's owner (if a GitHub input).
|
||||||
|
`supportedRefs` | A list of [supported Git refs](#supported-branches) (all are branch names).
|
||||||
|
|
||||||
|
We recommend a condition *at least* this stringent:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
supportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 30) && owner == 'NixOS'
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that not all Nixpkgs inputs have a `numDaysOld` field, so make sure to ensure that that field exists when checking for the number of days.
|
||||||
|
|
||||||
|
Here are some other example conditions:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Updated in the last two weeks
|
||||||
|
supportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 14) && owner == 'NixOS'
|
||||||
|
|
||||||
|
# Check for most recent stable Nixpkgs
|
||||||
|
gitRef.contains("24.05")
|
||||||
|
```
|
||||||
|
|
||||||
## The Nix Flake Checker Action
|
## The Nix Flake Checker Action
|
||||||
|
|
||||||
You can automate Nix Flake Checker by adding Determinate Systems' [Nix Flake Checker Action][action] to your GitHub Actions workflows:
|
You can automate Nix Flake Checker by adding Determinate Systems' [Nix Flake Checker Action][action] to your GitHub Actions workflows:
|
||||||
@ -96,6 +158,7 @@ The `parse-flake-lock` crate doesn't yet exhaustively parse all input node types
|
|||||||
If you'd like to help make the parser more exhaustive, [pull requests][prs] are quite welcome.
|
If you'd like to help make the parser more exhaustive, [pull requests][prs] are quite welcome.
|
||||||
|
|
||||||
[action]: https://github.com/DeterminateSystems/flake-checker-action
|
[action]: https://github.com/DeterminateSystems/flake-checker-action
|
||||||
|
[cel]: https://cel.dev
|
||||||
[detsys]: https://determinate.systems
|
[detsys]: https://determinate.systems
|
||||||
[flakes]: https://zero-to-nix.com/concepts/flakes
|
[flakes]: https://zero-to-nix.com/concepts/flakes
|
||||||
[install]: https://zero-to-nix.com/start/install
|
[install]: https://zero-to-nix.com/start/install
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
let
|
let
|
||||||
inherit (stdenv.hostPlatform) system;
|
inherit (stdenv.hostPlatform) system;
|
||||||
|
|
||||||
nightlyVersion = "2023-05-01";
|
nightlyVersion = "2024-06-13";
|
||||||
rustNightly = pkgs.rust-bin.nightly.${nightlyVersion}.default.override {
|
rustNightly = pkgs.rust-bin.nightly.${nightlyVersion}.default.override {
|
||||||
extensions = [ "rust-src" "rust-analyzer-preview" ];
|
extensions = [ "rust-src" "rust-analyzer-preview" ];
|
||||||
targets = cargoTargets;
|
targets = cargoTargets;
|
||||||
@ -80,7 +80,7 @@ let
|
|||||||
# The Rust toolchain from rust-overlay has a dynamic libiconv in depsTargetTargetPropagated
|
# The Rust toolchain from rust-overlay has a dynamic libiconv in depsTargetTargetPropagated
|
||||||
# Our static libiconv needs to take precedence
|
# Our static libiconv needs to take precedence
|
||||||
++ lib.optionals pkgs.stdenv.isDarwin [
|
++ lib.optionals pkgs.stdenv.isDarwin [
|
||||||
(libiconv.override { enableStatic = true; enableShared = false; })
|
libiconv
|
||||||
];
|
];
|
||||||
|
|
||||||
cargoExtraArgs = "--target ${crossPlatform.rustTargetSpec}";
|
cargoExtraArgs = "--target ${crossPlatform.rustTargetSpec}";
|
||||||
|
104
flake.lock
104
flake.lock
@ -2,31 +2,26 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
"crane": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": [
|
|
||||||
"flake-compat"
|
|
||||||
],
|
|
||||||
"flake-utils": [
|
|
||||||
"flake-utils"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
],
|
]
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"narHash": "sha256-ASliYUzlN/aTGDZ2d0FIqxq5fiz+Cwk0q2rYXgy4pB0=",
|
"lastModified": 1717383740,
|
||||||
"rev": "8cb0282cb7c7b5ad7ce1c47d48f647836f8924a0",
|
"narHash": "sha256-559HbY4uhNeoYvK3H6AMZAtVfmR3y8plXZ1x6ON/cWU=",
|
||||||
"revCount": 432,
|
"rev": "b65673fce97d277934488a451724be94cc62499a",
|
||||||
|
"revCount": 580,
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.14.2/018b3503-625a-71c8-96ff-c86e61bd12f7/source.tar.gz"
|
"url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.17.3/018fdc0e-176b-7a0f-92ce-cc2d0db7b735/source.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://flakehub.com/f/ipetkov/crane/0.14.%2A.tar.gz"
|
"url": "https://flakehub.com/f/ipetkov/crane/0.17.%2A"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"locked": {
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
"revCount": 57,
|
"revCount": 57,
|
||||||
@ -35,89 +30,43 @@
|
|||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://flakehub.com/f/edolstra/flake-compat/1.0.1.tar.gz"
|
"url": "https://flakehub.com/f/edolstra/flake-compat/1.0.1"
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1694529238,
|
|
||||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "flake-utils",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1704290814,
|
"lastModified": 1717952948,
|
||||||
"narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=",
|
"narHash": "sha256-mJi4/gjiwQlSaxjA6AusXBN/6rQRaPCycR7bd8fydnQ=",
|
||||||
"rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421",
|
"rev": "2819fffa7fa42156680f0d282c60d81e8fb185b7",
|
||||||
"revCount": 492897,
|
"revCount": 631440,
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2305.492897%2Brev-70bdadeb94ffc8806c0570eb5c2695ad29f0e421/018ce318-b896-7d27-b495-cc2cdb39d680/source.tar.gz"
|
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.631440%2Brev-2819fffa7fa42156680f0d282c60d81e8fb185b7/0190034c-678d-7039-b45c-fa38168f2500/source.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.2305.%2A.tar.gz"
|
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.2405.%2A"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"crane": "crane",
|
"crane": "crane",
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"rust-overlay": "rust-overlay_2"
|
"rust-overlay": "rust-overlay"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": [
|
|
||||||
"crane",
|
|
||||||
"flake-utils"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"crane",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1696299134,
|
|
||||||
"narHash": "sha256-RS77cAa0N+Sfj5EmKbm5IdncNXaBCE1BSSQvUE8exvo=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "611ccdceed92b4d94ae75328148d84ee4a5b462d",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay_2": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": [
|
|
||||||
"flake-utils"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1697422411,
|
"lastModified": 1719800573,
|
||||||
"narHash": "sha256-eCj20wEwATLm7Bd/+/wOIdbqq9jgvS6ZxMrxujX2DxU=",
|
"narHash": "sha256-9DLgG4T6l7cc4pJNOCcXGUwHsFfUp8KLsiwed65MdHk=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "056256f2fcf3c5a652dbc3edba9ec1a956d41f56",
|
"rev": "648b25dd9c3acd255dc50c1eb3ca8b987856f675",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -125,21 +74,6 @@
|
|||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
12
flake.nix
12
flake.nix
@ -1,25 +1,21 @@
|
|||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2305.*.tar.gz";
|
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2405.*";
|
||||||
|
|
||||||
|
|
||||||
rust-overlay = {
|
rust-overlay = {
|
||||||
url = "github:oxalica/rust-overlay";
|
url = "github:oxalica/rust-overlay";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
inputs.flake-utils.follows = "flake-utils";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
crane = {
|
crane = {
|
||||||
url = "https://flakehub.com/f/ipetkov/crane/0.14.*.tar.gz";
|
url = "https://flakehub.com/f/ipetkov/crane/0.17.*";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
inputs.flake-compat.follows = "flake-compat";
|
|
||||||
inputs.flake-utils.follows = "flake-utils";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.0.1.tar.gz";
|
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.0.1";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, crane, ... }:
|
outputs = { self, nixpkgs, rust-overlay, crane, ... }:
|
||||||
let
|
let
|
||||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec {
|
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "parse-flake-lock"
|
name = "parse-flake-lock"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -107,7 +107,10 @@ impl<'de> Deserialize<'de> for FlakeLock {
|
|||||||
let mut root_nodes = HashMap::new();
|
let mut root_nodes = HashMap::new();
|
||||||
let root_node = &nodes[&root];
|
let root_node = &nodes[&root];
|
||||||
let Node::Root(root_node) = root_node else {
|
let Node::Root(root_node) = root_node else {
|
||||||
return Err(de::Error::custom(format!("root node was not a Root node, but was a {} node", root_node.variant())));
|
return Err(de::Error::custom(format!(
|
||||||
|
"root node was not a Root node, but was a {} node",
|
||||||
|
root_node.variant()
|
||||||
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
for (root_name, root_input) in root_node.inputs.iter() {
|
for (root_name, root_input) in root_node.inputs.iter() {
|
||||||
@ -204,7 +207,7 @@ pub enum Node {
|
|||||||
Indirect(IndirectNode),
|
Indirect(IndirectNode),
|
||||||
/// A [PathNode] flake input stemming from a filesystem path.
|
/// A [PathNode] flake input stemming from a filesystem path.
|
||||||
Path(PathNode),
|
Path(PathNode),
|
||||||
/// TODO
|
/// Nodes that point to tarball paths.
|
||||||
Tarball(TarballNode),
|
Tarball(TarballNode),
|
||||||
/// A "catch-all" variant for node types that don't (yet) have explicit struct definitions in
|
/// A "catch-all" variant for node types that don't (yet) have explicit struct definitions in
|
||||||
/// this crate.
|
/// this crate.
|
||||||
@ -372,6 +375,9 @@ pub struct TarballNode {
|
|||||||
/// Information about the tarball input that's "locked" because it's supplied by Nix.
|
/// Information about the tarball input that's "locked" because it's supplied by Nix.
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct TarballLocked {
|
pub struct TarballLocked {
|
||||||
|
/// The timestamp for when the input was last modified.
|
||||||
|
#[serde(alias = "lastModified")]
|
||||||
|
pub last_modified: Option<i64>,
|
||||||
/// The NAR hash of the input.
|
/// The NAR hash of the input.
|
||||||
#[serde(alias = "narHash")]
|
#[serde(alias = "narHash")]
|
||||||
pub nar_hash: String,
|
pub nar_hash: String,
|
||||||
|
82
src/condition.rs
Normal file
82
src/condition.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use cel_interpreter::{Context, Program, Value};
|
||||||
|
use parse_flake_lock::{FlakeLock, Node};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::FlakeCheckerError,
|
||||||
|
flake::{nixpkgs_deps, num_days_old},
|
||||||
|
issue::{Issue, IssueKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY_GIT_REF: &str = "gitRef";
|
||||||
|
const KEY_NUM_DAYS_OLD: &str = "numDaysOld";
|
||||||
|
const KEY_OWNER: &str = "owner";
|
||||||
|
const KEY_SUPPORTED_REFS: &str = "supportedRefs";
|
||||||
|
|
||||||
|
pub(super) fn evaluate_condition(
|
||||||
|
flake_lock: &FlakeLock,
|
||||||
|
nixpkgs_keys: &[String],
|
||||||
|
condition: &str,
|
||||||
|
supported_refs: Vec<String>,
|
||||||
|
) -> Result<Vec<Issue>, FlakeCheckerError> {
|
||||||
|
let mut issues: Vec<Issue> = vec![];
|
||||||
|
let mut ctx = Context::default();
|
||||||
|
ctx.add_variable_from_value(KEY_SUPPORTED_REFS, supported_refs);
|
||||||
|
|
||||||
|
let deps = nixpkgs_deps(flake_lock, nixpkgs_keys)?;
|
||||||
|
|
||||||
|
for (name, node) in deps {
|
||||||
|
let (git_ref, last_modified, owner) = match node {
|
||||||
|
Node::Repo(repo) => (
|
||||||
|
repo.original.git_ref,
|
||||||
|
Some(repo.locked.last_modified),
|
||||||
|
Some(repo.original.owner),
|
||||||
|
),
|
||||||
|
Node::Tarball(tarball) => (None, tarball.locked.last_modified, None),
|
||||||
|
_ => (None, None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
add_cel_variables(&mut ctx, git_ref, last_modified, owner);
|
||||||
|
|
||||||
|
match Program::compile(condition)?.execute(&ctx) {
|
||||||
|
Ok(result) => match result {
|
||||||
|
Value::Bool(b) if !b => {
|
||||||
|
issues.push(Issue {
|
||||||
|
input: name.clone(),
|
||||||
|
kind: IssueKind::Violation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Value::Bool(b) if b => continue,
|
||||||
|
result => {
|
||||||
|
return Err(FlakeCheckerError::NonBooleanCondition(
|
||||||
|
result.type_of().to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => return Err(FlakeCheckerError::CelExecution(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_cel_variables(
|
||||||
|
ctx: &mut Context,
|
||||||
|
git_ref: Option<String>,
|
||||||
|
last_modified: Option<i64>,
|
||||||
|
owner: Option<String>,
|
||||||
|
) {
|
||||||
|
ctx.add_variable_from_value(KEY_GIT_REF, value_or_empty_string(git_ref));
|
||||||
|
ctx.add_variable_from_value(
|
||||||
|
KEY_NUM_DAYS_OLD,
|
||||||
|
value_or_zero(last_modified.map(num_days_old)),
|
||||||
|
);
|
||||||
|
ctx.add_variable_from_value(KEY_OWNER, value_or_empty_string(owner));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_or_empty_string(value: Option<String>) -> Value {
|
||||||
|
Value::from(value.unwrap_or(String::from("")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_or_zero(value: Option<i64>) -> Value {
|
||||||
|
Value::from(value.unwrap_or(0))
|
||||||
|
}
|
@ -1,11 +1,17 @@
|
|||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum FlakeCheckerError {
|
pub enum FlakeCheckerError {
|
||||||
|
#[error("CEL execution error: {0}")]
|
||||||
|
CelExecution(#[from] cel_interpreter::ExecutionError),
|
||||||
|
#[error("CEL parsing error: {0}")]
|
||||||
|
CelParse(#[from] cel_interpreter::ParseError),
|
||||||
#[error("env var error: {0}")]
|
#[error("env var error: {0}")]
|
||||||
EnvVar(#[from] std::env::VarError),
|
EnvVar(#[from] std::env::VarError),
|
||||||
#[error("couldn't parse flake.lock: {0}")]
|
#[error("couldn't parse flake.lock: {0}")]
|
||||||
FlakeLock(#[from] parse_flake_lock::FlakeLockParseError),
|
FlakeLock(#[from] parse_flake_lock::FlakeLockParseError),
|
||||||
#[error("http client error: {0}")]
|
#[error("http client error: {0}")]
|
||||||
Http(#[from] reqwest::Error),
|
Http(#[from] reqwest::Error),
|
||||||
|
#[error("CEL conditions must return a Boolean but returned {0} instead")]
|
||||||
|
NonBooleanCondition(String),
|
||||||
#[error("couldn't access flake.lock: {0}")]
|
#[error("couldn't access flake.lock: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("couldn't parse flake.lock: {0}")]
|
#[error("couldn't parse flake.lock: {0}")]
|
||||||
|
129
src/flake.rs
129
src/flake.rs
@ -30,9 +30,9 @@ impl Default for FlakeCheckConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nixpkgs_deps(
|
pub(super) fn nixpkgs_deps(
|
||||||
flake_lock: &FlakeLock,
|
flake_lock: &FlakeLock,
|
||||||
keys: Vec<String>,
|
keys: &[String],
|
||||||
) -> Result<HashMap<String, Node>, FlakeCheckerError> {
|
) -> Result<HashMap<String, Node>, FlakeCheckerError> {
|
||||||
let mut deps: HashMap<String, Node> = HashMap::new();
|
let mut deps: HashMap<String, Node> = HashMap::new();
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ fn nixpkgs_deps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Node::Indirect(indirect_node) => {
|
Node::Indirect(indirect_node) => {
|
||||||
if &indirect_node.original.id == key {
|
if keys.contains(key) && &indirect_node.original.id == key {
|
||||||
deps.insert(key.to_string(), node);
|
deps.insert(key.to_string(), node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,14 +83,23 @@ pub(crate) fn check_flake_lock(
|
|||||||
) -> Result<Vec<Issue>, FlakeCheckerError> {
|
) -> Result<Vec<Issue>, FlakeCheckerError> {
|
||||||
let mut issues = vec![];
|
let mut issues = vec![];
|
||||||
|
|
||||||
let deps = nixpkgs_deps(flake_lock, config.nixpkgs_keys.clone())?;
|
let deps = nixpkgs_deps(flake_lock, &config.nixpkgs_keys)?;
|
||||||
|
|
||||||
|
for (name, node) in deps {
|
||||||
|
let (git_ref, last_modified, owner) = match node {
|
||||||
|
Node::Repo(repo) => (
|
||||||
|
repo.original.git_ref,
|
||||||
|
Some(repo.locked.last_modified),
|
||||||
|
Some(repo.original.owner),
|
||||||
|
),
|
||||||
|
Node::Tarball(tarball) => (None, tarball.locked.last_modified, None),
|
||||||
|
_ => (None, None, None),
|
||||||
|
};
|
||||||
|
|
||||||
for (name, dep) in deps {
|
|
||||||
if let Node::Repo(repo) = dep {
|
|
||||||
// Check if not explicitly supported
|
// Check if not explicitly supported
|
||||||
if config.check_supported {
|
if let Some(git_ref) = git_ref {
|
||||||
if let Some(ref git_ref) = repo.original.git_ref {
|
// Check if not explicitly supported
|
||||||
if !allowed_refs.contains(git_ref) {
|
if config.check_supported && !allowed_refs.contains(&git_ref) {
|
||||||
issues.push(Issue {
|
issues.push(Issue {
|
||||||
input: name.clone(),
|
input: name.clone(),
|
||||||
kind: IssueKind::Disallowed(Disallowed {
|
kind: IssueKind::Disallowed(Disallowed {
|
||||||
@ -99,13 +108,11 @@ pub(crate) fn check_flake_lock(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if let Some(last_modified) = last_modified {
|
||||||
// Check if outdated
|
// Check if outdated
|
||||||
if config.check_outdated {
|
if config.check_outdated {
|
||||||
let now_timestamp = Utc::now().timestamp();
|
let num_days_old = num_days_old(last_modified);
|
||||||
let diff = now_timestamp - repo.locked.last_modified;
|
|
||||||
let num_days_old = Duration::seconds(diff).num_days();
|
|
||||||
|
|
||||||
if num_days_old > MAX_DAYS {
|
if num_days_old > MAX_DAYS {
|
||||||
issues.push(Issue {
|
issues.push(Issue {
|
||||||
@ -114,11 +121,11 @@ pub(crate) fn check_flake_lock(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(owner) = owner {
|
||||||
// Check that the GitHub owner is NixOS
|
// Check that the GitHub owner is NixOS
|
||||||
if config.check_owner {
|
if config.check_owner && owner.to_lowercase() != "nixos" {
|
||||||
let owner = repo.original.owner;
|
|
||||||
if owner.to_lowercase() != "nixos" {
|
|
||||||
issues.push(Issue {
|
issues.push(Issue {
|
||||||
input: name.clone(),
|
input: name.clone(),
|
||||||
kind: IssueKind::NonUpstream(NonUpstream { owner }),
|
kind: IssueKind::NonUpstream(NonUpstream { owner }),
|
||||||
@ -126,27 +133,77 @@ pub(crate) fn check_flake_lock(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(issues)
|
Ok(issues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn num_days_old(timestamp: i64) -> i64 {
|
||||||
|
let now_timestamp = Utc::now().timestamp();
|
||||||
|
let diff = now_timestamp - timestamp;
|
||||||
|
Duration::seconds(diff).num_days()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
check_flake_lock,
|
check_flake_lock,
|
||||||
|
condition::evaluate_condition,
|
||||||
issue::{Disallowed, Issue, IssueKind, NonUpstream},
|
issue::{Disallowed, Issue, IssueKind, NonUpstream},
|
||||||
FlakeCheckConfig, FlakeLock,
|
FlakeCheckConfig, FlakeLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clean_flake_locks() {
|
fn cel_conditions() {
|
||||||
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json"))
|
// (condition, expected)
|
||||||
.expect("couldn't deserialize allowed-refs.json file");
|
let cases: Vec<(&str, bool)> = vec![
|
||||||
|
(include_str!("../tests/cel-condition.txt"), true),
|
||||||
|
(
|
||||||
|
|
||||||
|
"has(gitRef) && has(numDaysOld) && has(owner) && has(supportedRefs) && supportedRefs.contains(gitRef) && owner != 'NixOS'",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
|
||||||
|
"has(gitRef) && has(numDaysOld) && has(owner) && has(supportedRefs) && supportedRefs.contains(gitRef) && owner != 'NixOS'",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let supported_refs: Vec<String> =
|
||||||
|
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
|
||||||
|
let path = PathBuf::from("tests/flake.cel.0.lock");
|
||||||
|
|
||||||
|
for (condition, expected) in cases {
|
||||||
|
let flake_lock = FlakeLock::new(&path).unwrap();
|
||||||
|
let config = FlakeCheckConfig {
|
||||||
|
nixpkgs_keys: vec![String::from("nixpkgs")],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = evaluate_condition(
|
||||||
|
&flake_lock,
|
||||||
|
&config.nixpkgs_keys,
|
||||||
|
condition,
|
||||||
|
supported_refs.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if expected {
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().is_empty());
|
||||||
|
} else {
|
||||||
|
assert!(!result.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clean_flake_locks() {
|
||||||
|
let allowed_refs: Vec<String> =
|
||||||
|
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
|
||||||
for n in 0..=7 {
|
for n in 0..=7 {
|
||||||
let path = PathBuf::from(format!("tests/flake.clean.{n}.lock"));
|
let path = PathBuf::from(format!("tests/flake.clean.{n}.lock"));
|
||||||
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock");
|
let flake_lock = FlakeLock::new(&path).unwrap();
|
||||||
let config = FlakeCheckConfig {
|
let config = FlakeCheckConfig {
|
||||||
check_outdated: false,
|
check_outdated: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@ -161,9 +218,9 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dirty_flake_locks() {
|
fn dirty_flake_locks() {
|
||||||
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json"))
|
let allowed_refs: Vec<String> =
|
||||||
.expect("couldn't deserialize allowed-refs.json file");
|
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
|
||||||
let cases: Vec<(&str, Vec<Issue>)> = vec![
|
let cases: Vec<(&str, Vec<Issue>)> = vec![
|
||||||
(
|
(
|
||||||
"flake.dirty.0.lock",
|
"flake.dirty.0.lock",
|
||||||
@ -203,22 +260,21 @@ mod test {
|
|||||||
|
|
||||||
for (file, expected_issues) in cases {
|
for (file, expected_issues) in cases {
|
||||||
let path = PathBuf::from(format!("tests/{file}"));
|
let path = PathBuf::from(format!("tests/{file}"));
|
||||||
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock");
|
let flake_lock = FlakeLock::new(&path).unwrap();
|
||||||
let config = FlakeCheckConfig {
|
let config = FlakeCheckConfig {
|
||||||
check_outdated: false,
|
check_outdated: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone())
|
let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()).unwrap();
|
||||||
.expect("couldn't run check_flake_lock function");
|
|
||||||
dbg!(&path);
|
dbg!(&path);
|
||||||
assert_eq!(issues, expected_issues);
|
assert_eq!(issues, expected_issues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_explicit_nixpkgs_keys() {
|
fn explicit_nixpkgs_keys() {
|
||||||
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json"))
|
let allowed_refs: Vec<String> =
|
||||||
.expect("couldn't deserialize allowed-refs.json file");
|
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
|
||||||
let cases: Vec<(&str, Vec<String>, Vec<Issue>)> = vec![(
|
let cases: Vec<(&str, Vec<String>, Vec<Issue>)> = vec![(
|
||||||
"flake.explicit-keys.0.lock",
|
"flake.explicit-keys.0.lock",
|
||||||
vec![String::from("nixpkgs"), String::from("nixpkgs-alt")],
|
vec![String::from("nixpkgs"), String::from("nixpkgs-alt")],
|
||||||
@ -232,22 +288,21 @@ mod test {
|
|||||||
|
|
||||||
for (file, nixpkgs_keys, expected_issues) in cases {
|
for (file, nixpkgs_keys, expected_issues) in cases {
|
||||||
let path = PathBuf::from(format!("tests/{file}"));
|
let path = PathBuf::from(format!("tests/{file}"));
|
||||||
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock");
|
let flake_lock = FlakeLock::new(&path).unwrap();
|
||||||
let config = FlakeCheckConfig {
|
let config = FlakeCheckConfig {
|
||||||
check_outdated: false,
|
check_outdated: false,
|
||||||
nixpkgs_keys,
|
nixpkgs_keys,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone())
|
let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()).unwrap();
|
||||||
.expect("couldn't run check_flake_lock function");
|
|
||||||
assert_eq!(issues, expected_issues);
|
assert_eq!(issues, expected_issues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_missing_nixpkgs_keys() {
|
fn missing_nixpkgs_keys() {
|
||||||
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json"))
|
let allowed_refs: Vec<String> =
|
||||||
.expect("couldn't deserialize allowed-refs.json file");
|
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
|
||||||
let cases: Vec<(&str, Vec<String>, String)> = vec![(
|
let cases: Vec<(&str, Vec<String>, String)> = vec![(
|
||||||
"flake.clean.0.lock",
|
"flake.clean.0.lock",
|
||||||
vec![String::from("nixpkgs"), String::from("foo"), String::from("bar")],
|
vec![String::from("nixpkgs"), String::from("foo"), String::from("bar")],
|
||||||
@ -260,7 +315,7 @@ mod test {
|
|||||||
)];
|
)];
|
||||||
for (file, nixpkgs_keys, expected_err) in cases {
|
for (file, nixpkgs_keys, expected_err) in cases {
|
||||||
let path = PathBuf::from(format!("tests/{file}"));
|
let path = PathBuf::from(format!("tests/{file}"));
|
||||||
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock");
|
let flake_lock = FlakeLock::new(&path).unwrap();
|
||||||
let config = FlakeCheckConfig {
|
let config = FlakeCheckConfig {
|
||||||
check_outdated: false,
|
check_outdated: false,
|
||||||
nixpkgs_keys,
|
nixpkgs_keys,
|
||||||
|
@ -12,6 +12,7 @@ pub(crate) enum IssueKind {
|
|||||||
Disallowed(Disallowed),
|
Disallowed(Disallowed),
|
||||||
Outdated(Outdated),
|
Outdated(Outdated),
|
||||||
NonUpstream(NonUpstream),
|
NonUpstream(NonUpstream),
|
||||||
|
Violation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
@ -41,4 +42,8 @@ impl IssueKind {
|
|||||||
pub(crate) fn is_non_upstream(&self) -> bool {
|
pub(crate) fn is_non_upstream(&self) -> bool {
|
||||||
matches!(self, Self::NonUpstream(_))
|
matches!(self, Self::NonUpstream(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_violation(&self) -> bool {
|
||||||
|
matches!(self, Self::Violation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
28
src/main.rs
28
src/main.rs
@ -1,5 +1,6 @@
|
|||||||
#[cfg(feature = "allowed-refs")]
|
#[cfg(feature = "allowed-refs")]
|
||||||
mod allowed_refs;
|
mod allowed_refs;
|
||||||
|
mod condition;
|
||||||
mod error;
|
mod error;
|
||||||
mod flake;
|
mod flake;
|
||||||
mod issue;
|
mod issue;
|
||||||
@ -16,6 +17,8 @@ use std::process::ExitCode;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use parse_flake_lock::FlakeLock;
|
use parse_flake_lock::FlakeLock;
|
||||||
|
|
||||||
|
use crate::condition::evaluate_condition;
|
||||||
|
|
||||||
/// A flake.lock checker for Nix projects.
|
/// A flake.lock checker for Nix projects.
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
@ -86,6 +89,10 @@ struct Cli {
|
|||||||
)]
|
)]
|
||||||
markdown_summary: bool,
|
markdown_summary: bool,
|
||||||
|
|
||||||
|
/// The Common Expression Language (CEL) policy to apply to each Nixpkgs input.
|
||||||
|
#[arg(long, short, env = "NIX_FLAKE_CHECKER_CONDITION")]
|
||||||
|
condition: Option<String>,
|
||||||
|
|
||||||
#[cfg(feature = "allowed-refs")]
|
#[cfg(feature = "allowed-refs")]
|
||||||
// Check to make sure that Flake Checker is aware of the current supported branches.
|
// Check to make sure that Flake Checker is aware of the current supported branches.
|
||||||
#[arg(long, hide = true)]
|
#[arg(long, hide = true)]
|
||||||
@ -98,8 +105,8 @@ struct Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<ExitCode, FlakeCheckerError> {
|
fn main() -> Result<ExitCode, FlakeCheckerError> {
|
||||||
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json"))
|
let allowed_refs: Vec<String> =
|
||||||
.expect("couldn't deserialize allowed-refs.json file");
|
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
|
||||||
|
|
||||||
let Cli {
|
let Cli {
|
||||||
no_telemetry,
|
no_telemetry,
|
||||||
@ -111,6 +118,7 @@ fn main() -> Result<ExitCode, FlakeCheckerError> {
|
|||||||
fail_mode,
|
fail_mode,
|
||||||
nixpkgs_keys,
|
nixpkgs_keys,
|
||||||
markdown_summary,
|
markdown_summary,
|
||||||
|
condition,
|
||||||
#[cfg(feature = "allowed-refs")]
|
#[cfg(feature = "allowed-refs")]
|
||||||
check_allowed_refs,
|
check_allowed_refs,
|
||||||
#[cfg(feature = "allowed-refs")]
|
#[cfg(feature = "allowed-refs")]
|
||||||
@ -166,17 +174,27 @@ fn main() -> Result<ExitCode, FlakeCheckerError> {
|
|||||||
check_supported,
|
check_supported,
|
||||||
check_outdated,
|
check_outdated,
|
||||||
check_owner,
|
check_owner,
|
||||||
nixpkgs_keys,
|
nixpkgs_keys: nixpkgs_keys.clone(),
|
||||||
fail_mode,
|
fail_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
let issues = check_flake_lock(&flake_lock, &flake_check_config, allowed_refs.clone())?;
|
let issues = if let Some(condition) = &condition {
|
||||||
|
evaluate_condition(&flake_lock, &nixpkgs_keys, condition, allowed_refs.clone())?
|
||||||
|
} else {
|
||||||
|
check_flake_lock(&flake_lock, &flake_check_config, allowed_refs.clone())?
|
||||||
|
};
|
||||||
|
|
||||||
if !no_telemetry {
|
if !no_telemetry {
|
||||||
telemetry::TelemetryReport::make_and_send(&issues);
|
telemetry::TelemetryReport::make_and_send(&issues);
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary = Summary::new(&issues, flake_lock_path, flake_check_config, allowed_refs);
|
let summary = Summary::new(
|
||||||
|
&issues,
|
||||||
|
flake_lock_path,
|
||||||
|
flake_check_config,
|
||||||
|
allowed_refs,
|
||||||
|
condition,
|
||||||
|
);
|
||||||
|
|
||||||
if std::env::var("GITHUB_ACTIONS").is_ok() {
|
if std::env::var("GITHUB_ACTIONS").is_ok() {
|
||||||
if markdown_summary {
|
if markdown_summary {
|
||||||
|
@ -10,14 +10,24 @@ use std::path::PathBuf;
|
|||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
static MARKDOWN_TEMPLATE: &str = include_str!(concat!(
|
static CEL_MARKDOWN_TEMPLATE: &str = include_str!(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/src/templates/summary_md.hbs"
|
"/src/templates/summary.cel.md.hbs"
|
||||||
));
|
));
|
||||||
|
|
||||||
static TEXT_TEMPLATE: &str = include_str!(concat!(
|
static CEL_TEXT_TEMPLATE: &str = include_str!(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/src/templates/summary_txt.hbs"
|
"/src/templates/summary.cel.txt.hbs"
|
||||||
|
));
|
||||||
|
|
||||||
|
static STANDARD_MARKDOWN_TEMPLATE: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/src/templates/summary.standard.md.hbs"
|
||||||
|
));
|
||||||
|
|
||||||
|
static STANDARD_TEXT_TEMPLATE: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/src/templates/summary.standard.txt.hbs"
|
||||||
));
|
));
|
||||||
|
|
||||||
pub(crate) struct Summary {
|
pub(crate) struct Summary {
|
||||||
@ -25,6 +35,7 @@ pub(crate) struct Summary {
|
|||||||
data: serde_json::Value,
|
data: serde_json::Value,
|
||||||
flake_lock_path: PathBuf,
|
flake_lock_path: PathBuf,
|
||||||
flake_check_config: FlakeCheckConfig,
|
flake_check_config: FlakeCheckConfig,
|
||||||
|
condition: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Summary {
|
impl Summary {
|
||||||
@ -33,18 +44,41 @@ impl Summary {
|
|||||||
flake_lock_path: PathBuf,
|
flake_lock_path: PathBuf,
|
||||||
flake_check_config: FlakeCheckConfig,
|
flake_check_config: FlakeCheckConfig,
|
||||||
allowed_refs: Vec<String>,
|
allowed_refs: Vec<String>,
|
||||||
|
condition: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let disallowed: Vec<&Issue> = issues.iter().filter(|i| i.kind.is_disallowed()).collect();
|
let num_issues = issues.len();
|
||||||
|
let clean = issues.is_empty();
|
||||||
|
let issue_word = if issues.len() == 1 { "issue" } else { "issues" };
|
||||||
|
|
||||||
|
let data = if let Some(condition) = &condition {
|
||||||
|
let inputs_with_violations: Vec<String> = issues
|
||||||
|
.iter()
|
||||||
|
.filter(|i| i.kind.is_violation())
|
||||||
|
.map(|i| i.input.to_owned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"issues": issues,
|
||||||
|
"num_issues": num_issues,
|
||||||
|
"clean": clean,
|
||||||
|
"dirty": !clean,
|
||||||
|
"issue_word": issue_word,
|
||||||
|
"condition": condition,
|
||||||
|
"inputs_with_violations": inputs_with_violations,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let disallowed: Vec<&Issue> =
|
||||||
|
issues.iter().filter(|i| i.kind.is_disallowed()).collect();
|
||||||
let outdated: Vec<&Issue> = issues.iter().filter(|i| i.kind.is_outdated()).collect();
|
let outdated: Vec<&Issue> = issues.iter().filter(|i| i.kind.is_outdated()).collect();
|
||||||
let non_upstream: Vec<&Issue> =
|
let non_upstream: Vec<&Issue> =
|
||||||
issues.iter().filter(|i| i.kind.is_non_upstream()).collect();
|
issues.iter().filter(|i| i.kind.is_non_upstream()).collect();
|
||||||
|
|
||||||
let data = json!({
|
json!({
|
||||||
"issues": issues,
|
"issues": issues,
|
||||||
"num_issues": issues.len(),
|
"num_issues": num_issues,
|
||||||
"clean": issues.is_empty(),
|
"clean": clean,
|
||||||
"dirty": !issues.is_empty(),
|
"dirty": !clean,
|
||||||
"issue_word": if issues.len() == 1 { "issue" } else { "issues" },
|
"issue_word": issue_word,
|
||||||
// Disallowed refs
|
// Disallowed refs
|
||||||
"has_disallowed": !disallowed.is_empty(),
|
"has_disallowed": !disallowed.is_empty(),
|
||||||
"disallowed": disallowed,
|
"disallowed": disallowed,
|
||||||
@ -57,13 +91,15 @@ impl Summary {
|
|||||||
// Constants
|
// Constants
|
||||||
"max_days": MAX_DAYS,
|
"max_days": MAX_DAYS,
|
||||||
"supported_ref_names": allowed_refs,
|
"supported_ref_names": allowed_refs,
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
issues: issues.to_vec(),
|
issues: issues.to_vec(),
|
||||||
data,
|
data,
|
||||||
flake_lock_path,
|
flake_lock_path,
|
||||||
flake_check_config,
|
flake_check_config,
|
||||||
|
condition,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +108,18 @@ impl Summary {
|
|||||||
|
|
||||||
if self.issues.is_empty() {
|
if self.issues.is_empty() {
|
||||||
println!("The Determinate Nix Flake Checker scanned {file} and found no issues");
|
println!("The Determinate Nix Flake Checker scanned {file} and found no issues");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(condition) = &self.condition {
|
||||||
|
println!(
|
||||||
|
"You supplied this CEL condition for your flake:\n\n{}",
|
||||||
|
condition
|
||||||
|
);
|
||||||
|
println!("The following inputs violate that condition:\n");
|
||||||
|
for issue in self.issues.iter() {
|
||||||
|
println!("* {}", issue.input);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let level = if self.flake_check_config.fail_mode {
|
let level = if self.flake_check_config.fail_mode {
|
||||||
"error"
|
"error"
|
||||||
@ -113,6 +161,7 @@ impl Summary {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
IssueKind::Violation => Some(String::from("policy violation")),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(message) = message {
|
if let Some(message) = message {
|
||||||
@ -124,10 +173,16 @@ impl Summary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_markdown(&self) -> Result<(), FlakeCheckerError> {
|
pub fn generate_markdown(&self) -> Result<(), FlakeCheckerError> {
|
||||||
|
let template = if self.condition.is_some() {
|
||||||
|
CEL_MARKDOWN_TEMPLATE
|
||||||
|
} else {
|
||||||
|
STANDARD_MARKDOWN_TEMPLATE
|
||||||
|
};
|
||||||
|
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
|
|
||||||
handlebars
|
handlebars
|
||||||
.register_template_string("summary.md", MARKDOWN_TEMPLATE)
|
.register_template_string("summary.md", template)
|
||||||
.map_err(Box::new)?;
|
.map_err(Box::new)?;
|
||||||
let summary_md = handlebars.render("summary.md", &self.data)?;
|
let summary_md = handlebars.render("summary.md", &self.data)?;
|
||||||
|
|
||||||
@ -142,9 +197,15 @@ impl Summary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_text(&self) -> Result<(), FlakeCheckerError> {
|
pub fn generate_text(&self) -> Result<(), FlakeCheckerError> {
|
||||||
|
let template = if self.condition.is_some() {
|
||||||
|
CEL_TEXT_TEMPLATE
|
||||||
|
} else {
|
||||||
|
STANDARD_TEXT_TEMPLATE
|
||||||
|
};
|
||||||
|
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
handlebars
|
handlebars
|
||||||
.register_template_string("summary.txt", TEXT_TEMPLATE)
|
.register_template_string("summary.txt", template)
|
||||||
.map_err(Box::new)?;
|
.map_err(Box::new)?;
|
||||||
|
|
||||||
let summary_txt = handlebars.render("summary.txt", &self.data)?;
|
let summary_txt = handlebars.render("summary.txt", &self.data)?;
|
||||||
|
23
src/templates/summary.cel.md.hbs
Normal file
23
src/templates/summary.cel.md.hbs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# ![](https://avatars.githubusercontent.com/u/80991770?s=30) Flake checkup
|
||||||
|
|
||||||
|
{{#if clean}}
|
||||||
|
The Determinate Flake Checker Action scanned your `flake.lock` and didn't identify any issues.
|
||||||
|
All Nixpkgs inputs conform to the flake policy expressed in your supplied [Common Expression Language](https://cel.dev) condition.
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if dirty}}
|
||||||
|
⚠️ The Determinate Nix Installer Action scanned your `flake.lock` and discovered {{num_issues}} {{issue_word}} that we recommend looking into.
|
||||||
|
You supplied this CEL condition:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
{{condition}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The following inputs violate that condition:
|
||||||
|
|
||||||
|
{{#each inputs_with_violations}}
|
||||||
|
* `{{this}}`
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<p>Feedback? Let us know at <a href="https://github.com/DeterminateSystems/flake-checker">DeterminateSystems/flake-checker</a>.</p>
|
19
src/templates/summary.cel.txt.hbs
Normal file
19
src/templates/summary.cel.txt.hbs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Flake checker results:
|
||||||
|
|
||||||
|
{{#if clean}}
|
||||||
|
The flake checker scanned your flake.lock and didn't identify any issues. You specified this CEL
|
||||||
|
condition:
|
||||||
|
|
||||||
|
{{{condition}}}
|
||||||
|
|
||||||
|
All Nixpkgs inputs satisfy this condition.
|
||||||
|
{{/if}}
|
||||||
|
{{#if dirty}}
|
||||||
|
The flake checker scanned your flake.lock and discovered {{num_issues}} {{issue_word}}
|
||||||
|
that we recommend looking into. Here are the inputs that violate your supplied
|
||||||
|
condition:
|
||||||
|
|
||||||
|
{{#each inputs_with_violations}}
|
||||||
|
* {{this}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
6
tests/cel-condition.txt
Normal file
6
tests/cel-condition.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
supportedRefs == ['nixos-24.05', 'nixos-24.05-small', 'nixos-unstable', 'nixos-unstable-small', 'nixpkgs-24.05-darwin', 'nixpkgs-unstable']
|
||||||
|
&& owner == 'NixOS'
|
||||||
|
&& gitRef == 'nixos-unstable'
|
||||||
|
&& supportedRefs.contains(gitRef)
|
||||||
|
&& has(numDaysOld)
|
||||||
|
&& numDaysOld > 0
|
154
tests/flake.cel.0.lock
Normal file
154
tests/flake.cel.0.lock
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"flake-compat"
|
||||||
|
],
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1684468982,
|
||||||
|
"narHash": "sha256-EoC1N5sFdmjuAP3UOkyQujSOT6EdcXTnRw8hPjJkEgc=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "99de890b6ef4b4aab031582125b6056b792a4a30",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1673956053,
|
||||||
|
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681202837,
|
||||||
|
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "flake-utils",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1686960236,
|
||||||
|
"narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"crane",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"crane",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1683080331,
|
||||||
|
"narHash": "sha256-nGDvJ1DAxZIwdn6ww8IFwzoHb2rqBP4wv/65Wt5vflk=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "d59c3fa0cba8336e115b376c2d9e91053aa59e56",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay_2": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1684808436,
|
||||||
|
"narHash": "sha256-WG5LgB1+Oguj4H4Bpqr5GoLSc382LyGlaToiOw5xhwA=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "a227d4571dd1f948138a40ea8b0d0c413eefb44b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user