[
  {
    "path": ".cargo/config.toml",
    "content": "[target.'cfg(target_os = \"linux\")']\nrustflags = [\n  \"--cfg\", \"tokio_unstable\",\n  \"-Crelocation-model=static\",\n]\n"
  },
  {
    "path": ".editorconfig",
    "content": "# https://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.rs]\nindent_size = 4\n\n[*.hbs]\ninsert_final_newline = false\n"
  },
  {
    "path": ".envrc",
    "content": "use flake\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Build flake-checker artifacts\n\non:\n  workflow_call:\n  workflow_dispatch:\n\njobs:\n  build-artifacts:\n    runs-on: ${{ matrix.systems.runner }}\n    permissions:\n      id-token: write\n      contents: read\n    env:\n      ARTIFACT_KEY: flake-checker-${{ matrix.systems.system }}\n    strategy:\n      matrix:\n        systems:\n          - nix-system: aarch64-darwin\n            runner: macos-15\n            system: ARM64-macOS\n          - nix-system: aarch64-linux\n            runner: ubuntu-24.04-arm\n            system: ARM64-Linux\n          - nix-system: x86_64-linux\n            runner: ubuntu-24.04\n            system: X64-Linux\n    steps:\n      - name: git checkout\n        uses: actions/checkout@v6\n\n      - name: Install Determinate Nix\n        uses: DeterminateSystems/determinate-nix-action@main\n\n      - name: Set up FlakeHub Cache\n        uses: DeterminateSystems/flakehub-cache-action@main\n\n      - name: Build and cache dev shell for ${{ matrix.systems.nix-system }} on ${{ matrix.systems.runner }}\n        run: |\n          nix build -L \".#devShells.${{ matrix.systems.nix-system }}.default\"\n\n      - name: Build package for ${{ matrix.systems.nix-system }}\n        run: |\n          nix build -L \".#packages.${{ matrix.systems.nix-system }}.default\"\n          cp ./result/bin/flake-checker flake-checker\n\n      - name: Ensure that flake-checker binary is static on Linux\n        if: contains(matrix.systems.nix-system, 'linux')\n        run: |\n          if file ./flake-checker | grep -E -q \"static.+linked\"; then\n            echo \"✅👍 STATIC\"\n          else\n            echo \"❌👎 DYNAMIC\"\n            exit 1\n          fi\n\n      - name: Upload flake-checker executable for ${{ matrix.systems.system }}\n        uses: actions/upload-artifact@v4.3.3\n        with:\n          # Artifact name\n          name: ${{ env.ARTIFACT_KEY }}\n          path: flake-checker\n          retention-days: 1\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: Flake checker CI\n\non:\n  pull_request:\n  push:\n    branches: [main]\n\njobs:\n  checks:\n    name: Nix and Rust checks\n    runs-on: ubuntu-24.04\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      - uses: DeterminateSystems/determinate-nix-action@main\n      - uses: DeterminateSystems/flakehub-cache-action@main\n      - name: Check flake.lock\n        uses: DeterminateSystems/flake-checker-action@main\n        with:\n          fail-mode: true\n      - name: Check Nix formatting\n        run: nix develop -c check-nix-fmt\n      - name: Check Rust formatting\n        run: nix develop -c check-rust-fmt\n      - name: Clippy\n        run: nix develop -c cargo clippy\n\n  rust-tests:\n    name: Test Rust\n    runs-on: ubuntu-24.04\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      - uses: DeterminateSystems/determinate-nix-action@main\n      - uses: DeterminateSystems/flakehub-cache-action@main\n      - name: cargo test\n        run: nix develop -c cargo test\n\n  check-flake-cel-condition:\n    name: Check flake.lock test (CEL condition)\n    runs-on: ubuntu-24.04\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      - uses: DeterminateSystems/determinate-nix-action@main\n      - uses: DeterminateSystems/flakehub-cache-action@main\n      - name: Check flake.lock\n        run: |\n          nix develop -c \\\n            cargo run -- \\\n              --condition \"supportedRefs.contains(gitRef) && numDaysOld > 30 && owner == 'NixOS'\" \\\n              ./tests/flake.cel.0.lock\n\n  check-flake-dirty:\n    name: Check flake.lock test (dirty 😈)\n    runs-on: ubuntu-24.04\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      - uses: DeterminateSystems/determinate-nix-action@main\n      - uses: DeterminateSystems/flakehub-cache-action@main\n      - name: Check flake.lock\n        run: |\n          nix develop -c cargo run -- ./tests/flake.dirty.0.lock\n\n  check-flake-clean:\n    name: Check flake.lock test (clean 👼)\n    runs-on: ubuntu-24.04\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      - uses: DeterminateSystems/determinate-nix-action@main\n      - uses: DeterminateSystems/flakehub-cache-action@main\n      - name: Check flake.lock\n        run: |\n          nix develop -c cargo run\n\n  check-flake-dirty-fail-mode:\n    name: Check flake.lock test (dirty 😈 plus fail mode activated)\n    runs-on: ubuntu-24.04\n    if: false\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      - uses: DeterminateSystems/determinate-nix-action@main\n      - uses: DeterminateSystems/flakehub-cache-action@main\n      - name: Check flake.lock\n        run: |\n          nix develop -c cargo run -- --fail-mode ./tests/flake.dirty.0.lock\n\n  build-artifacts:\n    name: Build artifacts\n    needs: checks\n    uses: ./.github/workflows/build.yaml\n    secrets: inherit\n\n  action-integration-test:\n    name: Integration test for flake-checker-action\n    needs: build-artifacts\n    runs-on: ${{ matrix.systems.runner }}\n    permissions:\n      contents: read\n      id-token: write\n    env:\n      ARTIFACT_KEY: flake-checker-${{ matrix.systems.system }}\n    strategy:\n      matrix:\n        systems:\n          - system: X64-Linux\n            runner: ubuntu-24.04\n          - system: ARM64-Linux\n            runner: ubuntu-24.04-arm\n          - system: ARM64-macOS\n            runner: macos-15\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install Determinate Nix\n        uses: DeterminateSystems/determinate-nix-action@main\n\n      - name: Download flake-checker for ${{ matrix.systems.system }}\n        uses: actions/download-artifact@v4.1.7\n        with:\n          name: ${{ env.ARTIFACT_KEY }}\n          path: ${{ env.ARTIFACT_KEY }}\n\n      - name: chmod flake-checker executable on ${{ matrix.systems.system }}\n        run: |\n          chmod +x \"${{ env.ARTIFACT_KEY }}/flake-checker\"\n\n          file \"${{ env.ARTIFACT_KEY }}/flake-checker\"\n\n      - name: Test flake-checker-action@main on ${{ matrix.systems.runner }}\n        uses: DeterminateSystems/flake-checker-action@main\n        with:\n          source-binary: ${{ env.ARTIFACT_KEY }}/flake-checker\n"
  },
  {
    "path": ".github/workflows/flakehub-publish-tagged.yaml",
    "content": "name: \"Publish tags to FlakeHub\"\n\non:\n  push:\n    tags:\n      - \"v?[0-9]+.[0-9]+.[0-9]+*\"\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"The existing tag to publish to FlakeHub\"\n        type: \"string\"\n        required: true\n\njobs:\n  flakehub-publish:\n    runs-on: \"ubuntu-latest\"\n    permissions:\n      id-token: \"write\"\n      contents: \"read\"\n    steps:\n      - uses: \"actions/checkout@v6\"\n        with:\n          ref: \"${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}\"\n      - uses: \"DeterminateSystems/determinate-nix-action@main\"\n      - uses: \"DeterminateSystems/flakehub-push@main\"\n        with:\n          visibility: \"public\"\n          name: \"DeterminateSystems/flake-checker\"\n          tag: \"${{ inputs.tag }}\"\n          include-output-paths: true\n"
  },
  {
    "path": ".github/workflows/ref-statuses.yaml",
    "content": "name: Check that ref statuses are up to date\n\non:\n  schedule:\n    - cron: \"0 0 * * *\" # Daily\n\njobs:\n  check-ref-statuses:\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: write\n      pull-requests: write\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: DeterminateSystems/determinate-nix-action@main\n\n      - uses: DeterminateSystems/flakehub-cache-action@main\n\n      - name: Check ref statuses\n        run: |\n          nix develop --command cargo run --features ref-statuses -- --check-ref-statuses\n\n      - name: Update ref-statuses.json\n        if: failure()\n        run: |\n          ref_statuses_json=$(nix develop --command cargo run --features ref-statuses -- --get-ref-statuses | jq --sort-keys .)\n          echo \"${ref_statuses_json}\" > ref-statuses.json\n\n      - name: Update README in light of new list\n        if: failure()\n        run: |\n          nix develop --command update-readme\n\n      - name: Create pull request\n        if: failure()\n        uses: peter-evans/create-pull-request@v6\n        with:\n          commit-message: Update ref-statuses.json to new valid Git refs list and update README\n          title: Update ref-statuses.json\n          body: |\n            Nixpkgs has changed its list of maintained references. This PR updates `ref-statuses.json` to reflect that change.\n          branch: updated-ref-statuses\n          base: main\n"
  },
  {
    "path": ".github/workflows/release-branches.yaml",
    "content": "name: Release Branch\n\non:\n  push:\n    branches:\n      # NOTE: make sure any branches here are also valid directory names,\n      # otherwise creating the directory and uploading to s3 will fail\n      - \"main\"\n\njobs:\n  build:\n    uses: ./.github/workflows/build.yaml\n\n  release:\n    needs: build\n\n    concurrency: release\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write # In order to request a JWT for AWS auth\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Create the artifacts directory\n        run: rm -rf ./artifacts && mkdir ./artifacts\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-ARM64-macOS\n          path: cache-binary-ARM64-macOS\n      - name: Persist the cache binary\n        run: cp ./cache-binary-ARM64-macOS/flake-checker ./artifacts/ARM64-macOS\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-X64-Linux\n          path: cache-binary-X64-Linux\n      - name: Persist the cache binary\n        run: cp ./cache-binary-X64-Linux/flake-checker ./artifacts/X64-Linux\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-ARM64-Linux\n          path: cache-binary-ARM64-Linux\n      - name: Persist the cache binary\n        run: cp ./cache-binary-ARM64-Linux/flake-checker ./artifacts/ARM64-Linux\n\n      - uses: DeterminateSystems/push-artifact-ids@main\n        with:\n          s3_upload_role: ${{ secrets.AWS_S3_UPLOAD_ROLE }}\n          bucket: ${{ secrets.AWS_S3_UPLOAD_BUCKET }}\n          directory: ./artifacts\n          ids_project_name: flake-checker\n          ids_binary_prefix: flake-checker\n"
  },
  {
    "path": ".github/workflows/release-prs.yaml",
    "content": "name: Release PR\n\non:\n  pull_request:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - labeled\n\njobs:\n  build:\n    # We want to build and upload artifacts only if the `upload to s3` label is applied\n    # Only intra-repo PRs are allowed to have PR artifacts uploaded\n    # We only want to trigger once the upload once in the case the upload label is added, not when any label is added\n    if: |\n      github.event.pull_request.head.repo.full_name == 'DeterminateSystems/flake-checker'\n      && (\n        (github.event.action == 'labeled' && github.event.label.name == 'upload to s3')\n        || (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'upload to s3'))\n      )\n    uses: ./.github/workflows/build.yaml\n\n  release:\n    needs: build\n    concurrency: release\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write # In order to request a JWT for AWS auth\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Create the artifacts directory\n        run: rm -rf ./artifacts && mkdir ./artifacts\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-ARM64-macOS\n          path: cache-binary-ARM64-macOS\n      - name: Persist the cache binary\n        run: cp ./cache-binary-ARM64-macOS/flake-checker ./artifacts/ARM64-macOS\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-X64-Linux\n          path: cache-binary-X64-Linux\n      - name: Persist the cache binary\n        run: cp ./cache-binary-X64-Linux/flake-checker ./artifacts/X64-Linux\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-ARM64-Linux\n          path: cache-binary-ARM64-Linux\n      - name: Persist the cache binary\n        run: cp ./cache-binary-ARM64-Linux/flake-checker ./artifacts/ARM64-Linux\n\n      - uses: DeterminateSystems/push-artifact-ids@main\n        with:\n          s3_upload_role: ${{ secrets.AWS_S3_UPLOAD_ROLE }}\n          bucket: ${{ secrets.AWS_S3_UPLOAD_BUCKET }}\n          directory: ./artifacts\n          ids_project_name: flake-checker\n          ids_binary_prefix: flake-checker\n"
  },
  {
    "path": ".github/workflows/release-tags.yaml",
    "content": "name: Release Tags\n\non:\n  push:\n    tags:\n      - \"v*.*.*\"\n\njobs:\n  build:\n    uses: ./.github/workflows/build.yaml\n\n  release:\n    needs: build\n\n    concurrency: release\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write # In order to upload artifacts to GitHub releases\n      id-token: write # In order to request a JWT for AWS auth\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Create the artifacts directory\n        run: rm -rf ./artifacts && mkdir ./artifacts\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-ARM64-macOS\n          path: cache-binary-ARM64-macOS\n      - name: Persist the cache binary\n        run: cp ./cache-binary-ARM64-macOS/flake-checker ./artifacts/ARM64-macOS\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-X64-Linux\n          path: cache-binary-X64-Linux\n      - name: Persist the cache binary\n        run: cp ./cache-binary-X64-Linux/flake-checker ./artifacts/X64-Linux\n\n      - uses: actions/download-artifact@v4.1.7\n        with:\n          name: flake-checker-ARM64-Linux\n          path: cache-binary-ARM64-Linux\n      - name: Persist the cache binary\n        run: cp ./cache-binary-ARM64-Linux/flake-checker ./artifacts/ARM64-Linux\n\n      - uses: DeterminateSystems/push-artifact-ids@main\n        with:\n          s3_upload_role: ${{ secrets.AWS_S3_UPLOAD_ROLE }}\n          bucket: ${{ secrets.AWS_S3_UPLOAD_BUCKET }}\n          directory: ./artifacts\n          ids_project_name: flake-checker\n          ids_binary_prefix: flake-checker\n\n      - name: Rename binaries for GH release\n        run: |\n          mv ./artifacts/{,flake-checker-}ARM64-macOS\n          mv ./artifacts/{,flake-checker-}X64-Linux\n          mv ./artifacts/{,flake-checker-}ARM64-Linux\n\n      - name: Publish Release to GitHub (Tag)\n        uses: softprops/action-gh-release@v1\n        with:\n          fail_on_unmatched_files: true\n          draft: true\n          files: |\n            artifacts/**\n"
  },
  {
    "path": ".github/workflows/update-flake-lock.yaml",
    "content": "name: update-flake-lock\n\non:\n  workflow_dispatch: # enable manual triggering\n  schedule:\n    - cron: \"0 0 */15 * *\" # every 15th day of the month\n\njobs:\n  lockfile:\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      - uses: DeterminateSystems/determinate-nix-action@main\n      - uses: DeterminateSystems/flakehub-cache-action@main\n      - uses: DeterminateSystems/update-flake-lock@main\n        with:\n          pr-title: \"Update flake.lock\"\n          pr-labels: |\n            dependencies\n            automated\n          inputs: |\n            nixpkgs\n"
  },
  {
    "path": ".gitignore",
    "content": "# Rust artifacts\n/target\n\n# Nix artifacts\nresult\n\n# Generated\nsummary.md\n!src/templates/summary.md\nsrc/policy.json\n\n# Release script artifacts\nreleases\n.direnv\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"flake-checker\"\nversion = \"0.2.11\"\nedition = \"2024\"\n\n[workspace]\nresolver = \"2\"\nmembers = [\".\", \"parse-flake-lock\"]\n\n[workspace.dependencies]\nserde = { version = \"1.0.163\", features = [\"derive\"] }\nserde_json = { version = \"1.0.100\", default-features = false, features = [\n  \"std\",\n] }\nthiserror = { version = \"1.0.40\", default-features = false }\n\n[dependencies]\ncel-interpreter = { version = \"0.7.1\", default-features = false }\nchrono = { version = \"0.4.25\", default-features = false, features = [\"clock\"] }\nclap = { version = \"4.3.0\", default-features = false, features = [\n  \"derive\",\n  \"env\",\n  \"std\",\n  \"wrap_help\",\n] }\ndetsys-ids-client = { version = \"0.6\", features = [\"tracing-instrument\"] }\nhandlebars = { version = \"4.3.7\", default-features = false }\nparse-flake-lock = { path = \"./parse-flake-lock\" }\nreqwest = { version = \"0.13\", default-features = false, features = [\n  \"blocking\",\n  \"json\",\n  \"rustls\",\n] }\nserde = { workspace = true }\nserde_json = { workspace = true }\nthiserror = { workspace = true }\ntokio = { version = \"1\", features = [\"full\", \"tracing\"] }\ntracing = \"0.1.41\"\ntracing-subscriber = { version = \"0.3.19\", features = [\"env-filter\"] }\n\n[features]\ndefault = []\nref-statuses = []\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Nix Flake Checker\n\n[![FlakeHub](https://img.shields.io/endpoint?url=https://flakehub.com/f/DeterminateSystems/flake-checker/badge)](https://flakehub.com/flake/DeterminateSystems/flake-checker)\n\n**Nix Flake Checker** is a tool from [Determinate Systems][detsys] that performs \"health\" checks on the [`flake.lock`][lockfile] files in your [flake][flakes]-powered Nix projects.\nIts goal is to help your Nix projects stay on recent and supported versions of [Nixpkgs].\n\nTo run the checker in the root of a Nix project:\n\n```shell\nnix run github:DeterminateSystems/flake-checker\n\n# Or point to an explicit path for flake.lock\nnix run github:DeterminateSystems/flake-checker /path/to/flake.lock\n```\n\nNix Flake Checker looks at your `flake.lock`'s root-level [Nixpkgs] inputs.\nThere are two ways to express flake policies:\n\n- Via [config parameters](#parameters).\n- Via [policy conditions](#policy-conditions) using [Common Expression Language][cel] (CEL).\n\nIf you're running it locally, Nix Flake Checker reports any issues via text output in your terminal.\nBut you can also use Nix Flake Checker [in CI](#the-flake-checker-action).\n\n## Supported branches\n\nAt any given time, [Nixpkgs] has a bounded set of branches that are considered _supported_.\nThe current list, with their statuses:\n\n- `nixos-25.05`\n- `nixos-25.05-small`\n- `nixos-25.11`\n- `nixos-25.11-small`\n- `nixos-unstable`\n- `nixos-unstable-small`\n- `nixpkgs-25.05-darwin`\n- `nixpkgs-25.11-darwin`\n- `nixpkgs-unstable`\n\n## Parameters\n\nBy default, Flake Checker verifies that:\n\n- Any explicit Nixpkgs Git refs are in the [supported list](#supported-branches).\n- Any Nixpkgs dependencies are less than 30 days old.\n- 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).\n\nYou can adjust this behavior via configuration (all are enabled by default but you can disable them):\n\n| Flag                | Environment variable                | Action                                                     | Default |\n| :------------------ | :---------------------------------- | :--------------------------------------------------------- | :------ |\n| `--check-outdated`  | `NIX_FLAKE_CHECKER_CHECK_OUTDATED`  | Check for outdated Nixpkgs inputs                          | `true`  |\n| `--check-owner`     | `NIX_FLAKE_CHECKER_CHECK_OWNER`     | Check that Nixpkgs inputs have `NixOS` as the GitHub owner | `true`  |\n| `--check-supported` | `NIX_FLAKE_CHECKER_CHECK_SUPPORTED` | Check that Git refs for Nixpkgs inputs are supported       | `true`  |\n\n## Policy conditions\n\nYou can apply a CEL condition to your flake using the `--condition` flag.\nHere's an example:\n\n```shell\nflake-checker --condition \"has(numDaysOld) && numDaysOld < 365\"\n```\n\nThis would check that each Nixpkgs input in your `flake.lock` is less than 365 days old.\nThese variables are available in each condition:\n\n| Variable        | Description                                                                                                                              |\n| :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |\n| `gitRef`        | The Git reference of the input.                                                                                                          |\n| `numDaysOld`    | The number of days old the input is.                                                                                                     |\n| `owner`         | The input's owner (if a GitHub input).                                                                                                   |\n| `supportedRefs` | A list of [supported Git refs](#supported-branches) (all are branch names).                                                              |\n| `refStatuses`   | A map. Each key is a branch name. Each value is a branch status (`\"rolling\"`, `\"beta\"`, `\"stable\"`, `\"deprecated\"` or `\"unmaintained\"`). |\n\nWe recommend a condition _at least_ this stringent:\n\n```ruby\nsupportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 30) && owner == 'NixOS'\n```\n\nNote 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.\n\nHere are some other example conditions:\n\n```ruby\n# Updated in the last two weeks\nsupportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 14) && owner == 'NixOS'\n\n# Check for most recent stable Nixpkgs\ngitRef.contains(\"24.05\")\n```\n\n## The Nix Flake Checker Action\n\nYou can automate Nix Flake Checker by adding Determinate Systems' [Nix Flake Checker Action][action] to your GitHub Actions workflows:\n\n```yaml\nchecks:\n  steps:\n    - uses: actions/checkout@v6\n    - name: Check Nix flake Nixpkgs inputs\n      uses: DeterminateSystems/flake-checker-action@main\n```\n\nWhen run in GitHub Actions, Nix Flake Checker always exits with a status code of 0 by default&mdash;and thus never fails your workflows&mdash;and reports its findings as a [Markdown summary][md].\n\n## Telemetry\n\nThe goal of Nix Flake Checker is to help teams stay on recent and supported versions of Nixpkgs.\nThe flake checker collects a little bit of telemetry information to help us make that true.\n\nTo disable diagnostic reporting, set the diagnostics URL to an empty string by passing `--no-telemetry` or setting `FLAKE_CHECKER_NO_TELEMETRY=true`.\n\nYou can read the full privacy policy for [Determinate Systems][detsys], the creators of this tool and the [Determinate Nix Installer][installer], [here][privacy].\n\n## Rust library\n\nThe Nix Flake Checker is written in [Rust].\nThis repo exposes a [`parse-flake-lock`](./parse-flake-lock) crate that you can use to parse [`flake.lock` files][lockfile] in your own Rust projects.\nTo add that dependency:\n\n```toml\n[dependencies]\nparse-flake-lock = { git = \"https://github.com/DeterminateSystems/flake-checker\", branch = \"main\" }\n```\n\nHere's an example usage:\n\n```rust\nuse std::path::Path;\n\nuse parse_flake_lock::{FlakeLock, FlakeLockParseError};\n\nfn main() -> Result<(), FlakeLockParseError> {\n    let flake_lock = FlakeLock::new(Path::new(\"flake.lock\"))?;\n    println!(\"flake.lock info:\");\n    println!(\"version: {version}\", version=flake_lock.version);\n    println!(\"root node: {root:?}\", root=flake_lock.root);\n    println!(\"all nodes: {nodes:?}\", nodes=flake_lock.nodes);\n\n    Ok(())\n}\n```\n\nThe `parse-flake-lock` crate doesn't yet exhaustively parse all input node types, instead using a \"fallthrough\" mechanism that parses input types that don't yet have explicit struct definitions to a [`serde_json::value::Value`][val].\nIf you'd like to help make the parser more exhaustive, [pull requests][prs] are quite welcome.\n\n[action]: https://github.com/DeterminateSystems/flake-checker-action\n[cel]: https://cel.dev\n[detsys]: https://determinate.systems\n[flakes]: https://zero-to-nix.com/concepts/flakes\n[install]: https://zero-to-nix.com/start/install\n[installer]: https://github.com/DeterminateSystems/nix-installer\n[lockfile]: https://zero-to-nix.com/concepts/flakes#lockfile\n[md]: https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries\n[nixos-org]: https://github.com/NixOS\n[nixpkgs]: https://github.com/NixOS/nixpkgs\n[privacy]: https://determinate.systems/policies/privacy\n[prs]: /pulls\n[rust]: https://rust-lang.org\n[telemetry]: https://github.com/DeterminateSystems/nix-flake-checker/blob/main/src/telemetry.rs#L29-L43\n[val]: https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"https://flakehub.com/f/NixOS/nixpkgs/0.1\";\n\n    fenix = {\n      url = \"https://flakehub.com/f/nix-community/fenix/0.1\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n\n    crane.url = \"https://flakehub.com/f/ipetkov/crane/0\";\n\n    easy-template = {\n      url = \"https://flakehub.com/f/DeterminateSystems/easy-template/0\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n\n  outputs =\n    { self, ... }@inputs:\n    let\n      inherit (inputs.nixpkgs) lib;\n\n      lastModifiedDate = self.lastModifiedDate or self.lastModified or \"19700101\";\n      version = \"${builtins.substring 0 8 lastModifiedDate}-${self.shortRev or \"dirty\"}\";\n\n      meta = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package;\n\n      supportedSystems = [\n        \"x86_64-linux\"\n        \"aarch64-linux\"\n        \"aarch64-darwin\"\n      ];\n\n      forAllSystems =\n        f:\n        lib.genAttrs supportedSystems (\n          system:\n          f {\n            inherit system;\n            pkgs = import inputs.nixpkgs {\n              inherit system;\n              overlays = [ self.overlays.default ];\n            };\n          }\n        );\n\n      staticTarget' =\n        system:\n        {\n          \"aarch64-linux\" = \"aarch64-unknown-linux-musl\";\n          \"x86_64-linux\" = \"x86_64-unknown-linux-musl\";\n        }\n        .${system} or null;\n\n    in\n    {\n      packages = forAllSystems (\n        { pkgs, system }:\n        {\n          default = self.packages.${system}.flake-checker;\n          inherit (pkgs) flake-checker;\n        }\n      );\n\n      devShells = forAllSystems (\n        { pkgs, system }:\n        {\n          default =\n            let\n              staticTarget = staticTarget' system;\n              pkgs' = if staticTarget != null then pkgs.pkgsStatic else pkgs;\n\n              check-nix-fmt = pkgs.writeShellApplication {\n                name = \"check-nix-fmt\";\n                runtimeInputs = with pkgs; [\n                  git\n                  nixfmt\n                ];\n                text = ''\n                  git ls-files '*.nix' | xargs nixfmt --check\n                '';\n              };\n              check-rust-fmt = pkgs.writeShellApplication {\n                name = \"check-rust-fmt\";\n                runtimeInputs = with pkgs; [\n                  rustToolchain\n                ];\n                text = \"cargo fmt --check\";\n              };\n              get-ref-statuses = pkgs.writeShellApplication {\n                name = \"get-ref-statuses\";\n                runtimeInputs = with pkgs; [\n                  rustToolchain\n                ];\n                text = \"cargo run --features ref-statuses -- --get-ref-statuses\";\n              };\n              update-readme = pkgs.writeShellApplication {\n                name = \"update-readme\";\n                runtimeInputs = [\n                  inputs.easy-template.packages.${system}.default\n                  pkgs.jq\n                ];\n                text = ''\n                  tmp=$(mktemp -d)\n                  inputs=\"''${tmp}/template-inputs.json\"\n\n                  jq '{supported: .}' ./ref-statuses.json > \"''${inputs}\"\n                  easy-template ./templates/README.md.handlebars \"''${inputs}\" > README.md\n\n                  rm -rf \"''${tmp}\"\n                '';\n              };\n            in\n            pkgs'.mkShell {\n              packages = with pkgs; [\n                bashInteractive\n\n                # Rust\n                lld\n                rustToolchain\n                cargo-bloat\n                cargo-edit\n                cargo-machete\n                cargo-watch\n\n                # CI checks\n                check-nix-fmt\n                check-rust-fmt\n\n                # Scripts\n                get-ref-statuses\n                update-readme\n\n                self.formatter.${system}\n              ];\n\n              # Required by rust-analyzer\n              env = {\n                RUST_SRC_PATH = \"${pkgs.rustToolchain}/lib/rustlib/src/rust/library\";\n              }\n              // pkgs.env;\n            };\n        }\n      );\n\n      formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt);\n\n      overlays.default =\n        final: prev:\n        let\n          meta = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package;\n\n          inherit (prev.stdenv.hostPlatform) system;\n\n          staticTarget = staticTarget' system;\n          pkgs' = if staticTarget != null then final.pkgsStatic else final;\n\n          rustToolchain =\n            with inputs.fenix.packages.${system};\n            combine (\n              with stable;\n              [\n                clippy\n                rustc\n                cargo\n                rustfmt\n                rust-src\n                rust-analyzer\n              ]\n              ++ lib.optionals (staticTarget != null) [\n                targets.${staticTarget}.stable.rust-std\n              ]\n            );\n\n          craneLib = (inputs.crane.mkLib pkgs').overrideToolchain (_: rustToolchain);\n\n          rustTargetSpec = final.stdenv.hostPlatform.rust.rustcTargetSpec;\n          rustTargetSpecEnv = lib.toUpper (builtins.replaceStrings [ \"-\" ] [ \"_\" ] rustTargetSpec);\n\n          env = lib.optionalAttrs (staticTarget != null) {\n            CARGO_BUILD_TARGET = staticTarget;\n            \"CARGO_TARGET_${rustTargetSpecEnv}_LINKER\" = \"${final.stdenv.cc.targetPrefix}cc\";\n          };\n        in\n        {\n          flake-checker =\n            let\n              sharedAttrs = {\n                inherit (meta) name;\n                inherit version;\n\n                src = builtins.path {\n                  name = \"flake-checker-src\";\n                  path = self;\n                };\n\n                depsBuildBuild = [\n                  pkgs'.buildPackages.stdenv.cc\n                  pkgs'.lld\n                ];\n\n                doIncludeCrossToolchainEnv = false;\n\n                inherit env;\n              };\n            in\n            craneLib.buildPackage (\n              sharedAttrs\n              // {\n                cargoArtifacts = craneLib.buildDepsOnly sharedAttrs;\n\n                disallowedReferences = lib.optionals final.stdenv.hostPlatform.isDarwin [\n                  final.libiconv\n                ];\n\n                postFixup = lib.optionalString final.stdenv.hostPlatform.isDarwin ''\n                  install_name_tool -change \\\n                    \"$(otool -L $out/bin/flake-checker | grep libiconv | awk '{print $1}')\" \\\n                    /usr/lib/libiconv.2.dylib \\\n                    $out/bin/flake-checker\n                '';\n              }\n            );\n\n          inherit env rustToolchain;\n        };\n    };\n}\n"
  },
  {
    "path": "parse-flake-lock/Cargo.toml",
    "content": "[package]\nname = \"parse-flake-lock\"\nversion = \"0.1.1\"\nedition = \"2021\"\n\n[dependencies]\nserde = { workspace = true }\nserde_json = { workspace = true }\nthiserror = { workspace = true }\n"
  },
  {
    "path": "parse-flake-lock/src/lib.rs",
    "content": "#![allow(dead_code)]\n\n//! A library for parsing Nix [`flake.lock`][lock] files\n//! into a structured Rust representation. [Determinate Systems][detsys] currently uses this library\n//! for its [Nix Flake Checker][checker] and [Nix Flake Checker Action][action] but it's designed to\n//! be generally useful.\n//!\n//! [action]: https://github.com/DeterminateSystems/flake-checker-action\n//! [checker]: https://github.com/DeterminateSystems/flake-checker\n//! [detsys]: https://determinate.systems\n//! [lock]: https://zero-to-nix.com/concepts/flakes#lockfile\n\nuse std::collections::{HashMap, VecDeque};\nuse std::fmt;\nuse std::fs::read_to_string;\nuse std::path::{Path, PathBuf};\n\nuse serde::de::{self, MapAccess, Visitor};\nuse serde::{Deserialize, Deserializer};\n\n/// A custom error type for the `parse-flake-lock` crate.\n#[derive(Debug, thiserror::Error)]\npub enum FlakeLockParseError {\n    /// The `flake.lock` can be parsed as JSON but is nonetheless invalid.\n    #[error(\"invalid flake.lock file: {0}\")]\n    Invalid(String),\n    /// The `flake.lock` file couldn't be found.\n    #[error(\"couldn't find the flake.lock file: {0}\")]\n    NotFound(#[from] std::io::Error),\n    /// The specified `flake.lock` file couldn't be parsed as JSON.\n    #[error(\"couldn't parse the flake.lock file as json: {0}\")]\n    Json(#[from] serde_json::Error),\n}\n\n/// A Rust representation of a Nix [`flake.lock`\n/// file](https://zero-to-nix.com/concepts/flakes#lockfile).\n#[derive(Clone, Debug)]\npub struct FlakeLock {\n    /// The `nodes` field of the `flake.lock`, representing all input [Node]s for the flake.\n    pub nodes: HashMap<String, Node>,\n    /// The `root` of the `flake.lock` with all input references resolved into the corresponding\n    /// [Node]s represented by the `nodes` field.\n    pub root: HashMap<String, Node>,\n    /// The version of the `flake.lock` (incremented whenever the `flake.nix` dependencies are\n    /// updated).\n    pub version: usize,\n}\n\n/// A custom [Deserializer] for `flake.lock` files, which are standard JSON but require some special\n/// logic to create a meaningful Rust representation.\nimpl<'de> Deserialize<'de> for FlakeLock {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        #[derive(Deserialize)]\n        #[serde(field_identifier, rename_all = \"lowercase\")]\n        enum Field {\n            Nodes,\n            Root,\n            Version,\n        }\n\n        struct FlakeLockVisitor;\n\n        impl<'de> Visitor<'de> for FlakeLockVisitor {\n            type Value = FlakeLock;\n\n            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n                formatter.write_str(\"struct FlakeLock\")\n            }\n\n            fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>\n            where\n                V: MapAccess<'de>,\n            {\n                let mut nodes = None;\n                let mut root = None;\n                let mut version = None;\n                while let Some(key) = map.next_key()? {\n                    match key {\n                        Field::Nodes => {\n                            if nodes.is_some() {\n                                return Err(de::Error::duplicate_field(\"nodes\"));\n                            }\n                            nodes = Some(map.next_value()?);\n                        }\n                        Field::Root => {\n                            if root.is_some() {\n                                return Err(de::Error::duplicate_field(\"root\"));\n                            }\n                            root = Some(map.next_value()?);\n                        }\n                        Field::Version => {\n                            if version.is_some() {\n                                return Err(de::Error::duplicate_field(\"version\"));\n                            }\n                            version = Some(map.next_value()?);\n                        }\n                    }\n                }\n                let nodes: HashMap<String, Node> =\n                    nodes.ok_or_else(|| de::Error::missing_field(\"nodes\"))?;\n                let root: String = root.ok_or_else(|| de::Error::missing_field(\"root\"))?;\n                let version: usize = version.ok_or_else(|| de::Error::missing_field(\"version\"))?;\n\n                let mut root_nodes = HashMap::new();\n                let root_node = &nodes[&root];\n                let Node::Root(root_node) = root_node else {\n                    return Err(de::Error::custom(format!(\n                        \"root node was not a Root node, but was a {} node\",\n                        root_node.variant()\n                    )));\n                };\n\n                for (root_name, root_input) in root_node.inputs.iter() {\n                    let inputs: VecDeque<String> = match root_input.clone() {\n                        Input::String(s) => [s].into(),\n                        Input::List(keys) => keys.into(),\n                    };\n\n                    let real_node = chase_input_node(&nodes, inputs).map_err(|e| {\n                        de::Error::custom(format!(\"failed to chase input {}: {:?}\", root_name, e))\n                    })?;\n                    root_nodes.insert(root_name.clone(), real_node.clone());\n                }\n\n                Ok(FlakeLock {\n                    nodes,\n                    root: root_nodes,\n                    version,\n                })\n            }\n        }\n\n        deserializer.deserialize_any(FlakeLockVisitor)\n    }\n}\n\nfn chase_input_node(\n    nodes: &HashMap<String, Node>,\n    mut inputs: VecDeque<String>,\n) -> Result<&Node, FlakeLockParseError> {\n    let Some(next_input) = inputs.pop_front() else {\n        unreachable!(\"there should always be at least one input\");\n    };\n\n    let mut node = &nodes[&next_input];\n    for input in inputs {\n        let maybe_node_inputs = match node {\n            Node::Root(_) => None,\n            Node::Repo(node) => node.inputs.to_owned(),\n            Node::Indirect(node) => node.inputs.to_owned(),\n            Node::Path(node) => node.inputs.to_owned(),\n            Node::Tarball(node) => node.inputs.to_owned(),\n            Node::Fallthrough(node) => match node.get(\"inputs\") {\n                Some(node_inputs) => serde_json::from_value(node_inputs.clone())\n                    .map_err(FlakeLockParseError::Json)?,\n                None => None,\n            },\n        };\n\n        let node_inputs = match maybe_node_inputs {\n            Some(node_inputs) => node_inputs,\n            None => {\n                return Err(FlakeLockParseError::Invalid(format!(\n                    \"lock node should have had some inputs but had none:\\n{:?}\",\n                    node\n                )));\n            }\n        };\n\n        let next_inputs = &node_inputs[&input];\n        node = match next_inputs {\n            Input::String(s) => &nodes[s],\n            Input::List(inputs) => chase_input_node(nodes, inputs.to_owned().into())?,\n        };\n    }\n\n    Ok(node)\n}\n\nimpl FlakeLock {\n    /// Instantiate a new [FlakeLock] from the provided [Path].\n    pub fn new(path: &Path) -> Result<Self, FlakeLockParseError> {\n        let flake_lock_file = read_to_string(path)?;\n        let flake_lock: FlakeLock = serde_json::from_str(&flake_lock_file)?;\n        Ok(flake_lock)\n    }\n}\n\n/// A flake input [node]. This enum represents two concrete node types, [RepoNode] and [RootNode],\n/// and uses the `Fallthrough` variant to capture node types that don't have explicitly defined\n/// structs in this library, representing them as raw [Value][serde_json::value::Value]s.\n///\n/// [node]: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#lock-files\n#[derive(Clone, Debug, Deserialize)]\n#[serde(untagged)]\npub enum Node {\n    /// A [RootNode] specifying an [Input] map.\n    Root(RootNode),\n    /// A [RepoNode] flake input for a [Git](https://git-scm.com) repository (or another version\n    /// control system).\n    Repo(Box<RepoNode>),\n    /// An [IndirectNode] flake input stemming from an indirect flake reference like `inputs.nixpkgs.url =\n    /// \"nixpkgs\";`.\n    Indirect(IndirectNode),\n    /// A [PathNode] flake input stemming from a filesystem path.\n    Path(PathNode),\n    /// Nodes that point to tarball paths.\n    Tarball(TarballNode),\n    /// A \"catch-all\" variant for node types that don't (yet) have explicit struct definitions in\n    /// this crate.\n    Fallthrough(serde_json::value::Value), // Covers all other node types\n}\n\n// A string representation of the node variant (for logging).\nimpl Node {\n    fn variant(&self) -> &'static str {\n        match self {\n            Node::Root(_) => \"Root\",\n            Node::Repo(_) => \"Repo\",\n            Node::Indirect(_) => \"Indirect\",\n            Node::Path(_) => \"Path\",\n            Node::Tarball(_) => \"Tarball\",\n            Node::Fallthrough(_) => \"Fallthrough\", // Covers all other node types\n        }\n    }\n}\n\n/// An enum type representing node input references.\n#[derive(Clone, Debug, Deserialize)]\n#[serde(untagged)]\npub enum Input {\n    /// An input expressed as a string.\n    String(String),\n    /// An input expressed as a list of strings.\n    List(Vec<String>),\n}\n\n/// A flake [Node] representing a raw mapping of strings to [Input]s.\n#[derive(Clone, Debug, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct RootNode {\n    /// A mapping of the flake's input [Node]s.\n    pub inputs: HashMap<String, Input>,\n}\n\n/// A [Node] representing a [Git](https://git-scm.com) repository (or another version control\n/// system).\n#[derive(Clone, Debug, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct RepoNode {\n    /// Whether the input is itself a flake.\n    pub flake: Option<bool>,\n    /// The node's inputs.\n    pub inputs: Option<HashMap<String, Input>>,\n    /// The \"locked\" attributes of the input (set by Nix).\n    pub locked: RepoLocked,\n    /// The \"original\" (user-supplied) attributes of the repository input.\n    pub original: RepoOriginal,\n}\n\n/// Information about the repository input that's \"locked\" because it's supplied by Nix.\n#[derive(Clone, Debug, Deserialize)]\npub struct RepoLocked {\n    /// The timestamp for when the input was last modified.\n    #[serde(alias = \"lastModified\")]\n    pub last_modified: i64,\n    /// The NAR hash of the input.\n    #[serde(alias = \"narHash\")]\n    pub nar_hash: Option<String>,\n    /// The repository owner.\n    pub owner: String,\n    /// The repository.\n    pub repo: String,\n    /// The Git revision.\n    pub rev: String,\n    /// The type of the node (either `\"repo\"` or `\"indirect\"`).\n    #[serde(alias = \"type\")]\n    pub node_type: String,\n}\n\n/// The `original` field of a [Repo][Node::Repo] node.\n#[derive(Clone, Debug, Deserialize)]\npub struct RepoOriginal {\n    /// The repository owner.\n    pub owner: String,\n    /// The repository.\n    pub repo: String,\n    /// The Git reference of the input.\n    #[serde(alias = \"ref\")]\n    pub git_ref: Option<String>,\n    /// The type of the node (always `\"repo\"`).\n    #[serde(alias = \"type\")]\n    pub node_type: String,\n}\n\n/// An indirect flake input (using the [flake\n/// registry](https://nixos.org/manual/nix/stable/command-ref/conf-file.html#conf-flake-registry)).\n#[derive(Clone, Debug, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct IndirectNode {\n    /// The \"locked\" attributes of the input (set by Nix).\n    pub locked: RepoLocked,\n    /// The node's inputs.\n    pub inputs: Option<HashMap<String, Input>>,\n    /// The \"original\" (user-supplied) attributes of the indirect flake registry input.\n    pub original: IndirectOriginal,\n}\n\n/// The `original` field of an [Indirect][Node::Indirect] node.\n#[derive(Clone, Debug, Deserialize)]\npub struct IndirectOriginal {\n    /// The ID of the input (recognized by the [flake\n    /// registry]((https://nixos.org/manual/nix/stable/command-ref/conf-file.html#conf-flake-registry))).\n    pub id: String,\n    /// The type of the node (always `\"indirect\"`).\n    #[serde(alias = \"type\")]\n    pub node_type: String,\n}\n\n/// A flake input as a filesystem path, e.g. `inputs.local.url = \"path:./subdir\";`.\n#[derive(Clone, Debug, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct PathNode {\n    /// The \"locked\" attributes of the input (set by Nix).\n    pub locked: PathLocked,\n    /// The node's inputs.\n    pub inputs: Option<HashMap<String, Input>>,\n    /// The \"original\" (user-supplied) attributes of the path input.\n    pub original: PathOriginal,\n}\n\n/// Information about the path input that's \"locked\" because it's supplied by Nix.\n#[derive(Clone, Debug, Deserialize)]\npub struct PathLocked {\n    /// The timestamp for when the input was last modified.\n    #[serde(alias = \"lastModified\")]\n    pub last_modified: i64,\n    /// The NAR hash of the input.\n    #[serde(alias = \"narHash\")]\n    pub nar_hash: Option<String>,\n    /// The relative filesystem path for the input.\n    pub path: PathBuf,\n    /// The type of the node (always `\"path\"`).\n    #[serde(alias = \"type\")]\n    pub node_type: String,\n}\n\n/// The user-supplied path input info.\n#[derive(Clone, Debug, Deserialize)]\npub struct PathOriginal {\n    /// The relative filesystem path for the input.\n    pub path: PathBuf,\n    /// The Git reference of the input.\n    #[serde(alias = \"ref\")]\n    pub git_ref: Option<String>,\n    /// The type of the node (always `\"path\"`).\n    #[serde(alias = \"type\")]\n    pub node_type: String,\n}\n\n/// A flake input as a tarball URL.\n#[derive(Clone, Debug, Deserialize)]\npub struct TarballNode {\n    /// The \"locked\" attributes of the input (set by Nix).\n    pub locked: TarballLocked,\n    /// The node's inputs.\n    pub inputs: Option<HashMap<String, Input>>,\n    /// The \"original\" (user-supplied) attributes of the tarball input.\n    pub original: TarballOriginal,\n}\n\n/// Information about the tarball input that's \"locked\" because it's supplied by Nix.\n#[derive(Clone, Debug, Deserialize)]\npub struct TarballLocked {\n    /// The timestamp for when the input was last modified.\n    #[serde(alias = \"lastModified\")]\n    pub last_modified: Option<i64>,\n    /// The NAR hash of the input.\n    #[serde(alias = \"narHash\")]\n    pub nar_hash: Option<String>,\n    /// The type of the node (always `\"tarball\"`).\n    #[serde(alias = \"type\")]\n    pub node_type: String,\n    /// The URL used to fetch the tarball.\n    pub url: String,\n}\n\n/// The user-supplied tarball input info.\n#[derive(Clone, Debug, Deserialize)]\npub struct TarballOriginal {\n    /// The URL for the tarball input.\n    pub url: String,\n    /// The type of the node (always `\"tarball\"`).\n    #[serde(alias = \"type\")]\n    pub node_type: String,\n}\n"
  },
  {
    "path": "ref-statuses.json",
    "content": "{\n  \"nixos-25.05\": \"unmaintained\",\n  \"nixos-25.05-small\": \"unmaintained\",\n  \"nixos-25.11\": \"stable\",\n  \"nixos-25.11-small\": \"stable\",\n  \"nixos-unstable\": \"rolling\",\n  \"nixos-unstable-small\": \"rolling\",\n  \"nixpkgs-25.05-darwin\": \"unmaintained\",\n  \"nixpkgs-25.11-darwin\": \"stable\",\n  \"nixpkgs-unstable\": \"rolling\"\n}\n"
  },
  {
    "path": "src/condition.rs",
    "content": "use cel_interpreter::{Context, Program, Value};\nuse parse_flake_lock::{FlakeLock, Node};\n\nuse std::collections::{BTreeMap, HashMap};\n\nuse crate::{\n    error::FlakeCheckerError,\n    flake::{nixpkgs_deps, num_days_old},\n    issue::{Issue, IssueKind},\n};\n\nconst KEY_GIT_REF: &str = \"gitRef\";\nconst KEY_NUM_DAYS_OLD: &str = \"numDaysOld\";\nconst KEY_OWNER: &str = \"owner\";\nconst KEY_REF_STATUSES: &str = \"refStatuses\";\nconst KEY_SUPPORTED_REFS: &str = \"supportedRefs\";\n\npub(super) fn evaluate_condition(\n    flake_lock: &FlakeLock,\n    nixpkgs_keys: &[String],\n    condition: &str,\n    ref_statuses: BTreeMap<String, String>,\n    supported_refs: Vec<String>,\n) -> Result<Vec<Issue>, FlakeCheckerError> {\n    let mut issues: Vec<Issue> = vec![];\n    let mut ctx = Context::default();\n\n    let ref_statuses = ref_statuses\n        .into_iter()\n        .collect::<HashMap<String, String>>();\n    ctx.add_variable_from_value(KEY_REF_STATUSES, ref_statuses);\n    ctx.add_variable_from_value(KEY_SUPPORTED_REFS, supported_refs);\n\n    let deps = nixpkgs_deps(flake_lock, nixpkgs_keys)?;\n\n    for (name, node) in deps {\n        let (git_ref, last_modified, owner) = match node {\n            Node::Repo(repo) => (\n                repo.original.git_ref,\n                Some(repo.locked.last_modified),\n                Some(repo.original.owner),\n            ),\n            Node::Tarball(tarball) => (None, tarball.locked.last_modified, None),\n            _ => (None, None, None),\n        };\n\n        add_cel_variables(&mut ctx, git_ref, last_modified, owner);\n\n        match Program::compile(condition)?.execute(&ctx) {\n            Ok(result) => match result {\n                Value::Bool(b) if !b => {\n                    issues.push(Issue {\n                        input: name.clone(),\n                        kind: IssueKind::Violation,\n                    });\n                }\n                Value::Bool(b) if b => continue,\n                result => {\n                    return Err(FlakeCheckerError::NonBooleanCondition(\n                        result.type_of().to_string(),\n                    ));\n                }\n            },\n            Err(e) => return Err(FlakeCheckerError::CelExecution(e)),\n        }\n    }\n\n    Ok(issues)\n}\n\nfn add_cel_variables(\n    ctx: &mut Context,\n    git_ref: Option<String>,\n    last_modified: Option<i64>,\n    owner: Option<String>,\n) {\n    ctx.add_variable_from_value(KEY_GIT_REF, value_or_empty_string(git_ref));\n    ctx.add_variable_from_value(\n        KEY_NUM_DAYS_OLD,\n        value_or_zero(last_modified.map(num_days_old)),\n    );\n    ctx.add_variable_from_value(KEY_OWNER, value_or_empty_string(owner));\n}\n\nfn value_or_empty_string(value: Option<String>) -> Value {\n    Value::from(value.unwrap_or(String::from(\"\")))\n}\n\nfn value_or_zero(value: Option<i64>) -> Value {\n    Value::from(value.unwrap_or(0))\n}\n"
  },
  {
    "path": "src/error.rs",
    "content": "#[derive(Debug, thiserror::Error)]\npub enum FlakeCheckerError {\n    #[error(\"CEL execution error: {0}\")]\n    CelExecution(#[from] cel_interpreter::ExecutionError),\n    #[error(\"CEL parsing error: {0}\")]\n    CelParse(#[from] cel_interpreter::ParseError),\n    #[error(\"env var error: {0}\")]\n    EnvVar(#[from] std::env::VarError),\n    #[error(\"couldn't parse flake.lock: {0}\")]\n    FlakeLock(#[from] parse_flake_lock::FlakeLockParseError),\n    #[error(\"http client error: {0}\")]\n    Http(#[from] reqwest::Error),\n    #[error(\"CEL conditions must return a Boolean but returned {0} instead\")]\n    NonBooleanCondition(String),\n    #[error(\"couldn't access flake.lock: {0}\")]\n    Io(#[from] std::io::Error),\n    #[error(\"couldn't parse flake.lock: {0}\")]\n    Json(#[from] serde_json::Error),\n    #[error(\"handlebars render error: {0}\")]\n    Render(#[from] handlebars::RenderError),\n    #[error(\"handlebars template error: {0}\")]\n    Template(#[from] Box<handlebars::TemplateError>),\n    #[error(\"invalid flake.lock: {0}\")]\n    Invalid(String),\n}\n"
  },
  {
    "path": "src/flake.rs",
    "content": "#![allow(dead_code)]\n\nuse std::collections::BTreeMap;\n\nuse crate::FlakeCheckerError;\nuse crate::issue::{Disallowed, Issue, IssueKind, NonUpstream, Outdated};\n\nuse chrono::{Duration, Utc};\nuse parse_flake_lock::{FlakeLock, Node};\n\npub const MAX_DAYS: i64 = 30;\n\npub(crate) struct FlakeCheckConfig {\n    pub check_supported: bool,\n    pub check_outdated: bool,\n    pub check_owner: bool,\n    pub fail_mode: bool,\n    pub nixpkgs_keys: Vec<String>,\n}\n\nimpl Default for FlakeCheckConfig {\n    fn default() -> Self {\n        Self {\n            check_supported: true,\n            check_outdated: true,\n            check_owner: true,\n            fail_mode: false,\n            nixpkgs_keys: vec![String::from(\"nixpkgs\")],\n        }\n    }\n}\n\npub(super) fn nixpkgs_deps(\n    flake_lock: &FlakeLock,\n    keys: &[String],\n) -> Result<BTreeMap<String, Node>, FlakeCheckerError> {\n    let mut deps: BTreeMap<String, Node> = BTreeMap::new();\n\n    for (ref key, node) in flake_lock.root.clone() {\n        match &node {\n            Node::Repo(_) => {\n                if keys.contains(key) {\n                    deps.insert(key.to_string(), node);\n                }\n            }\n            Node::Tarball(_) => {\n                if keys.contains(key) {\n                    deps.insert(key.to_string(), node);\n                }\n            }\n            Node::Indirect(indirect_node) => {\n                if keys.contains(key) && &indirect_node.original.id == key {\n                    deps.insert(key.to_string(), node);\n                }\n            }\n            _ => {\n                // NOTE: it's unclear that a path node for Nixpkgs should be accepted\n            }\n        }\n    }\n    let missing: Vec<String> = keys\n        .iter()\n        .filter(|k| !deps.contains_key(*k))\n        .map(String::from)\n        .collect();\n\n    if !missing.is_empty() {\n        let error_msg = format!(\n            \"no nixpkgs dependency found for specified {}: {}\",\n            if missing.len() > 1 { \"keys\" } else { \"key\" },\n            missing.join(\", \")\n        );\n        return Err(FlakeCheckerError::Invalid(error_msg));\n    }\n\n    Ok(deps)\n}\n\npub(crate) fn check_flake_lock(\n    flake_lock: &FlakeLock,\n    config: &FlakeCheckConfig,\n    allowed_refs: Vec<String>,\n) -> Result<Vec<Issue>, FlakeCheckerError> {\n    let mut issues = vec![];\n\n    let deps = nixpkgs_deps(flake_lock, &config.nixpkgs_keys)?;\n\n    for (name, node) in deps {\n        let (git_ref, last_modified, owner) = match node {\n            Node::Repo(repo) => (\n                repo.original.git_ref,\n                Some(repo.locked.last_modified),\n                Some(repo.original.owner),\n            ),\n            Node::Tarball(tarball) => (None, tarball.locked.last_modified, None),\n            _ => (None, None, None),\n        };\n\n        // Check if not explicitly supported\n        if let Some(git_ref) = git_ref {\n            // Check if not explicitly supported\n            if config.check_supported && !allowed_refs.contains(&git_ref) {\n                issues.push(Issue {\n                    input: name.clone(),\n                    kind: IssueKind::Disallowed(Disallowed {\n                        reference: git_ref.to_string(),\n                    }),\n                });\n            }\n        }\n\n        if let Some(last_modified) = last_modified {\n            // Check if outdated\n            if config.check_outdated {\n                let num_days_old = num_days_old(last_modified);\n\n                if num_days_old > MAX_DAYS {\n                    issues.push(Issue {\n                        input: name.clone(),\n                        kind: IssueKind::Outdated(Outdated { num_days_old }),\n                    });\n                }\n            }\n        }\n\n        if let Some(owner) = owner {\n            // Check that the GitHub owner is NixOS\n            if config.check_owner && owner.to_lowercase() != \"nixos\" {\n                issues.push(Issue {\n                    input: name.clone(),\n                    kind: IssueKind::NonUpstream(NonUpstream { owner }),\n                });\n            }\n        }\n    }\n    Ok(issues)\n}\n\npub(super) fn num_days_old(timestamp: i64) -> i64 {\n    let now_timestamp = Utc::now().timestamp();\n    let diff = now_timestamp - timestamp;\n    Duration::seconds(diff).num_days()\n}\n\n#[cfg(test)]\nmod test {\n    use std::collections::BTreeMap;\n    use std::path::PathBuf;\n\n    use crate::{\n        FlakeCheckConfig, FlakeLock, check_flake_lock,\n        condition::evaluate_condition,\n        issue::{Disallowed, Issue, IssueKind, NonUpstream},\n        supported_refs,\n    };\n\n    #[test]\n    fn cel_conditions() {\n        // (condition, expected)\n        let cases: Vec<(&str, bool)> = vec![\n            (include_str!(\"../tests/cel-condition.cel\"), true),\n            (\n                \"has(gitRef) && has(numDaysOld) && has(owner) && has(supportedRefs) && supportedRefs.contains(gitRef) && owner != 'NixOS'\",\n                false,\n            ),\n            (\n                \"has(gitRef) && has(numDaysOld) && has(owner) && has(supportedRefs) && supportedRefs.contains(gitRef) && owner != 'NixOS'\",\n                false,\n            ),\n        ];\n\n        let ref_statuses: BTreeMap<String, String> =\n            serde_json::from_str(include_str!(\"../ref-statuses.json\")).unwrap();\n        let supported_refs = supported_refs(ref_statuses.clone());\n        let path = PathBuf::from(\"tests/flake.cel.0.lock\");\n\n        for (condition, expected) in cases {\n            let flake_lock = FlakeLock::new(&path).unwrap();\n            let config = FlakeCheckConfig {\n                nixpkgs_keys: vec![String::from(\"nixpkgs\")],\n                ..Default::default()\n            };\n\n            let result = evaluate_condition(\n                &flake_lock,\n                &config.nixpkgs_keys,\n                condition,\n                ref_statuses.clone(),\n                supported_refs.clone(),\n            );\n\n            if expected {\n                println!(\"{result:?}\");\n\n                assert!(result.is_ok());\n                assert!(result.unwrap().is_empty());\n            } else {\n                assert!(!result.unwrap().is_empty());\n            }\n        }\n    }\n\n    #[test]\n    fn clean_flake_locks() {\n        let ref_statuses: BTreeMap<String, String> =\n            serde_json::from_str(include_str!(\"../ref-statuses.json\")).unwrap();\n        let allowed_refs = supported_refs(ref_statuses);\n        for n in 0..=7 {\n            let path = PathBuf::from(format!(\"tests/flake.clean.{n}.lock\"));\n            let flake_lock = FlakeLock::new(&path).unwrap();\n            let config = FlakeCheckConfig {\n                check_outdated: false,\n                ..Default::default()\n            };\n            let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone())\n                .unwrap_or_else(|_| panic!(\"couldn't run check_flake_lock function in {path:?}\"));\n            assert!(\n                issues.is_empty(),\n                \"expected clean flake.lock in tests/flake.clean.{n}.lock but encountered an issue\"\n            );\n        }\n    }\n\n    #[test]\n    fn dirty_flake_locks() {\n        let ref_statuses: BTreeMap<String, String> =\n            serde_json::from_str(include_str!(\"../ref-statuses.json\")).unwrap();\n        let allowed_refs = supported_refs(ref_statuses);\n        let cases: Vec<(&str, Vec<Issue>)> = vec![\n            (\n                \"flake.dirty.0.lock\",\n                vec![\n                    Issue {\n                        input: String::from(\"nixpkgs\"),\n                        kind: IssueKind::Disallowed(Disallowed {\n                            reference: String::from(\"this-should-fail\"),\n                        }),\n                    },\n                    Issue {\n                        input: String::from(\"nixpkgs\"),\n                        kind: IssueKind::NonUpstream(NonUpstream {\n                            owner: String::from(\"bitcoin-miner-org\"),\n                        }),\n                    },\n                ],\n            ),\n            (\n                \"flake.dirty.1.lock\",\n                vec![\n                    Issue {\n                        input: String::from(\"nixpkgs\"),\n                        kind: IssueKind::Disallowed(Disallowed {\n                            reference: String::from(\"probably-nefarious\"),\n                        }),\n                    },\n                    Issue {\n                        input: String::from(\"nixpkgs\"),\n                        kind: IssueKind::NonUpstream(NonUpstream {\n                            owner: String::from(\"pretty-shady\"),\n                        }),\n                    },\n                ],\n            ),\n        ];\n\n        for (file, expected_issues) in cases {\n            let path = PathBuf::from(format!(\"tests/{file}\"));\n            let flake_lock = FlakeLock::new(&path).unwrap();\n            let config = FlakeCheckConfig {\n                check_outdated: false,\n                ..Default::default()\n            };\n            let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()).unwrap();\n            dbg!(&path);\n            assert_eq!(issues, expected_issues);\n        }\n    }\n\n    #[test]\n    fn explicit_nixpkgs_keys() {\n        let ref_statuses: BTreeMap<String, String> =\n            serde_json::from_str(include_str!(\"../ref-statuses.json\")).unwrap();\n        let allowed_refs = supported_refs(ref_statuses);\n        let cases: Vec<(&str, Vec<String>, Vec<Issue>)> = vec![(\n            \"flake.explicit-keys.0.lock\",\n            vec![String::from(\"nixpkgs\"), String::from(\"nixpkgs-alt\")],\n            vec![Issue {\n                input: String::from(\"nixpkgs-alt\"),\n                kind: IssueKind::NonUpstream(NonUpstream {\n                    owner: String::from(\"seems-pretty-shady\"),\n                }),\n            }],\n        )];\n\n        for (file, nixpkgs_keys, expected_issues) in cases {\n            let path = PathBuf::from(format!(\"tests/{file}\"));\n            let flake_lock = FlakeLock::new(&path).unwrap();\n            let config = FlakeCheckConfig {\n                check_outdated: false,\n                nixpkgs_keys,\n                ..Default::default()\n            };\n            let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()).unwrap();\n            assert_eq!(issues, expected_issues);\n        }\n    }\n\n    #[test]\n    fn missing_nixpkgs_keys() {\n        let ref_statuses: BTreeMap<String, String> =\n            serde_json::from_str(include_str!(\"../ref-statuses.json\")).unwrap();\n        let allowed_refs = supported_refs(ref_statuses);\n        let cases: Vec<(&str, Vec<String>, String)> = vec![\n            (\n                \"flake.clean.0.lock\",\n                vec![\n                    String::from(\"nixpkgs\"),\n                    String::from(\"foo\"),\n                    String::from(\"bar\"),\n                ],\n                String::from(\n                    \"invalid flake.lock: no nixpkgs dependency found for specified keys: foo, bar\",\n                ),\n            ),\n            (\n                \"flake.clean.1.lock\",\n                vec![String::from(\"nixpkgs\"), String::from(\"nixpkgs-other\")],\n                String::from(\n                    \"invalid flake.lock: no nixpkgs dependency found for specified key: nixpkgs-other\",\n                ),\n            ),\n        ];\n        for (file, nixpkgs_keys, expected_err) in cases {\n            let path = PathBuf::from(format!(\"tests/{file}\"));\n            let flake_lock = FlakeLock::new(&path).unwrap();\n            let config = FlakeCheckConfig {\n                check_outdated: false,\n                nixpkgs_keys,\n                ..Default::default()\n            };\n\n            let result = check_flake_lock(&flake_lock, &config, allowed_refs.clone());\n\n            assert!(result.is_err());\n            assert_eq!(result.unwrap_err().to_string(), expected_err);\n        }\n    }\n}\n"
  },
  {
    "path": "src/issue.rs",
    "content": "use serde::Serialize;\n\n#[derive(Clone, Debug, PartialEq, Serialize)]\npub(crate) struct Issue {\n    pub input: String,\n    pub kind: IssueKind,\n}\n\n#[derive(Clone, Debug, PartialEq, Serialize)]\n#[serde(untagged)]\npub(crate) enum IssueKind {\n    Disallowed(Disallowed),\n    Outdated(Outdated),\n    NonUpstream(NonUpstream),\n    Violation,\n}\n\n#[derive(Clone, Debug, PartialEq, Serialize)]\npub(crate) struct Disallowed {\n    pub(crate) reference: String,\n}\n\n#[derive(Clone, Debug, PartialEq, Serialize)]\npub(crate) struct Outdated {\n    pub(crate) num_days_old: i64,\n}\n\n#[derive(Clone, Debug, PartialEq, Serialize)]\npub(crate) struct NonUpstream {\n    pub(crate) owner: String,\n}\n\nimpl IssueKind {\n    pub(crate) fn is_disallowed(&self) -> bool {\n        matches!(self, Self::Disallowed(_))\n    }\n\n    pub(crate) fn is_outdated(&self) -> bool {\n        matches!(self, Self::Outdated(_))\n    }\n\n    pub(crate) fn is_non_upstream(&self) -> bool {\n        matches!(self, Self::NonUpstream(_))\n    }\n\n    pub(crate) fn is_violation(&self) -> bool {\n        matches!(self, Self::Violation)\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "mod condition;\nmod error;\nmod flake;\nmod issue;\nmod summary;\n\n#[cfg(feature = \"ref-statuses\")]\nmod ref_statuses;\n\nuse std::collections::BTreeMap;\nuse std::path::PathBuf;\nuse std::process::ExitCode;\n\nuse clap::Parser;\nuse parse_flake_lock::FlakeLock;\nuse tracing_subscriber::{EnvFilter, fmt, prelude::*};\n\nuse crate::condition::evaluate_condition;\nuse error::FlakeCheckerError;\nuse flake::{FlakeCheckConfig, check_flake_lock};\nuse summary::Summary;\n\n/// A flake.lock checker for Nix projects.\n#[cfg(not(feature = \"ref-statuses\"))]\n#[derive(Parser)]\n#[command(author, version, about, long_about = None)]\nstruct Cli {\n    /// Don't send aggregate sums of each issue type.\n    ///\n    /// See <https://github.com/determinateSystems/flake-checker>.\n    #[arg(long, env = \"NIX_FLAKE_CHECKER_NO_TELEMETRY\", default_value_t = false)]\n    no_telemetry: bool,\n\n    /// Check for outdated Nixpkgs inputs.\n    #[arg(long, env = \"NIX_FLAKE_CHECKER_CHECK_OUTDATED\", default_value_t = true)]\n    check_outdated: bool,\n\n    /// Check that Nixpkgs inputs have \"NixOS\" as the GitHub owner.\n    #[arg(long, env = \"NIX_FLAKE_CHECKER_CHECK_OWNER\", default_value_t = true)]\n    check_owner: bool,\n\n    /// Check that Git refs for Nixpkgs inputs are supported.\n    #[arg(\n        long,\n        env = \"NIX_FLAKE_CHECKER_CHECK_SUPPORTED\",\n        default_value_t = true\n    )]\n    check_supported: bool,\n\n    /// Ignore a missing flake.lock file.\n    #[arg(\n        long,\n        env = \"NIX_FLAKE_CHECKER_IGNORE_MISSING_FLAKE_LOCK\",\n        default_value_t = true\n    )]\n    ignore_missing_flake_lock: bool,\n\n    /// The path to the flake.lock file to check.\n    #[arg(\n        env = \"NIX_FLAKE_CHECKER_FLAKE_LOCK_PATH\",\n        default_value = \"flake.lock\"\n    )]\n    flake_lock_path: PathBuf,\n\n    /// Fail with an exit code of 1 if any issues are encountered.\n    #[arg(\n        long,\n        short,\n        env = \"NIX_FLAKE_CHECKER_FAIL_MODE\",\n        default_value_t = false\n    )]\n    fail_mode: bool,\n\n    /// Nixpkgs input keys as a comma-separated list.\n    #[arg(\n        long,\n        short,\n        env = \"NIX_FLAKE_CHECKER_NIXPKGS_KEYS\",\n        default_value = \"nixpkgs\",\n        value_delimiter = ',',\n        name = \"KEY_LIST\"\n    )]\n    nixpkgs_keys: Vec<String>,\n\n    /// Display Markdown summary (in GitHub Actions).\n    #[arg(\n        long,\n        short,\n        env = \"NIX_FLAKE_CHECKER_MARKDOWN_SUMMARY\",\n        default_value_t = true\n    )]\n    markdown_summary: bool,\n\n    /// The Common Expression Language (CEL) policy to apply to each Nixpkgs input.\n    #[arg(long, short, env = \"NIX_FLAKE_CHECKER_CONDITION\")]\n    condition: Option<String>,\n}\n\n#[cfg(not(feature = \"ref-statuses\"))]\npub(crate) fn supported_refs(ref_statuses: BTreeMap<String, String>) -> Vec<String> {\n    let mut return_value: Vec<String> = ref_statuses\n        .iter()\n        .filter_map(|(channel, status)| {\n            if [\"rolling\", \"stable\", \"deprecated\"].contains(&status.as_str()) {\n                Some(channel.clone())\n            } else {\n                None\n            }\n        })\n        .collect();\n    return_value.sort();\n    return_value\n}\n\n#[cfg(not(feature = \"ref-statuses\"))]\n#[tokio::main]\nasync fn main() -> Result<ExitCode, FlakeCheckerError> {\n    tracing_subscriber::registry()\n        .with(fmt::layer())\n        .with(EnvFilter::from_default_env())\n        .init();\n\n    let ref_statuses: BTreeMap<String, String> =\n        serde_json::from_str(include_str!(\"../ref-statuses.json\")).unwrap();\n\n    let Cli {\n        no_telemetry,\n        check_outdated,\n        check_owner,\n        check_supported,\n        ignore_missing_flake_lock,\n        flake_lock_path,\n        fail_mode,\n        nixpkgs_keys,\n        markdown_summary,\n        condition,\n    } = Cli::parse();\n\n    let (reporter, worker) = detsys_ids_client::builder!()\n        .enable_reporting(!no_telemetry)\n        .fact(\"check_owner\", check_owner)\n        .fact(\"check_outdated\", check_outdated)\n        .fact(\"check_supported\", check_supported)\n        .fact(\"ignore_missing_flake_lock\", ignore_missing_flake_lock)\n        .fact(\"flake_lock_path\", flake_lock_path.to_string_lossy())\n        .fact(\"fail_mode\", fail_mode)\n        .fact(\"condition\", condition.as_deref())\n        .build_or_default()\n        .await;\n\n    if !flake_lock_path.exists() {\n        if ignore_missing_flake_lock {\n            println!(\"no flake lockfile found at {flake_lock_path:?}; ignoring\");\n            return Ok(ExitCode::SUCCESS);\n        } else {\n            println!(\"no flake lockfile found at {flake_lock_path:?}\");\n            return Ok(ExitCode::FAILURE);\n        }\n    }\n\n    let flake_lock = FlakeLock::new(&flake_lock_path)?;\n\n    let flake_check_config = FlakeCheckConfig {\n        check_supported,\n        check_outdated,\n        check_owner,\n        nixpkgs_keys: nixpkgs_keys.clone(),\n        fail_mode,\n    };\n\n    let allowed_refs = supported_refs(ref_statuses.clone());\n\n    let issues = if let Some(condition) = &condition {\n        evaluate_condition(\n            &flake_lock,\n            &nixpkgs_keys,\n            condition,\n            ref_statuses,\n            allowed_refs.clone(),\n        )?\n    } else {\n        check_flake_lock(&flake_lock, &flake_check_config, allowed_refs.clone())?\n    };\n\n    reporter\n        .record(\n            \"flake_issues\",\n            Some(detsys_ids_client::Map::from_iter([\n                (\n                    \"disallowed\".into(),\n                    issues\n                        .iter()\n                        .filter(|issue| issue.kind.is_disallowed())\n                        .count()\n                        .into(),\n                ),\n                (\n                    \"outdated\".into(),\n                    issues\n                        .iter()\n                        .filter(|issue| issue.kind.is_outdated())\n                        .count()\n                        .into(),\n                ),\n                (\n                    \"non_upstream\".into(),\n                    issues\n                        .iter()\n                        .filter(|issue| issue.kind.is_non_upstream())\n                        .count()\n                        .into(),\n                ),\n            ])),\n        )\n        .await;\n\n    let summary = Summary::new(\n        &issues,\n        flake_lock_path,\n        flake_check_config,\n        allowed_refs,\n        condition,\n    );\n\n    if std::env::var(\"GITHUB_ACTIONS\").is_ok() {\n        if markdown_summary {\n            summary.generate_markdown()?;\n        }\n        summary.console_log_errors()?;\n    } else {\n        summary.generate_text()?;\n    }\n\n    drop(reporter);\n    worker.wait().await;\n\n    if fail_mode && !issues.is_empty() {\n        return Ok(ExitCode::FAILURE);\n    }\n\n    Ok(ExitCode::SUCCESS)\n}\n\n#[cfg(feature = \"ref-statuses\")]\n#[derive(Parser)]\nstruct Cli {\n    // Check to make sure that Flake Checker is aware of the current supported branches.\n    #[arg(long, hide = true)]\n    check_ref_statuses: bool,\n\n    // Check to make sure that Flake Checker is aware of the current supported branches.\n    #[arg(long, hide = true)]\n    get_ref_statuses: bool,\n}\n\n#[cfg(feature = \"ref-statuses\")]\nfn main() -> Result<ExitCode, FlakeCheckerError> {\n    let Cli {\n        check_ref_statuses,\n        get_ref_statuses,\n    } = Cli::parse();\n\n    if !get_ref_statuses && !check_ref_statuses {\n        panic!(\"You must select either --get-ref-statuses or --check-ref-statuses\");\n    }\n\n    if get_ref_statuses {\n        match ref_statuses::fetch_ref_statuses() {\n            Ok(refs) => {\n                let json_refs = serde_json::to_string(&refs)?;\n                println!(\"{json_refs}\");\n                return Ok(ExitCode::SUCCESS);\n            }\n            Err(e) => {\n                println!(\"Error fetching ref statuses: {}\", e);\n                return Ok(ExitCode::FAILURE);\n            }\n        }\n    }\n\n    if check_ref_statuses {\n        let mut ref_statuses: BTreeMap<String, String> =\n            serde_json::from_str(include_str!(\"../ref-statuses.json\")).unwrap();\n\n        match ref_statuses::check_ref_statuses(ref_statuses) {\n            Ok(equals) => {\n                if equals {\n                    println!(\"The reference statuses sets are up to date.\");\n                    return Ok(ExitCode::SUCCESS);\n                } else {\n                    println!(\n                        \"The reference statuses sets are NOT up to date. Make sure to update.\"\n                    );\n                    return Ok(ExitCode::FAILURE);\n                }\n            }\n            Err(e) => {\n                println!(\"Error checking ref statuses: {}\", e);\n                return Ok(ExitCode::FAILURE);\n            }\n        }\n    }\n\n    Ok(ExitCode::SUCCESS)\n}\n"
  },
  {
    "path": "src/ref_statuses.rs",
    "content": "use crate::error::FlakeCheckerError;\n\nuse serde::Deserialize;\n\nuse std::collections::BTreeMap;\n\nconst ALLOWED_REFS_URL: &str = \"https://prometheus.nixos.org/api/v1/query?query=channel_revision\";\n\n#[derive(Deserialize)]\nstruct Response {\n    data: Data,\n}\n\n#[derive(Deserialize)]\nstruct Data {\n    result: Vec<DataResult>,\n}\n\n#[derive(Deserialize)]\nstruct DataResult {\n    metric: Metric,\n}\n\n#[derive(Deserialize)]\nstruct Metric {\n    channel: String,\n    status: String,\n}\n\npub(crate) fn check_ref_statuses(\n    ref_statuses: BTreeMap<String, String>,\n) -> Result<bool, FlakeCheckerError> {\n    Ok(fetch_ref_statuses()? == ref_statuses)\n}\n\npub(crate) fn fetch_ref_statuses() -> Result<BTreeMap<String, String>, FlakeCheckerError> {\n    let mut officially_supported: BTreeMap<String, String> =\n        reqwest::blocking::get(ALLOWED_REFS_URL)?\n            .json::<Response>()?\n            .data\n            .result\n            .iter()\n            .map(|res| (res.metric.channel.clone(), res.metric.status.clone()))\n            .collect();\n\n    Ok(officially_supported)\n}\n"
  },
  {
    "path": "src/summary.rs",
    "content": "use crate::FlakeCheckConfig;\nuse crate::error::FlakeCheckerError;\nuse crate::flake::MAX_DAYS;\nuse crate::issue::{Issue, IssueKind};\n\nuse std::fs::OpenOptions;\nuse std::io::Write;\nuse std::path::PathBuf;\n\nuse handlebars::Handlebars;\nuse serde_json::json;\n\nstatic CEL_MARKDOWN_TEMPLATE: &str = include_str!(concat!(\n    env!(\"CARGO_MANIFEST_DIR\"),\n    \"/src/templates/summary.cel.md.hbs\"\n));\n\nstatic CEL_TEXT_TEMPLATE: &str = include_str!(concat!(\n    env!(\"CARGO_MANIFEST_DIR\"),\n    \"/src/templates/summary.cel.txt.hbs\"\n));\n\nstatic STANDARD_MARKDOWN_TEMPLATE: &str = include_str!(concat!(\n    env!(\"CARGO_MANIFEST_DIR\"),\n    \"/src/templates/summary.standard.md.hbs\"\n));\n\nstatic STANDARD_TEXT_TEMPLATE: &str = include_str!(concat!(\n    env!(\"CARGO_MANIFEST_DIR\"),\n    \"/src/templates/summary.standard.txt.hbs\"\n));\n\npub(crate) struct Summary {\n    pub issues: Vec<Issue>,\n    data: serde_json::Value,\n    flake_lock_path: PathBuf,\n    flake_check_config: FlakeCheckConfig,\n    condition: Option<String>,\n}\n\nimpl Summary {\n    pub(crate) fn new(\n        issues: &Vec<Issue>,\n        flake_lock_path: PathBuf,\n        flake_check_config: FlakeCheckConfig,\n        allowed_refs: Vec<String>,\n        condition: Option<String>,\n    ) -> Self {\n        let num_issues = issues.len();\n        let clean = issues.is_empty();\n        let issue_word = if issues.len() == 1 { \"issue\" } else { \"issues\" };\n\n        let data = if let Some(condition) = &condition {\n            let inputs_with_violations: Vec<String> = issues\n                .iter()\n                .filter(|i| i.kind.is_violation())\n                .map(|i| i.input.to_owned())\n                .collect();\n\n            json!({\n                \"issues\": issues,\n                \"num_issues\": num_issues,\n                \"clean\": clean,\n                \"dirty\": !clean,\n                \"issue_word\": issue_word,\n                \"condition\": condition,\n                \"inputs_with_violations\": inputs_with_violations,\n            })\n        } else {\n            let disallowed: Vec<&Issue> =\n                issues.iter().filter(|i| i.kind.is_disallowed()).collect();\n            let outdated: Vec<&Issue> = issues.iter().filter(|i| i.kind.is_outdated()).collect();\n            let non_upstream: Vec<&Issue> =\n                issues.iter().filter(|i| i.kind.is_non_upstream()).collect();\n\n            json!({\n                \"issues\": issues,\n                \"num_issues\": num_issues,\n                \"clean\": clean,\n                \"dirty\": !clean,\n                \"issue_word\": issue_word,\n                // Disallowed refs\n                \"has_disallowed\": !disallowed.is_empty(),\n                \"disallowed\": disallowed,\n                // Outdated refs\n                \"has_outdated\": !outdated.is_empty(),\n                \"outdated\": outdated,\n                // Non-upstream refs\n                \"has_non_upstream\": !non_upstream.is_empty(),\n                \"non_upstream\": non_upstream,\n                // Constants\n                \"max_days\": MAX_DAYS,\n                \"supported_ref_names\": allowed_refs,\n            })\n        };\n\n        Self {\n            issues: issues.to_vec(),\n            data,\n            flake_lock_path,\n            flake_check_config,\n            condition,\n        }\n    }\n\n    pub fn console_log_errors(&self) -> Result<(), FlakeCheckerError> {\n        let file = self.flake_lock_path.to_string_lossy();\n\n        if self.issues.is_empty() {\n            println!(\"The Determinate Nix Flake Checker scanned {file} and found no issues\");\n            return Ok(());\n        }\n\n        if let Some(condition) = &self.condition {\n            println!(\"You supplied this CEL condition for your flake:\\n\\n{condition}\");\n            println!(\"The following inputs violate that condition:\\n\");\n            for issue in self.issues.iter() {\n                println!(\"* {}\", issue.input);\n            }\n        } else {\n            let level = if self.flake_check_config.fail_mode {\n                \"error\"\n            } else {\n                \"warning\"\n            };\n\n            for issue in self.issues.iter() {\n                let input = &issue.input;\n\n                let message: Option<String> = match &issue.kind {\n                    IssueKind::Disallowed(disallowed) => {\n                        if self.flake_check_config.check_supported {\n                            let reference = &disallowed.reference;\n                            Some(format!(\n                                \"the `{input}` input uses the non-supported Git branch `{reference}` for Nixpkgs\"\n                            ))\n                        } else {\n                            None\n                        }\n                    }\n                    IssueKind::Outdated(outdated) => {\n                        if self.flake_check_config.check_outdated {\n                            let num_days_old = outdated.num_days_old;\n                            Some(format!(\n                                \"the `{input}` input is {num_days_old} days old (the max allowed is {MAX_DAYS})\"\n                            ))\n                        } else {\n                            None\n                        }\n                    }\n                    IssueKind::NonUpstream(non_upstream) => {\n                        if self.flake_check_config.check_owner {\n                            let owner = &non_upstream.owner;\n                            Some(format!(\n                                \"the `{input}` input has the non-upstream owner `{owner}` rather than `NixOS` (upstream)\"\n                            ))\n                        } else {\n                            None\n                        }\n                    }\n                    IssueKind::Violation => Some(String::from(\"policy violation\")),\n                };\n\n                if let Some(message) = message {\n                    println!(\"{}: {}\", level.to_uppercase(), message);\n                }\n            }\n        }\n        Ok(())\n    }\n\n    pub fn generate_markdown(&self) -> Result<(), FlakeCheckerError> {\n        let template = if self.condition.is_some() {\n            CEL_MARKDOWN_TEMPLATE\n        } else {\n            STANDARD_MARKDOWN_TEMPLATE\n        };\n\n        let mut handlebars = Handlebars::new();\n\n        handlebars\n            .register_template_string(\"summary.md\", template)\n            .map_err(Box::new)?;\n        let summary_md = handlebars.render(\"summary.md\", &self.data)?;\n\n        let summary_md_filepath = std::env::var(\"GITHUB_STEP_SUMMARY\")?;\n        let mut summary_md_file = OpenOptions::new()\n            .append(true)\n            .create(true)\n            .open(summary_md_filepath)?;\n        summary_md_file.write_all(summary_md.as_bytes())?;\n\n        Ok(())\n    }\n\n    pub fn generate_text(&self) -> Result<(), FlakeCheckerError> {\n        let template = if self.condition.is_some() {\n            CEL_TEXT_TEMPLATE\n        } else {\n            STANDARD_TEXT_TEMPLATE\n        };\n\n        let mut handlebars = Handlebars::new();\n        handlebars\n            .register_template_string(\"summary.txt\", template)\n            .map_err(Box::new)?;\n\n        let summary_txt = handlebars.render(\"summary.txt\", &self.data)?;\n\n        print!(\"{summary_txt}\");\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/templates/summary.cel.md.hbs",
    "content": "# ![](https://avatars.githubusercontent.com/u/80991770?s=30) Flake checkup\n\n{{#if clean}}\nThe Determinate Flake Checker Action scanned your `flake.lock` and didn't identify any issues.\nAll Nixpkgs inputs conform to the flake policy expressed in your supplied [Common Expression Language](https://cel.dev) condition.\n{{/if}}\n\n{{#if dirty}}\n⚠️ The Determinate Nix Installer Action scanned your `flake.lock` and discovered {{num_issues}} {{issue_word}} that we recommend looking into.\nYou supplied this CEL condition:\n\n```ruby\n{{condition}}\n```\n\nThe following inputs violate that condition:\n\n{{#each inputs_with_violations}}\n* `{{this}}`\n{{/each}}\n{{/if}}\n\n<p>Feedback? Let us know at <a href=\"https://github.com/DeterminateSystems/flake-checker\">DeterminateSystems/flake-checker</a>.</p>\n"
  },
  {
    "path": "src/templates/summary.cel.txt.hbs",
    "content": "Flake checker results:\n\n{{#if clean}}\nThe flake checker scanned your flake.lock and didn't identify any issues. You specified this CEL\ncondition:\n\n{{{condition}}}\n\nAll Nixpkgs inputs satisfy this condition.\n{{/if}}\n{{#if dirty}}\nThe flake checker scanned your flake.lock and discovered {{num_issues}} {{issue_word}}\nthat we recommend looking into. Here are the inputs that violate your supplied\ncondition:\n\n{{#each inputs_with_violations}}\n* {{this}}\n{{/each}}\n{{/if}}"
  },
  {
    "path": "src/templates/summary.standard.md.hbs",
    "content": "# ![](https://avatars.githubusercontent.com/u/80991770?s=30) Flake checkup\n\n{{#if clean}}\nThe Determinate Flake Checker Action scanned your `flake.lock` and didn't identify any issues. All Nixpkgs inputs:\n\n✅ Use supported branches\n✅ Are less than 30 days old\n✅ Use upstream Nixpkgs\n{{/if}}\n{{#if dirty}}\n⚠️ The Determinate Nix Installer Action scanned your `flake.lock` and discovered {{num_issues}} {{issue_word}} that we recommend looking into.\n\n{{#if has_disallowed}}\n## Non-supported Git branches for Nixpkgs\n\n{{#each disallowed}}\n* The `{{this.input}}` input uses the `{{this.kind.reference}}` branch\n{{/each}}\n\n<details>\n<summary>What to do 🧰</summary>\n<p>Use one of these branches instead:</p>\n\n{{#each supported_ref_names}}\n* `{{this}}`\n{{/each}}\n\n<p>Here's an example:</p>\n\n```nix\n{\n  inputs.nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n}\n```\n</details>\n\n<details>\n<summary>Why it's important to use supported branches 📚</summary>\n<a href=\"https://zero-to-nix.com/concepts/nixos\">NixOS</a>'s release branches stop receiving updates roughly 7 months after release and then gradually become more and more insecure over time.\nNon-release branches receive unpredictable updates and should be avoided as dependencies.\nRelease branches are also certain to have good <a href=\"https://zero-to-nix.com/concepts/caching\">binary cache</a> coverage, which other branches can't promise.\n</details>\n{{/if}}\n\n{{#if has_outdated}}\n## Outdated Nixpkgs dependencies\n\n{{#each outdated}}\n* The `{{this.input}}` input is **{{this.kind.num_days_old}}** days old\n{{/each}}\n\nThe maximum recommended age is **{{max_days}}** days.\n\n<details>\n<summary>What to do 🧰</summary>\n<p>For a more automated approach, use the <a href=\"https://github.com/determinateSystems/update-flake-lock\"><code>update-flake-lock</code></a>\nGitHub Action to create pull requests to update your <code>flake.lock</code>. Here's an example Actions workflow:</p>\n\n```yaml\nsteps:\n  - name: Automatically update flake.lock\n    uses: DeterminateSystems/update-flake-lock\n    with:\n      pr-title: \"Update flake.lock\"        # PR title\n      pr-labels: [dependencies, automated] # PR labels\n```\n\n<p>For a more ad hoc approach, use the <a href=\"https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake-update.html\"><code>nix flake update</code></a> utility:</p>\n\n```shell\nnix flake update\n```\n</details>\n\n<details>\n<summary>Why it's important to keep Nix dependencies up to date 📚</summary>\n<a href=\"https://github.com/NixOS/nixpkgs\">Nixpkgs</a> receives a continuous stream of security patches to keep your software and systems secure.\nUsing outdated revisions of Nixpkgs can inadvertently expose you to software security risks that have been resolved in more recent releases.\n</details>\n{{/if}}\n\n{{#if has_non_upstream}}\n## Non-upstream Nixpkgs dependencies\n\n{{#each non_upstream}}\n* The `{{this.input}}` input has `{{this.kind.owner}}` as an owner rather than the `NixOS` org\n{{/each}}\n\n<details>\n<summary>What to do 🧰</summary>\n<p>Use a Nixpkgs dependency from the <a href=\"https://github.com/nixos\"><code>NixOS</code></a> org. Here's an example:</p>\n\n```nix\n{\n  inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";\n}\n```\n\n<p>If you need a customized version of Nixpkgs, we recommend that you use\n<a href=\"https://nixos.wiki/wiki/Overlays\">overlays</a> and\nper-package <a href=\"https://ryantm.github.io/nixpkgs/using/overrides\">overrides</a>.</p>\n</details>\n\n<details>\n<summary>Why it's important to use upstream Nixpkgs 📚</summary>\nWe don't recommend using forked or re-exported versions of Nixpkgs.\nWhile this may be convenient in some cases, it can introduce unexpected behaviors and unwanted security risks.\nWhile <a href=\"https://github.com/NixOS/nixpkgs\">upstream Nixpkgs</a> isn't bulletproof&mdash;nothing in software is!&mdash;it has a wide range of security measures in place, most notably continuous integration testing with <a href=\"https://hydra.nixos.org/\">Hydra</a>, that mitigate a great deal of supply chain risk.\n</details>\n{{/if}}\n{{/if}}\n\n<p>Feedback? Let us know at <a href=\"https://github.com/DeterminateSystems/flake-checker\">DeterminateSystems/flake-checker</a>.</p>\n"
  },
  {
    "path": "src/templates/summary.standard.txt.hbs",
    "content": "Flake checker results:\n\n{{#if clean}}\nThe flake checker scanned your flake.lock and didn't identify any issues. All\nNixpkgs inputs:\n\n> Use supported branches\n> Are less than 30 days old\n> Use upstream Nixpkgs\n{{/if}}\n{{#if dirty}}\nThe flake checker scanned your flake.lock and discovered {{num_issues}} {{issue_word}}\nthat we recommend looking into:\n\n{{#if has_disallowed}}\n>>> Non-supported Git branches for Nixpkgs\n\n{{#each disallowed}}\n> The {{this.input}} input uses the {{this.kind.reference}} branch\n{{/each}}\n\n>> What to do\n\nUse one of these branches instead:\n\n{{#each supported_ref_names}}\n* {{this}}\n{{/each}}\n\n>> Why it's important to use supported branches\n\nNixOS's release branches stop receiving updates roughly 7 months after release\nand then gradually become more and more insecure over time. Non-release branches\nreceive unpredictable updates and should be avoided as dependencies. Release\nbranches are also certain to have good binary cache coverage, which other\nbranches can't promise.\n{{/if}}\n\n{{#if has_outdated}}\n>>> Outdated Nixpkgs dependencies\n\n{{#each outdated}}\n> The {{this.input}} input is {{this.kind.num_days_old}} days old\n{{/each}}\n\nThe maximum recommended age is {{max_days}} days.\n\n>> What to do\n\nFor a more automated approach, use the update-flake-lock GitHub Action to create\ncreate pull requests to update your flake.lock (if you're using Github Actions).\n\nFor a more ad hoc approach, use the nix flake update utility.\n\n>> Why it's important to keep Nix dependencies up to date\n\nNixpkgs receives a continuous stream of security patches to keep your software\nand systems secure. Using outdated revisions of Nixpkgs can inadvertently expose\nyou to software security risks that have been resolved in more recent releases.\n{{/if}}\n\n{{#if has_non_upstream}}\n>>> Non-upstream Nixpkgs dependencies\n\n{{#each non_upstream}}\n> The {{this.input}} input has {{this.kind.owner}} as an owner rather\n  than the NixOS org\n{{/each}}\n\n>> What to do\n\nUse a Nixpkgs dependency from the NixOS org, such as github:NixOS/nixpkgs.\n\nIf you need a customized version of Nixpkgs, we recommend that you use overlays\nand per-package overrides.\n\n>> Why it's important to use upstream Nixpkgs\n\nWe don't recommend using forked or re-exported versions of Nixpkgs. While this\nmay be convenient in some cases, it can introduce unexpected behaviors and\nunwanted security risks. While upstream Nixpkgs isn't bulletproof (nothing in\nsoftware is!) it has a wide range of security measures in place, most notably\ncontinuous integration testing with Hydra, that mitigate a great deal of supply\nchain risk.\n{{/if}}\n{{/if}}"
  },
  {
    "path": "templates/README.md.handlebars",
    "content": "# Nix Flake Checker\n\n[![FlakeHub](https://img.shields.io/endpoint?url=https://flakehub.com/f/DeterminateSystems/flake-checker/badge)](https://flakehub.com/flake/DeterminateSystems/flake-checker)\n\n**Nix Flake Checker** is a tool from [Determinate Systems][detsys] that performs \"health\" checks on the [`flake.lock`][lockfile] files in your [flake][flakes]-powered Nix projects.\nIts goal is to help your Nix projects stay on recent and supported versions of [Nixpkgs].\n\nTo run the checker in the root of a Nix project:\n\n```shell\nnix run github:DeterminateSystems/flake-checker\n\n# Or point to an explicit path for flake.lock\nnix run github:DeterminateSystems/flake-checker /path/to/flake.lock\n```\n\nNix Flake Checker looks at your `flake.lock`'s root-level [Nixpkgs] inputs.\nThere are two ways to express flake policies:\n\n- Via [config parameters](#parameters).\n- Via [policy conditions](#policy-conditions) using [Common Expression Language][cel] (CEL).\n\nIf you're running it locally, Nix Flake Checker reports any issues via text output in your terminal.\nBut you can also use Nix Flake Checker [in CI](#the-flake-checker-action).\n\n## Supported branches\n\nAt any given time, [Nixpkgs] has a bounded set of branches that are considered _supported_.\nThe current list, with their statuses:\n\n{{#each supported}}\n- `{{@key}}`\n{{/each}}\n\n## Parameters\n\nBy default, Flake Checker verifies that:\n\n- Any explicit Nixpkgs Git refs are in the [supported list](#supported-branches).\n- Any Nixpkgs dependencies are less than 30 days old.\n- 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).\n\nYou can adjust this behavior via configuration (all are enabled by default but you can disable them):\n\n| Flag                | Environment variable                | Action                                                     | Default |\n| :------------------ | :---------------------------------- | :--------------------------------------------------------- | :------ |\n| `--check-outdated`  | `NIX_FLAKE_CHECKER_CHECK_OUTDATED`  | Check for outdated Nixpkgs inputs                          | `true`  |\n| `--check-owner`     | `NIX_FLAKE_CHECKER_CHECK_OWNER`     | Check that Nixpkgs inputs have `NixOS` as the GitHub owner | `true`  |\n| `--check-supported` | `NIX_FLAKE_CHECKER_CHECK_SUPPORTED` | Check that Git refs for Nixpkgs inputs are supported       | `true`  |\n\n## Policy conditions\n\nYou can apply a CEL condition to your flake using the `--condition` flag.\nHere's an example:\n\n```shell\nflake-checker --condition \"has(numDaysOld) && numDaysOld < 365\"\n```\n\nThis would check that each Nixpkgs input in your `flake.lock` is less than 365 days old.\nThese variables are available in each condition:\n\n| Variable        | Description                                                                                                                              |\n| :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |\n| `gitRef`        | The Git reference of the input.                                                                                                          |\n| `numDaysOld`    | The number of days old the input is.                                                                                                     |\n| `owner`         | The input's owner (if a GitHub input).                                                                                                   |\n| `supportedRefs` | A list of [supported Git refs](#supported-branches) (all are branch names).                                                              |\n| `refStatuses`   | A map. Each key is a branch name. Each value is a branch status (`\"rolling\"`, `\"beta\"`, `\"stable\"`, `\"deprecated\"` or `\"unmaintained\"`). |\n\nWe recommend a condition _at least_ this stringent:\n\n```ruby\nsupportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 30) && owner == 'NixOS'\n```\n\nNote 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.\n\nHere are some other example conditions:\n\n```ruby\n# Updated in the last two weeks\nsupportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 14) && owner == 'NixOS'\n\n# Check for most recent stable Nixpkgs\ngitRef.contains(\"24.05\")\n```\n\n## The Nix Flake Checker Action\n\nYou can automate Nix Flake Checker by adding Determinate Systems' [Nix Flake Checker Action][action] to your GitHub Actions workflows:\n\n```yaml\nchecks:\n  steps:\n    - uses: actions/checkout@v6\n    - name: Check Nix flake Nixpkgs inputs\n      uses: DeterminateSystems/flake-checker-action@main\n```\n\nWhen run in GitHub Actions, Nix Flake Checker always exits with a status code of 0 by default&mdash;and thus never fails your workflows&mdash;and reports its findings as a [Markdown summary][md].\n\n## Telemetry\n\nThe goal of Nix Flake Checker is to help teams stay on recent and supported versions of Nixpkgs.\nThe flake checker collects a little bit of telemetry information to help us make that true.\n\nTo disable diagnostic reporting, set the diagnostics URL to an empty string by passing `--no-telemetry` or setting `FLAKE_CHECKER_NO_TELEMETRY=true`.\n\nYou can read the full privacy policy for [Determinate Systems][detsys], the creators of this tool and the [Determinate Nix Installer][installer], [here][privacy].\n\n## Rust library\n\nThe Nix Flake Checker is written in [Rust].\nThis repo exposes a [`parse-flake-lock`](./parse-flake-lock) crate that you can use to parse [`flake.lock` files][lockfile] in your own Rust projects.\nTo add that dependency:\n\n```toml\n[dependencies]\nparse-flake-lock = { git = \"https://github.com/DeterminateSystems/flake-checker\", branch = \"main\" }\n```\n\nHere's an example usage:\n\n```rust\nuse std::path::Path;\n\nuse parse_flake_lock::{FlakeLock, FlakeLockParseError};\n\nfn main() -> Result<(), FlakeLockParseError> {\n    let flake_lock = FlakeLock::new(Path::new(\"flake.lock\"))?;\n    println!(\"flake.lock info:\");\n    println!(\"version: {version}\", version=flake_lock.version);\n    println!(\"root node: {root:?}\", root=flake_lock.root);\n    println!(\"all nodes: {nodes:?}\", nodes=flake_lock.nodes);\n\n    Ok(())\n}\n```\n\nThe `parse-flake-lock` crate doesn't yet exhaustively parse all input node types, instead using a \"fallthrough\" mechanism that parses input types that don't yet have explicit struct definitions to a [`serde_json::value::Value`][val].\nIf you'd like to help make the parser more exhaustive, [pull requests][prs] are quite welcome.\n\n[action]: https://github.com/DeterminateSystems/flake-checker-action\n[cel]: https://cel.dev\n[detsys]: https://determinate.systems\n[flakes]: https://zero-to-nix.com/concepts/flakes\n[install]: https://zero-to-nix.com/start/install\n[installer]: https://github.com/DeterminateSystems/nix-installer\n[lockfile]: https://zero-to-nix.com/concepts/flakes#lockfile\n[md]: https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries\n[nixos-org]: https://github.com/NixOS\n[nixpkgs]: https://github.com/NixOS/nixpkgs\n[privacy]: https://determinate.systems/policies/privacy\n[prs]: /pulls\n[rust]: https://rust-lang.org\n[telemetry]: https://github.com/DeterminateSystems/nix-flake-checker/blob/main/src/telemetry.rs#L29-L43\n[val]: https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html\n"
  },
  {
    "path": "tests/cel-condition.cel",
    "content": "['nixos-unstable', 'nixos-unstable-small', 'nixpkgs-unstable'].map(rev, supportedRefs.contains(rev))\n    && owner == 'NixOS'\n    && gitRef == 'nixos-unstable'\n    && supportedRefs.contains(gitRef)\n    && has(numDaysOld)\n    && numDaysOld > 0\n"
  }
]