├── .editorconfig ├── .envrc ├── .eslintrc.json ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── cache-test.sh │ ├── cache-tester.nix │ ├── ci.yml │ └── flakehub-cache.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── action.yml ├── dist ├── index.d.ts ├── index.js ├── index.js.map └── package.json ├── flake.lock ├── flake.nix ├── package.json ├── pnpm-lock.yaml ├── prettier.config.cjs ├── shell.nix ├── src ├── helpers.ts ├── index.ts └── mnc-warn.ts ├── tsconfig.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 2.1.1; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.1.1/direnvrc" "sha256-b6qJ4r34rbE23yWjMqbmu3ia2z4b2wIlZUksBke/ol0=" 3 | fi 4 | 5 | use_flake 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "settings": { 11 | "import/resolver": { 12 | "typescript": {} 13 | } 14 | }, 15 | "rules": { 16 | "i18n-text/no-en": "off", 17 | "eslint-comments/no-use": "off", 18 | "import/no-namespace": "off", 19 | "no-unused-vars": "off", 20 | "@typescript-eslint/no-unused-vars": [ 21 | "error", 22 | { 23 | "argsIgnorePattern": "^_" 24 | } 25 | ], 26 | "@typescript-eslint/explicit-member-accessibility": [ 27 | "error", 28 | { 29 | "accessibility": "no-public" 30 | } 31 | ], 32 | "@typescript-eslint/no-base-to-string": "error", 33 | "@typescript-eslint/no-require-imports": "error", 34 | "@typescript-eslint/array-type": "error", 35 | "@typescript-eslint/await-thenable": "error", 36 | "@typescript-eslint/ban-ts-comment": "error", 37 | "camelcase": "error", 38 | "@typescript-eslint/consistent-type-assertions": "error", 39 | "@typescript-eslint/explicit-function-return-type": [ 40 | "error", 41 | { 42 | "allowExpressions": true 43 | } 44 | ], 45 | "@typescript-eslint/func-call-spacing": ["error", "never"], 46 | "@typescript-eslint/no-array-constructor": "error", 47 | "@typescript-eslint/no-empty-interface": "error", 48 | "@typescript-eslint/no-explicit-any": "error", 49 | "@typescript-eslint/no-floating-promises": "error", 50 | "@typescript-eslint/no-extraneous-class": "error", 51 | "@typescript-eslint/no-for-in-array": "error", 52 | "@typescript-eslint/no-inferrable-types": "error", 53 | "@typescript-eslint/no-misused-new": "error", 54 | "@typescript-eslint/no-namespace": "error", 55 | "@typescript-eslint/no-non-null-assertion": "warn", 56 | "@typescript-eslint/no-unnecessary-qualifier": "error", 57 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 58 | "@typescript-eslint/no-useless-constructor": "error", 59 | "@typescript-eslint/no-var-requires": "error", 60 | "@typescript-eslint/prefer-for-of": "warn", 61 | "@typescript-eslint/prefer-function-type": "warn", 62 | "@typescript-eslint/prefer-includes": "error", 63 | "@typescript-eslint/prefer-nullish-coalescing": "error", 64 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 65 | "@typescript-eslint/promise-function-async": "error", 66 | "@typescript-eslint/require-array-sort-compare": "error", 67 | "@typescript-eslint/restrict-plus-operands": "error", 68 | "@typescript-eslint/type-annotation-spacing": "error", 69 | "@typescript-eslint/unbound-method": "error" 70 | }, 71 | "env": { 72 | "node": true, 73 | "es6": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ##### Description 2 | 3 | 7 | 8 | ##### Checklist 9 | 10 | - [ ] Tested changes against a test repository 11 | - [ ] Added or updated relevant documentation (leave unchecked if not applicable) 12 | - [ ] (If this PR is for a release) Updated README to point to the new tag (leave unchecked if not applicable) 13 | -------------------------------------------------------------------------------- /.github/workflows/cache-test.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e 4 | set -ux 5 | 6 | seed="$(date)-$RANDOM" 7 | 8 | log="${MAGIC_NIX_CACHE_DAEMONDIR}/daemon.log" 9 | 10 | flakehub_binary_cache=https://cache.flakehub.com 11 | gha_binary_cache=http://127.0.0.1:37515 12 | 13 | is_gh_throttled() { 14 | grep 'GitHub Actions Cache throttled Magic Nix Cache' "${log}" 15 | } 16 | 17 | # Check that the action initialized correctly. 18 | if [ "$EXPECT_FLAKEHUB" == "true" ]; then 19 | grep 'FlakeHub cache is enabled' "${log}" 20 | grep 'Using cache' "${log}" 21 | else 22 | grep 'FlakeHub cache is disabled' "${log}" \ 23 | || grep 'FlakeHub cache initialization failed:' "${log}" 24 | fi 25 | 26 | if [ "$EXPECT_GITHUB_CACHE" == "true" ]; then 27 | grep 'GitHub Action cache is enabled' "${log}" 28 | else 29 | grep 'Native GitHub Action cache is disabled' "${log}" 30 | fi 31 | 32 | # Build something. 33 | outpath=$(nix-build .github/workflows/cache-tester.nix --argstr seed "$seed") 34 | 35 | # Wait until it has been pushed succesfully. 36 | if [ "$EXPECT_FLAKEHUB" == "true" ]; then 37 | found= 38 | for ((i = 0; i < 60; i++)); do 39 | sleep 1 40 | if grep "✅ $(basename "${outpath}")" "${log}"; then 41 | found=1 42 | break 43 | fi 44 | done 45 | if [[ -z $found ]]; then 46 | echo "FlakeHub push did not happen." >&2 47 | exit 1 48 | fi 49 | fi 50 | 51 | if [ "$EXPECT_GITHUB_CACHE" == "true" ]; then 52 | found= 53 | for ((i = 0; i < 60; i++)); do 54 | sleep 1 55 | if grep "Uploaded '${outpath}' to the GitHub Action Cache" "${log}"; then 56 | found=1 57 | break 58 | fi 59 | done 60 | if [[ -z $found ]]; then 61 | echo "GitHub Actions Cache push did not happen." >&2 62 | 63 | if ! is_gh_throttled; then 64 | exit 1 65 | fi 66 | fi 67 | fi 68 | 69 | 70 | 71 | if [ "$EXPECT_FLAKEHUB" == "true" ]; then 72 | # Check the FlakeHub binary cache to see if the path is really there. 73 | nix path-info --store "${flakehub_binary_cache}" "${outpath}" 74 | fi 75 | 76 | if [ "$EXPECT_GITHUB_CACHE" == "true" ] && ! is_gh_throttled; then 77 | # Check the GitHub binary cache to see if the path is really there. 78 | nix path-info --store "${gha_binary_cache}" "${outpath}" 79 | fi 80 | 81 | rm ./result 82 | nix store delete "${outpath}" 83 | if [ -f "$outpath" ]; then 84 | echo "$outpath still exists? can't test" 85 | exit 1 86 | fi 87 | 88 | rm -rf ~/.cache/nix 89 | 90 | echo "-------" 91 | echo "Trying to substitute the build again..." 92 | echo "if it fails, the cache is broken." 93 | 94 | if [ "$EXPECT_FLAKEHUB" == "true" ]; then 95 | # Check the FlakeHub binary cache to see if the path is really there. 96 | nix path-info --store "${flakehub_binary_cache}" "${outpath}" 97 | fi 98 | 99 | if [ "$EXPECT_GITHUB_CACHE" == "true" ] && ! is_gh_throttled; then 100 | # Check the FlakeHub binary cache to see if the path is really there. 101 | nix path-info --store "${gha_binary_cache}" "${outpath}" 102 | fi 103 | 104 | if ([ "$EXPECT_GITHUB_CACHE" == "true" ] && ! is_gh_throttled) || [ "$EXPECT_FLAKEHUB" == "true" ]; then 105 | nix-store --realize -vvvvvvvv "$outpath" 106 | fi 107 | -------------------------------------------------------------------------------- /.github/workflows/cache-tester.nix: -------------------------------------------------------------------------------- 1 | { seed }: 2 | derivation { 3 | name = "cache-test"; 4 | system = builtins.currentSystem; 5 | 6 | builder = "/bin/sh"; 7 | args = [ "-euxc" "echo \"$seed\" > $out" ]; 8 | 9 | inherit seed; 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Install Nix 16 | uses: DeterminateSystems/determinate-nix-action@main 17 | - uses: DeterminateSystems/flakehub-cache-action@main 18 | - name: Check shell scripts 19 | run: | 20 | nix develop --command shellcheck ./.github/workflows/cache-test.sh 21 | - uses: DeterminateSystems/determinate-nix-action@main 22 | - name: Install pnpm dependencies 23 | run: nix develop --command pnpm install 24 | - name: Check formatting 25 | run: nix develop --command pnpm run check-fmt 26 | - name: Lint 27 | run: nix develop --command pnpm run lint 28 | - name: Build 29 | run: nix develop --command pnpm run build 30 | - name: Package 31 | run: nix develop --command pnpm run package 32 | - run: git status --porcelain=v1 33 | - run: git diff --exit-code 34 | 35 | test-no-nix: 36 | needs: build 37 | name: "Test: Nix not installed" 38 | runs-on: ubuntu-22.04 39 | permissions: 40 | id-token: "write" 41 | contents: "read" 42 | env: 43 | ACTIONS_STEP_DEBUG: true 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Cache the store 47 | uses: ./ 48 | with: 49 | _internal-strict-mode: true 50 | 51 | run-x86_64-linux-untrusted: 52 | needs: build 53 | name: Run x86_64-linux, Untrusted 54 | runs-on: ubuntu-22.04 55 | permissions: 56 | id-token: "write" 57 | contents: "read" 58 | env: 59 | ACTIONS_STEP_DEBUG: true 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Install Nix 63 | uses: DeterminateSystems/determinate-nix-action@main 64 | with: 65 | extra-conf: | 66 | narinfo-cache-negative-ttl = 0 67 | trusted-users = root 68 | - name: Cache the store 69 | uses: ./ 70 | with: 71 | _internal-strict-mode: true 72 | 73 | run-systems: 74 | if: github.event_name == 'merge_group' 75 | needs: build 76 | name: "Test: ${{ matrix.systems.nix-system }} gha:${{matrix.use-gha-cache}},fhc:${{matrix.use-flakehub}},id:${{matrix.id-token}},determinate:${{matrix.determinate}}" 77 | runs-on: "${{ matrix.systems.runner }}" 78 | permissions: 79 | id-token: "write" 80 | contents: "read" 81 | env: 82 | ACTIONS_STEP_DEBUG: true 83 | strategy: 84 | fail-fast: false 85 | matrix: 86 | determinate: [true, false] 87 | use-gha-cache: ["disabled", "no-preference", "enabled"] 88 | use-flakehub: ["disabled", "no-preference", "enabled"] 89 | id-token: ["write", "none"] 90 | systems: 91 | - nix-system: "aarch64-darwin" 92 | runner: "macos-latest" 93 | - nix-system: "x86_64-darwin" 94 | runner: "macos-13" 95 | - nix-system: "aarch64-linux" 96 | runner: "namespace-profile-default-arm64" 97 | - nix-system: "x86_64-linux" 98 | runner: "ubuntu-22.04" 99 | steps: 100 | - uses: actions/checkout@v4 101 | - name: Install Nix on ${{ matrix.systems.nix-system }} system 102 | uses: DeterminateSystems/nix-installer-action@main 103 | with: 104 | _internal-obliterate-actions-id-token-request-variables: ${{ matrix.id-token == 'none' }} 105 | determinate: ${{ matrix.determinate }} 106 | extra-conf: | 107 | narinfo-cache-negative-ttl = 0 108 | - name: Cache the store 109 | uses: ./ 110 | with: 111 | _internal-strict-mode: true 112 | _internal-obliterate-actions-id-token-request-variables: ${{ matrix.id-token == 'none' }} 113 | use-gha-cache: ${{ matrix.use-gha-cache }} 114 | use-flakehub: ${{ matrix.use-flakehub }} 115 | - name: Check the cache for liveness 116 | env: 117 | EXPECT_FLAKEHUB: ${{ toJson(matrix.use-flakehub != 'disabled' && matrix.id-token == 'write') }} 118 | EXPECT_GITHUB_CACHE: ${{ toJson( 119 | (matrix.use-gha-cache != 'disabled') 120 | && ( 121 | (!(matrix.use-flakehub != 'disabled' && matrix.id-token == 'write')) 122 | || (matrix.use-gha-cache == 'enabled') 123 | ) 124 | ) }} 125 | run: | 126 | .github/workflows/cache-test.sh 127 | 128 | success: 129 | runs-on: ubuntu-latest 130 | needs: run-systems 131 | steps: 132 | - run: "true" 133 | - run: | 134 | echo "A dependent in the build matrix failed." 135 | exit 1 136 | if: | 137 | contains(needs.*.result, 'failure') || 138 | contains(needs.*.result, 'cancelled') 139 | -------------------------------------------------------------------------------- /.github/workflows/flakehub-cache.yml: -------------------------------------------------------------------------------- 1 | name: Push dev shell to FlakeHub Cache 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | push-dev-shell-to-flakehub-cache: 9 | env: 10 | ACTIONS_STEP_DEBUG: true 11 | runs-on: ${{ matrix.systems.runner }} 12 | permissions: 13 | id-token: "write" 14 | contents: "read" 15 | strategy: 16 | matrix: 17 | systems: 18 | - nix-system: "aarch64-darwin" 19 | runner: "macos-latest-xlarge" 20 | - nix-system: "x86_64-darwin" 21 | runner: "macos-13" 22 | - nix-system: "x86_64-linux" 23 | runner: "ubuntu-22.04" 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: DeterminateSystems/determinate-nix-action@main 27 | - uses: DeterminateSystems/flakehub-cache-action@main 28 | - name: Build dev shell for ${{ matrix.systems.nix-system }} on ${{ matrix.systems.runner }} 29 | run: | 30 | nix build .#devShells.${{ matrix.systems.nix-system }}.default 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | result* 3 | 4 | node_modules 5 | 6 | # magic-nix-cache 7 | creds.env 8 | creds.json 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Determinate Systems, Inc., Zhaofeng Li 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magic Nix Cache 2 | 3 | > [!WARNING] 4 | > The [Magic Nix Cache will will stop working](https://determinate.systems/posts/magic-nix-cache-free-tier-eol) on **February 1st, 2025** unless you're on [GitHub Enterprise Server](https://github.com/enterprise). 5 | > 6 | > You can upgrade to [FlakeHub Cache](https://flakehub.com/cache) and get **one month free** using the coupon code **`FHC`**. 7 | > 8 | > For more information, read [this blog post](https://determinate.systems/posts/magic-nix-cache-free-tier-eol/). 9 | 10 | Save 30-50%+ of CI time without any effort or cost. 11 | Use Magic Nix Cache, a totally free and zero-configuration binary cache for Nix on GitHub Actions. 12 | 13 | Add our [GitHub Action][action] after installing Nix, in your workflow, like this: 14 | 15 | ```yaml 16 | - uses: DeterminateSystems/flakehub-cache-action@main 17 | ``` 18 | 19 | See [Usage](#usage) for a detailed example. 20 | 21 | ## Why use the Magic Nix Cache? 22 | 23 | Magic Nix Cache uses the GitHub Actions [built-in cache][ghacache] to share builds between Workflow runs, and has many advantages over alternatives. 24 | 25 | 1. Totally free: backed by GitHub Actions' cache, there is no additional service to pay for. 26 | 1. Zero configuration: add our action to your workflow. 27 | That's it. 28 | Everything built in your workflow will be cached. 29 | 1. No secrets: Forks and pull requests benefit from the cache, too. 30 | 1. Secure: Magic Nix Cache follows the [same semantics as the GitHub Actions cache][semantics], and malicious pull requests cannot pollute your project. 31 | 1. Private: The cache is stored in the GitHub Actions cache, not with an additional third party. 32 | 33 | > **Note:** the Magic Nix Cache doesn't offer a publicly available cache. 34 | > This means the cache is only usable in CI. 35 | > [Zero to Nix][z2n] has an article on binary caching if you want to [share Nix builds][z2ncache] with users outside of CI. 36 | 37 | ## Usage 38 | 39 | Add it to your Linux and macOS GitHub Actions workflows, like this: 40 | 41 | ```yaml 42 | name: CI 43 | 44 | on: 45 | push: 46 | pull_request: 47 | 48 | jobs: 49 | check: 50 | runs-on: ubuntu-22.04 51 | permissions: 52 | id-token: "write" 53 | contents: "read" 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: DeterminateSystems/determinate-nix-action@v3 57 | - uses: DeterminateSystems/flakehub-cache-action@main 58 | - uses: DeterminateSystems/flake-checker-action@main 59 | - name: Run `nix build` 60 | run: nix build . 61 | ``` 62 | 63 | That's it. 64 | Everything built in your workflow will be cached. 65 | 66 | ## Usage Notes 67 | 68 | The GitHub Actions Cache has a rate limit on reads and writes. 69 | Occasionally, large projects or large rebuilds may exceed those rate-limits, and you'll see evidence of that in your logs. 70 | The error looks like this: 71 | 72 | ``` 73 | error: unable to download 'http://127.0.0.1:37515/<...>': HTTP error 418 74 | response body: 75 | GitHub API error: API error (429 Too Many Requests): StructuredApiError { message: "Request was blocked due to exceeding usage of resource 'Count' in namespace ''." } 76 | ``` 77 | 78 | The caching daemon and Nix both handle this gracefully, and won't cause your CI to fail. 79 | When the rate limit is exceeded while pulling dependencies, your workflow may perform more builds than usual. 80 | When the rate limit is exceeded while uploading to the cache, the remainder of those store paths will be uploaded on the next run of the workflow. 81 | 82 | ## Concepts 83 | 84 | ### Upstream cache 85 | 86 | When you configure an upstream cache for the Magic Nix Cache, any store paths fetched from that source are _not_ cached because they are known to be fetchable on future workflow runs. 87 | The default is `https://cache.nixos.org` but you can set a different upstream: 88 | 89 | ```yaml 90 | - uses: DeterminateSystems/flakehub-cache-action@main 91 | with: 92 | upstream-cache: https://my-binary-cache.com 93 | ``` 94 | 95 | ## Action Options 96 | 97 | 100 | 101 | | Parameter | Description | Required | Default | 102 | | --------------------------- | --------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------- | 103 | | `diagnostic-endpoint` | Diagnostic endpoint url where diagnostics and performance data is sent. To disable set this to an empty string. | | https://install.determinate.systems/magic-nix-cache/perf | 104 | | `diff-store` | Whether or not to diff the store before and after Magic Nix Cache runs. | | `false` | 105 | | `flakehub-api-server` | The FlakeHub API server. | | https://api.flakehub.com | 106 | | `flakehub-cache-server` | The FlakeHub binary cache server. | | https://cache.flakehub.com | 107 | | `flakehub-flake-name` | The name of your flake on FlakeHub. The empty string will autodetect your FlakeHub flake. | | `""` | 108 | | `listen` | The host and port to listen on. | | 127.0.0.1:37515 | 109 | | `source-binary` | Run a version of the cache binary from somewhere already on disk. Conflicts with all other `source-*` options. | | | 110 | | `source-branch` | The branch of `magic-nix-cache` to use. Conflicts with all other `source-*` options. | | main | 111 | | `source-pr` | The PR of `magic-nix-cache` to use. Conflicts with all other `source-*` options. | | | 112 | | `source-revision` | The revision of `nix-magic-nix-cache` to use. Conflicts with all other `source-*` options. | | | 113 | | `source-tag` | The tag of `magic-nix-cache` to use. Conflicts with all other `source-*` options. | | | 114 | | `source-url` | A URL pointing to a `magic-nix-cache` binary. Overrides all other `source-*` options. | | | 115 | | `startup-notification-port` | The port magic-nix-cache uses for daemon startup notification. | | 41239 | 116 | | `upstream-cache` | Your preferred upstream cache. Store paths in this store will not be cached in GitHub Actions' cache. | | https://cache.nixos.org | 117 | | `use-flakehub` | Whether to upload build results to FlakeHub Cache (private beta). | | true | 118 | | `use-gha-cache` | Whether to upload build results to the GitHub Actions cache. | | true | 119 | 120 | [gha-cache]: https://docs.github.com/en/rest/actions/cache 121 | [detsys]: https://determinate.systems/ 122 | [action]: https://github.com/DeterminateSystems/magic-nix-cache-action/ 123 | [installer]: https://github.com/DeterminateSystems/nix-installer/ 124 | [ghacache]: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows 125 | [privacy]: https://determinate.systems/policies/privacy 126 | [telemetry]: https://github.com/DeterminateSystems/magic-nix-cache/blob/main/magic-nix-cache/src/telemetry.rs 127 | [semantics]: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache 128 | [z2ncache]: https://zero-to-nix.com/concepts/caching#binary-caches 129 | [zhaofeng]: https://github.com/zhaofengli/ 130 | [attic]: https://github.com/zhaofengli/attic 131 | [colmena]: https://github.com/zhaofengli/colmena 132 | [z2n]: https://zero-to-nix.com 133 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Magic Nix Cache 2 | branding: 3 | icon: "box" 4 | color: "purple" 5 | description: "Free, no-configuration Nix cache. Cut CI time by 50% or more by caching to GitHub Actions' cache." 6 | inputs: 7 | use-gha-cache: 8 | description: | 9 | Whether to upload build results to the Github Actions cache. 10 | Set to "no-preference" or null to have the GitHub Actions cache turn on if it is available, and FlakeHub Cache is not available (default). 11 | Set to "enabled" or true to explicitly request the GitHub Actions Cache. 12 | Set to "disabled" or false to explicitly disable the GitHub Actions Cache. 13 | default: null 14 | required: false 15 | listen: 16 | description: The host and port to listen on. 17 | default: 127.0.0.1:37515 18 | upstream-cache: 19 | description: Your preferred upstream cache. Store paths in this store will not be cached in GitHub Actions' cache. 20 | default: https://cache.nixos.org 21 | diagnostic-endpoint: 22 | description: "Diagnostic endpoint url where diagnostics and performance data is sent. To disable set this to an empty string." 23 | default: "-" 24 | use-flakehub: 25 | description: | 26 | Whether to upload build results to FlakeHub Cache. 27 | Set to "no-preference" or null to have FlakeHub Cache turn on opportunistically (default). 28 | Set to "enabled" or true to explicitly request FlakeHub Cache. 29 | Set to "disabled" or false to explicitly disable FlakeHub Cache. 30 | default: null 31 | required: false 32 | flakehub-cache-server: 33 | description: "The FlakeHub binary cache server." 34 | default: "https://cache.flakehub.com" 35 | flakehub-api-server: 36 | description: "The FlakeHub API server." 37 | default: "https://api.flakehub.com" 38 | flakehub-flake-name: 39 | description: "The name of your flake on FlakeHub. The empty string will autodetect your FlakeHub flake." 40 | default: "" 41 | required: false 42 | startup-notification-port: 43 | description: "The port magic-nix-cache uses for daemon startup notification." 44 | default: 41239 45 | diff-store: 46 | description: "Whether or not to diff the store before and after Magic Nix Cache runs" 47 | default: false 48 | required: false 49 | 50 | source-binary: 51 | description: Run a version of the cache binary from somewhere already on disk. Conflicts with all other `source-*` options. 52 | required: false 53 | source-branch: 54 | description: The branch of `magic-nix-cache` to use. Conflicts with all other `source-*` options. 55 | required: false 56 | source-pr: 57 | description: The PR of `magic-nix-cache` to use. Conflicts with all other `source-*` options. 58 | required: false 59 | source-revision: 60 | description: The revision of `nix-magic-nix-cache` to use. Conflicts with all other `source-*` options. 61 | required: false 62 | source-tag: 63 | description: The tag of `magic-nix-cache` to use. Conflicts with all other `source-*` options. 64 | required: false 65 | source-url: 66 | description: A URL pointing to a `magic-nix-cache` binary. Overrides all other `source-*` options. 67 | required: false 68 | _internal-strict-mode: 69 | description: Whether to fail when any errors are thrown. Used only to test the Action; do not set this in your own workflows. 70 | required: false 71 | default: false 72 | 73 | runs: 74 | using: "node20" 75 | main: "./dist/index.js" 76 | post: "./dist/index.js" 77 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export { } 3 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/helpers.ts","../src/mnc-warn.ts","../src/index.ts"],"sourcesContent":["import * as actionsCore from \"@actions/core\";\nimport * as fs from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport path from \"node:path\";\nimport { Tail } from \"tail\";\n\nexport function getTrinaryInput(\n name: string,\n): \"enabled\" | \"disabled\" | \"no-preference\" {\n const trueValue = [\"true\", \"True\", \"TRUE\", \"enabled\"];\n const falseValue = [\"false\", \"False\", \"FALSE\", \"disabled\"];\n const noPreferenceValue = [\"\", \"null\", \"no-preference\"];\n\n const val = actionsCore.getInput(name);\n if (trueValue.includes(val)) {\n return \"enabled\";\n }\n if (falseValue.includes(val)) {\n return \"disabled\";\n }\n if (noPreferenceValue.includes(val)) {\n return \"no-preference\";\n }\n\n const possibleValues = trueValue\n .concat(falseValue)\n .concat(noPreferenceValue)\n .join(\" | \");\n throw new TypeError(\n `Input ${name} does not look like a trinary, which requires one of:\\n${possibleValues}`,\n );\n}\n\nexport function tailLog(daemonDir: string): Tail {\n const log = new Tail(path.join(daemonDir, \"daemon.log\"));\n actionsCore.debug(`tailing daemon.log...`);\n log.on(\"line\", (line) => {\n actionsCore.info(line);\n });\n return log;\n}\n\nexport async function netrcPath(): Promise {\n const expectedNetrcPath = path.join(\n process.env[\"RUNNER_TEMP\"] ?? os.tmpdir(),\n \"determinate-nix-installer-netrc\",\n );\n try {\n await fs.access(expectedNetrcPath);\n return expectedNetrcPath;\n } catch {\n // `nix-installer` was not used, the user may be registered with FlakeHub though.\n const destinedNetrcPath = path.join(\n process.env[\"RUNNER_TEMP\"] ?? os.tmpdir(),\n \"magic-nix-cache-netrc\",\n );\n try {\n await flakeHubLogin(destinedNetrcPath);\n } catch (e) {\n actionsCore.info(\n \"FlakeHub Cache is disabled due to missing or invalid token\",\n );\n actionsCore.info(\n `If you're signed up for FlakeHub Cache, make sure that your Actions config has a \\`permissions\\` block with \\`id-token\\` set to \"write\" and \\`contents\\` set to \"read\"`,\n );\n actionsCore.debug(`Error while logging into FlakeHub: ${e}`);\n }\n return destinedNetrcPath;\n }\n}\n\nasync function flakeHubLogin(netrc: string): Promise {\n const jwt = await actionsCore.getIDToken(\"api.flakehub.com\");\n\n await fs.writeFile(\n netrc,\n [\n `machine api.flakehub.com login flakehub password ${jwt}`,\n `machine flakehub.com login flakehub password ${jwt}`,\n `machine cache.flakehub.com login flakehub password ${jwt}`,\n ].join(\"\\n\"),\n );\n\n actionsCore.info(\"Logged in to FlakeHub.\");\n}\n","import actionsCore from \"@actions/core\";\nimport glob from \"@actions/glob\";\nimport { stringifyError } from \"detsys-ts\";\nimport * as fs from \"node:fs/promises\";\n\nexport async function warnOnMnc(): Promise {\n const cwd = process.cwd();\n\n const patterns = [\n \".github/actions/*.yaml\",\n \".github/actions/*.yml\",\n \".github/workflows/*.yaml\",\n \".github/workflows/*.yml\",\n ];\n const globber = await glob.create(patterns.join(\"\\n\"));\n const files = await globber.glob();\n\n for (const file of files) {\n try {\n const relativeFilePath = file.replace(cwd, \".\");\n const lines = (await fs.readFile(file, { encoding: \"utf8\" })).split(\"\\n\");\n\n for (const lineIdx of lines.keys()) {\n const line = lines[lineIdx];\n\n if (\n line\n .toLowerCase()\n .includes(\"determinatesystems/magic-nix-cache-action\")\n ) {\n const actionPosition = line\n .toLowerCase()\n .indexOf(\"determinatesystems/magic-nix-cache-action\");\n const actionPositionEnd =\n actionPosition + \"determinatesystems/magic-nix-cache-action\".length;\n\n const newLine = line.replace(\n /DeterminateSystems\\/magic-nix-cache-action/gi,\n \"DeterminateSystems/flakehub-cache-action\",\n );\n actionsCore.warning(\n [\n \"Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025.\",\n \"To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead.\",\n \"\",\n \"Replace...\",\n line,\n \"\",\n \"...with...\",\n newLine,\n \"\",\n \"For more details: https://dtr.mn/magic-nix-cache-eol\",\n ].join(\"\\n\"),\n {\n title: \"Magic Nix Cache is deprecated\",\n file: relativeFilePath,\n startLine: lineIdx + 1,\n startColumn: actionPosition,\n endColumn: actionPositionEnd,\n },\n );\n }\n }\n } catch (err) {\n actionsCore.debug(stringifyError(err));\n }\n }\n}\n","import { getTrinaryInput, netrcPath, tailLog } from \"./helpers.js\";\nimport { warnOnMnc } from \"./mnc-warn.js\";\nimport * as actionsCore from \"@actions/core\";\nimport { DetSysAction, inputs, stringifyError } from \"detsys-ts\";\nimport got, { Got, Response } from \"got\";\nimport * as http from \"http\";\nimport { SpawnOptions, spawn } from \"node:child_process\";\nimport { mkdirSync, openSync, readFileSync } from \"node:fs\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { setTimeout } from \"node:timers/promises\";\n\n// The ENV_DAEMON_DIR is intended to determine if we \"own\" the daemon or not,\n// in the case that a user has put the magic nix cache into their workflow\n// twice.\nconst ENV_DAEMON_DIR = \"MAGIC_NIX_CACHE_DAEMONDIR\";\n\nconst FACT_ENV_VARS_PRESENT = \"required_env_vars_present\";\nconst FACT_SENT_SIGTERM = \"sent_sigterm\";\nconst FACT_DIFF_STORE_ENABLED = \"diff_store\";\nconst FACT_ALREADY_RUNNING = \"noop_mode\";\n\nconst STATE_DAEMONDIR = \"MAGIC_NIX_CACHE_DAEMONDIR\";\nconst STATE_ERROR_IN_MAIN = \"ERROR_IN_MAIN\";\nconst STATE_STARTED = \"MAGIC_NIX_CACHE_STARTED\";\nconst STARTED_HINT = \"true\";\n\nconst TEXT_ALREADY_RUNNING =\n \"Magic Nix Cache is already running, this workflow job is in noop mode. Is the Magic Nix Cache in the workflow twice?\";\nconst TEXT_TRUST_UNTRUSTED =\n \"The Nix daemon does not consider the user running this workflow to be trusted. Magic Nix Cache is disabled.\";\nconst TEXT_TRUST_UNKNOWN =\n \"The Nix daemon may not consider the user running this workflow to be trusted. Magic Nix Cache may not start correctly.\";\n\nclass MagicNixCacheAction extends DetSysAction {\n private hostAndPort: string;\n private diffStore: boolean;\n private httpClient: Got;\n private daemonDir: string;\n private daemonStarted: boolean;\n\n // This is set to `true` if the MNC is already running, in which case the\n // workflow will use the existing process rather than starting a new one.\n private alreadyRunning: boolean;\n\n constructor() {\n super({\n name: \"magic-nix-cache\",\n fetchStyle: \"gh-env-style\",\n idsProjectName: \"magic-nix-cache-closure\",\n requireNix: \"warn\",\n diagnosticsSuffix: \"perf\",\n });\n\n this.hostAndPort = inputs.getString(\"listen\");\n this.diffStore = inputs.getBool(\"diff-store\");\n\n this.addFact(FACT_DIFF_STORE_ENABLED, this.diffStore);\n\n this.httpClient = got.extend({\n retry: {\n limit: 1,\n methods: [\"POST\", \"GET\", \"PUT\", \"HEAD\", \"DELETE\", \"OPTIONS\", \"TRACE\"],\n },\n hooks: {\n beforeRetry: [\n (error, retryCount) => {\n actionsCore.info(\n `Retrying after error ${error.code}, retry #: ${retryCount}`,\n );\n },\n ],\n },\n });\n\n this.daemonStarted = actionsCore.getState(STATE_STARTED) === STARTED_HINT;\n\n if (actionsCore.getState(STATE_DAEMONDIR) !== \"\") {\n this.daemonDir = actionsCore.getState(STATE_DAEMONDIR);\n } else {\n this.daemonDir = this.getTemporaryName();\n mkdirSync(this.daemonDir);\n actionsCore.saveState(STATE_DAEMONDIR, this.daemonDir);\n }\n\n if (process.env[ENV_DAEMON_DIR] === undefined) {\n this.alreadyRunning = false;\n actionsCore.exportVariable(ENV_DAEMON_DIR, this.daemonDir);\n } else {\n this.alreadyRunning = process.env[ENV_DAEMON_DIR] !== this.daemonDir;\n }\n this.addFact(FACT_ALREADY_RUNNING, this.alreadyRunning);\n\n this.stapleFile(\"daemon.log\", path.join(this.daemonDir, \"daemon.log\"));\n }\n\n async main(): Promise {\n if (this.alreadyRunning) {\n actionsCore.warning(TEXT_ALREADY_RUNNING);\n return;\n }\n\n if (this.getFeature(\"warn-magic-nix-cache-eol\")?.variant === true) {\n await warnOnMnc();\n }\n\n if (this.nixStoreTrust === \"untrusted\") {\n actionsCore.warning(TEXT_TRUST_UNTRUSTED);\n return;\n } else if (this.nixStoreTrust === \"unknown\") {\n actionsCore.info(TEXT_TRUST_UNKNOWN);\n }\n\n await this.setUpAutoCache();\n await this.notifyAutoCache();\n }\n\n async post(): Promise {\n // If strict mode is off and there was an error in main, such as the daemon not starting,\n // then the post phase is skipped with a warning.\n if (!this.strictMode && this.errorInMain) {\n actionsCore.warning(\n `skipping post phase due to error in main phase: ${this.errorInMain}`,\n );\n return;\n }\n\n if (this.alreadyRunning) {\n actionsCore.debug(TEXT_ALREADY_RUNNING);\n return;\n }\n\n if (this.nixStoreTrust === \"untrusted\") {\n actionsCore.debug(TEXT_TRUST_UNTRUSTED);\n return;\n } else if (this.nixStoreTrust === \"unknown\") {\n actionsCore.debug(TEXT_TRUST_UNKNOWN);\n }\n\n await this.tearDownAutoCache();\n }\n\n async setUpAutoCache(): Promise {\n const requiredEnv = [\n \"ACTIONS_CACHE_URL\",\n \"ACTIONS_RUNTIME_URL\",\n \"ACTIONS_RUNTIME_TOKEN\",\n ];\n\n let anyMissing = false;\n for (const n of requiredEnv) {\n if (!process.env.hasOwnProperty(n)) {\n anyMissing = true;\n actionsCore.warning(\n `Disabling automatic caching since required environment ${n} isn't available`,\n );\n }\n }\n\n this.addFact(FACT_ENV_VARS_PRESENT, !anyMissing);\n if (anyMissing) {\n return;\n }\n\n if (this.daemonStarted) {\n actionsCore.debug(\"Already started.\");\n return;\n }\n\n actionsCore.debug(\n `GitHub Action Cache URL: ${process.env[\"ACTIONS_CACHE_URL\"]}`,\n );\n\n const daemonBin = await this.unpackClosure(\"magic-nix-cache\");\n\n let runEnv;\n if (actionsCore.isDebug()) {\n runEnv = {\n RUST_LOG: \"debug,magic_nix_cache=trace,gha_cache=trace\",\n RUST_BACKTRACE: \"full\",\n ...process.env,\n };\n } else {\n runEnv = process.env;\n }\n\n const notifyPort = inputs.getString(\"startup-notification-port\");\n\n const notifyPromise = new Promise>((resolveListening) => {\n const promise = new Promise(async (resolveQuit) => {\n const notifyServer = http.createServer((req, res) => {\n if (req.method === \"POST\" && req.url === \"/\") {\n actionsCore.debug(`Notify server shutting down.`);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\"{}\");\n notifyServer.close(() => {\n resolveQuit();\n });\n }\n });\n\n notifyServer.listen(notifyPort, () => {\n actionsCore.debug(`Notify server running.`);\n resolveListening(promise);\n });\n });\n });\n\n // Start tailing the daemon log.\n const outputPath = `${this.daemonDir}/daemon.log`;\n const output = openSync(outputPath, \"a\");\n const log = tailLog(this.daemonDir);\n const netrc = await netrcPath();\n const nixConfPath = `${process.env[\"HOME\"]}/.config/nix/nix.conf`;\n const upstreamCache = inputs.getString(\"upstream-cache\");\n const useFlakeHub = getTrinaryInput(\"use-flakehub\");\n const flakeHubCacheServer = inputs.getString(\"flakehub-cache-server\");\n const flakeHubApiServer = inputs.getString(\"flakehub-api-server\");\n const flakeHubFlakeName = inputs.getString(\"flakehub-flake-name\");\n const useGhaCache = getTrinaryInput(\"use-gha-cache\");\n\n const daemonCliFlags: string[] = [\n \"--startup-notification-url\",\n `http://127.0.0.1:${notifyPort}`,\n \"--listen\",\n this.hostAndPort,\n \"--upstream\",\n upstreamCache,\n \"--diagnostic-endpoint\",\n (await this.getDiagnosticsUrl())?.toString() ?? \"\",\n \"--nix-conf\",\n nixConfPath,\n \"--use-gha-cache\",\n useGhaCache,\n \"--use-flakehub\",\n useFlakeHub,\n ]\n .concat(this.diffStore ? [\"--diff-store\"] : [])\n .concat(\n useFlakeHub !== \"disabled\"\n ? [\n \"--flakehub-cache-server\",\n flakeHubCacheServer,\n \"--flakehub-api-server\",\n flakeHubApiServer,\n \"--flakehub-api-server-netrc\",\n netrc,\n \"--flakehub-flake-name\",\n flakeHubFlakeName,\n ]\n : [],\n );\n\n const opts: SpawnOptions = {\n stdio: [\"ignore\", output, output],\n env: runEnv,\n detached: true,\n };\n\n // Display the final command for debugging purposes\n actionsCore.debug(\"Full daemon start command:\");\n actionsCore.debug(`${daemonBin} ${daemonCliFlags.join(\" \")}`);\n\n // Start the server. Once it is ready, it will notify us via the notification server.\n const daemon = spawn(daemonBin, daemonCliFlags, opts);\n\n this.daemonStarted = true;\n actionsCore.saveState(STATE_STARTED, STARTED_HINT);\n\n const pidFile = path.join(this.daemonDir, \"daemon.pid\");\n await fs.writeFile(pidFile, `${daemon.pid}`);\n\n actionsCore.info(\"Waiting for magic-nix-cache to start...\");\n\n await new Promise((resolve) => {\n notifyPromise\n // eslint-disable-next-line github/no-then\n .then((_value) => {\n resolve();\n })\n // eslint-disable-next-line github/no-then\n .catch((e: unknown) => {\n this.exitMain(`Error in notifyPromise: ${stringifyError(e)}`);\n });\n\n daemon.on(\"exit\", async (code, signal) => {\n let msg: string;\n if (signal) {\n msg = `Daemon was killed by signal ${signal}`;\n } else if (code) {\n msg = `Daemon exited with code ${code}`;\n } else {\n msg = \"Daemon unexpectedly exited\";\n }\n\n this.exitMain(msg);\n });\n });\n\n daemon.unref();\n\n actionsCore.info(\"Launched Magic Nix Cache\");\n\n log.unwatch();\n }\n\n private async notifyAutoCache(): Promise {\n if (!this.daemonStarted) {\n actionsCore.debug(\"magic-nix-cache not started - Skipping\");\n return;\n }\n\n try {\n actionsCore.debug(`Indicating workflow start`);\n const res: Response = await this.httpClient.post(\n `http://${this.hostAndPort}/api/workflow-start`,\n );\n\n actionsCore.debug(\n `Response from POST to /api/workflow-start: (status: ${res.statusCode}, body: ${res.body})`,\n );\n\n if (res.statusCode !== 200) {\n throw new Error(\n `Failed to trigger workflow start hook; expected status 200 but got (status: ${res.statusCode}, body: ${res.body})`,\n );\n }\n\n actionsCore.debug(`back from post: ${res.body}`);\n } catch (e: unknown) {\n this.exitMain(`Error starting the Magic Nix Cache: ${stringifyError(e)}`);\n }\n }\n\n async tearDownAutoCache(): Promise {\n if (!this.daemonStarted) {\n actionsCore.debug(\"magic-nix-cache not started - Skipping\");\n return;\n }\n\n const pidFile = path.join(this.daemonDir, \"daemon.pid\");\n const pid = parseInt(await fs.readFile(pidFile, { encoding: \"ascii\" }));\n actionsCore.debug(`found daemon pid: ${pid}`);\n if (!pid) {\n throw new Error(\"magic-nix-cache did not start successfully\");\n }\n\n const log = tailLog(this.daemonDir);\n\n try {\n actionsCore.debug(`about to post to localhost`);\n const res: Response = await this.httpClient.post(\n `http://${this.hostAndPort}/api/workflow-finish`,\n );\n\n actionsCore.debug(\n `Response from POST to /api/workflow-finish: (status: ${res.statusCode}, body: ${res.body})`,\n );\n\n if (res.statusCode !== 200) {\n throw new Error(\n `Failed to trigger workflow finish hook; expected status 200 but got (status: ${res.statusCode}, body: ${res.body})`,\n );\n }\n } finally {\n actionsCore.debug(`unwatching the daemon log`);\n log.unwatch();\n }\n\n actionsCore.debug(`killing daemon process ${pid}`);\n\n try {\n // Repeatedly signal 0 the daemon to test if it is up.\n // If it exits, kill will raise an exception which breaks us out of this control flow and skips the sigterm.\n // If magic-nix-cache doesn't exit in 30s, we SIGTERM it.\n for (let i = 0; i < 30 * 10; i++) {\n process.kill(pid, 0);\n await setTimeout(100);\n }\n\n this.addFact(FACT_SENT_SIGTERM, true);\n actionsCore.info(`Sending Magic Nix Cache a SIGTERM`);\n process.kill(pid, \"SIGTERM\");\n } catch {\n // Perfectly normal to get an exception here, because the process shut down.\n }\n\n if (actionsCore.isDebug()) {\n actionsCore.info(\"Entire log:\");\n const entireLog = readFileSync(path.join(this.daemonDir, \"daemon.log\"));\n actionsCore.info(entireLog.toString());\n }\n }\n\n // Exit the workflow during the main phase. If strict mode is set, fail; if not, save the error\n // message to the workflow's state and exit successfully.\n private exitMain(msg: string): void {\n if (this.strictMode) {\n actionsCore.setFailed(msg);\n } else {\n actionsCore.saveState(STATE_ERROR_IN_MAIN, msg);\n process.exit(0);\n }\n }\n\n // If the main phase threw an error (not in strict mode), this will be a non-empty\n // string available in the post phase.\n private get errorInMain(): string | undefined {\n const state = actionsCore.getState(STATE_ERROR_IN_MAIN);\n return state !== \"\" ? state : undefined;\n }\n}\n\nfunction main(): void {\n new MagicNixCacheAction().execute();\n}\n\nmain();\n"],"mappings":"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","names":["actionsCore","fs","actionsCore","stringifyError","fs","path"]} -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "locked": { 5 | "narHash": "sha256-AAQ/2sD+0D18bb8hKuEEVpHUYD1GmO2Uh/taFamn6XQ=", 6 | "rev": "4f910c9827911b1ec2bf26b5a062cd09f8d89f85", 7 | "revCount": 55, 8 | "type": "tarball", 9 | "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.0/018af168-abf6-7903-8e2c-be5db8d27950/source.tar.gz" 10 | }, 11 | "original": { 12 | "type": "tarball", 13 | "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" 14 | } 15 | }, 16 | "nixpkgs": { 17 | "locked": { 18 | "narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=", 19 | "rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a", 20 | "revCount": 530560, 21 | "type": "tarball", 22 | "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.530560%2Brev-f5892ddac112a1e9b3612c39af1b72987ee5783a/018aec4d-58df-7d2d-a74e-b1bc82c4654f/source.tar.gz" 23 | }, 24 | "original": { 25 | "type": "tarball", 26 | "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz" 27 | } 28 | }, 29 | "root": { 30 | "inputs": { 31 | "flake-compat": "flake-compat", 32 | "nixpkgs": "nixpkgs" 33 | } 34 | } 35 | }, 36 | "root": "root", 37 | "version": 7 38 | } 39 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Magic Nix Cache"; 3 | 4 | inputs = { 5 | nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz"; 6 | 7 | flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, ... }: 11 | let 12 | supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 13 | forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f { 14 | pkgs = import nixpkgs { inherit system; }; 15 | }); 16 | in 17 | { 18 | devShells = forAllSystems ({ pkgs }: { 19 | default = pkgs.mkShell { 20 | packages = with pkgs; [ 21 | jq 22 | shellcheck 23 | nodejs_latest 24 | nixpkgs-fmt 25 | nodePackages_latest.pnpm 26 | nodePackages_latest.typescript-language-server 27 | ]; 28 | }; 29 | }); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-nix-cache-action", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "type": "module", 8 | "scripts": { 9 | "build": "tsup", 10 | "format": "prettier --write .", 11 | "check-fmt": "prettier --check .", 12 | "lint": "eslint src/**/*.ts", 13 | "package": "ncc build", 14 | "all": "pnpm run format && pnpm run lint && pnpm run build && pnpm run package" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/DeterminateSystems/magic-nix-cache-action.git" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "LGPL", 23 | "bugs": { 24 | "url": "https://github.com/DeterminateSystems/magic-nix-cache-action/issues" 25 | }, 26 | "homepage": "https://github.com/DeterminateSystems/magic-nix-cache-action#readme", 27 | "dependencies": { 28 | "@actions/core": "^1.11.1", 29 | "@actions/exec": "^1.1.1", 30 | "@actions/glob": "^0.5.0", 31 | "detsys-ts": "github:DeterminateSystems/detsys-ts", 32 | "got": "^14.4.7", 33 | "tail": "^2.2.6" 34 | }, 35 | "devDependencies": { 36 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 37 | "@types/node": "^20.17.57", 38 | "@types/tail": "^2.2.3", 39 | "@types/uuid": "^9.0.8", 40 | "@typescript-eslint/eslint-plugin": "^7.18.0", 41 | "@vercel/ncc": "^0.38.3", 42 | "eslint": "^8.57.1", 43 | "eslint-import-resolver-typescript": "^3.10.1", 44 | "eslint-plugin-github": "^4.10.2", 45 | "eslint-plugin-import": "^2.31.0", 46 | "eslint-plugin-prettier": "^5.4.1", 47 | "prettier": "^3.5.3", 48 | "tsup": "^8.5.0", 49 | "typescript": "^5.8.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | plugins: [require.resolve("@trivago/prettier-plugin-sort-imports")], 4 | semi: true, 5 | singleQuote: false, 6 | tabWidth: 2, 7 | trailingComma: "all", 8 | useTabs: false, 9 | // Import sorting 10 | importOrderSeparation: true, 11 | importOrderSortSpecifiers: true, 12 | }; 13 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 | fetchTarball { 5 | url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; } 10 | ).shellNix -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as actionsCore from "@actions/core"; 2 | import * as fs from "node:fs/promises"; 3 | import * as os from "node:os"; 4 | import path from "node:path"; 5 | import { Tail } from "tail"; 6 | 7 | export function getTrinaryInput( 8 | name: string, 9 | ): "enabled" | "disabled" | "no-preference" { 10 | const trueValue = ["true", "True", "TRUE", "enabled"]; 11 | const falseValue = ["false", "False", "FALSE", "disabled"]; 12 | const noPreferenceValue = ["", "null", "no-preference"]; 13 | 14 | const val = actionsCore.getInput(name); 15 | if (trueValue.includes(val)) { 16 | return "enabled"; 17 | } 18 | if (falseValue.includes(val)) { 19 | return "disabled"; 20 | } 21 | if (noPreferenceValue.includes(val)) { 22 | return "no-preference"; 23 | } 24 | 25 | const possibleValues = trueValue 26 | .concat(falseValue) 27 | .concat(noPreferenceValue) 28 | .join(" | "); 29 | throw new TypeError( 30 | `Input ${name} does not look like a trinary, which requires one of:\n${possibleValues}`, 31 | ); 32 | } 33 | 34 | export function tailLog(daemonDir: string): Tail { 35 | const log = new Tail(path.join(daemonDir, "daemon.log")); 36 | actionsCore.debug(`tailing daemon.log...`); 37 | log.on("line", (line) => { 38 | actionsCore.info(line); 39 | }); 40 | return log; 41 | } 42 | 43 | export async function netrcPath(): Promise { 44 | const expectedNetrcPath = path.join( 45 | process.env["RUNNER_TEMP"] ?? os.tmpdir(), 46 | "determinate-nix-installer-netrc", 47 | ); 48 | try { 49 | await fs.access(expectedNetrcPath); 50 | return expectedNetrcPath; 51 | } catch { 52 | // `nix-installer` was not used, the user may be registered with FlakeHub though. 53 | const destinedNetrcPath = path.join( 54 | process.env["RUNNER_TEMP"] ?? os.tmpdir(), 55 | "magic-nix-cache-netrc", 56 | ); 57 | try { 58 | await flakeHubLogin(destinedNetrcPath); 59 | } catch (e) { 60 | actionsCore.info( 61 | "FlakeHub Cache is disabled due to missing or invalid token", 62 | ); 63 | actionsCore.info( 64 | `If you're signed up for FlakeHub Cache, make sure that your Actions config has a \`permissions\` block with \`id-token\` set to "write" and \`contents\` set to "read"`, 65 | ); 66 | actionsCore.debug(`Error while logging into FlakeHub: ${e}`); 67 | } 68 | return destinedNetrcPath; 69 | } 70 | } 71 | 72 | async function flakeHubLogin(netrc: string): Promise { 73 | const jwt = await actionsCore.getIDToken("api.flakehub.com"); 74 | 75 | await fs.writeFile( 76 | netrc, 77 | [ 78 | `machine api.flakehub.com login flakehub password ${jwt}`, 79 | `machine flakehub.com login flakehub password ${jwt}`, 80 | `machine cache.flakehub.com login flakehub password ${jwt}`, 81 | ].join("\n"), 82 | ); 83 | 84 | actionsCore.info("Logged in to FlakeHub."); 85 | } 86 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getTrinaryInput, netrcPath, tailLog } from "./helpers.js"; 2 | import { warnOnMnc } from "./mnc-warn.js"; 3 | import * as actionsCore from "@actions/core"; 4 | import { DetSysAction, inputs, stringifyError } from "detsys-ts"; 5 | import got, { Got, Response } from "got"; 6 | import * as http from "http"; 7 | import { SpawnOptions, spawn } from "node:child_process"; 8 | import { mkdirSync, openSync, readFileSync } from "node:fs"; 9 | import * as fs from "node:fs/promises"; 10 | import * as path from "node:path"; 11 | import { setTimeout } from "node:timers/promises"; 12 | 13 | // The ENV_DAEMON_DIR is intended to determine if we "own" the daemon or not, 14 | // in the case that a user has put the magic nix cache into their workflow 15 | // twice. 16 | const ENV_DAEMON_DIR = "MAGIC_NIX_CACHE_DAEMONDIR"; 17 | 18 | const FACT_ENV_VARS_PRESENT = "required_env_vars_present"; 19 | const FACT_SENT_SIGTERM = "sent_sigterm"; 20 | const FACT_DIFF_STORE_ENABLED = "diff_store"; 21 | const FACT_ALREADY_RUNNING = "noop_mode"; 22 | 23 | const STATE_DAEMONDIR = "MAGIC_NIX_CACHE_DAEMONDIR"; 24 | const STATE_ERROR_IN_MAIN = "ERROR_IN_MAIN"; 25 | const STATE_STARTED = "MAGIC_NIX_CACHE_STARTED"; 26 | const STARTED_HINT = "true"; 27 | 28 | const TEXT_ALREADY_RUNNING = 29 | "Magic Nix Cache is already running, this workflow job is in noop mode. Is the Magic Nix Cache in the workflow twice?"; 30 | const TEXT_TRUST_UNTRUSTED = 31 | "The Nix daemon does not consider the user running this workflow to be trusted. Magic Nix Cache is disabled."; 32 | const TEXT_TRUST_UNKNOWN = 33 | "The Nix daemon may not consider the user running this workflow to be trusted. Magic Nix Cache may not start correctly."; 34 | 35 | class MagicNixCacheAction extends DetSysAction { 36 | private hostAndPort: string; 37 | private diffStore: boolean; 38 | private httpClient: Got; 39 | private daemonDir: string; 40 | private daemonStarted: boolean; 41 | 42 | // This is set to `true` if the MNC is already running, in which case the 43 | // workflow will use the existing process rather than starting a new one. 44 | private alreadyRunning: boolean; 45 | 46 | constructor() { 47 | super({ 48 | name: "magic-nix-cache", 49 | fetchStyle: "gh-env-style", 50 | idsProjectName: "magic-nix-cache-closure", 51 | requireNix: "warn", 52 | diagnosticsSuffix: "perf", 53 | }); 54 | 55 | this.hostAndPort = inputs.getString("listen"); 56 | this.diffStore = inputs.getBool("diff-store"); 57 | 58 | this.addFact(FACT_DIFF_STORE_ENABLED, this.diffStore); 59 | 60 | this.httpClient = got.extend({ 61 | retry: { 62 | limit: 1, 63 | methods: ["POST", "GET", "PUT", "HEAD", "DELETE", "OPTIONS", "TRACE"], 64 | }, 65 | hooks: { 66 | beforeRetry: [ 67 | (error, retryCount) => { 68 | actionsCore.info( 69 | `Retrying after error ${error.code}, retry #: ${retryCount}`, 70 | ); 71 | }, 72 | ], 73 | }, 74 | }); 75 | 76 | this.daemonStarted = actionsCore.getState(STATE_STARTED) === STARTED_HINT; 77 | 78 | if (actionsCore.getState(STATE_DAEMONDIR) !== "") { 79 | this.daemonDir = actionsCore.getState(STATE_DAEMONDIR); 80 | } else { 81 | this.daemonDir = this.getTemporaryName(); 82 | mkdirSync(this.daemonDir); 83 | actionsCore.saveState(STATE_DAEMONDIR, this.daemonDir); 84 | } 85 | 86 | if (process.env[ENV_DAEMON_DIR] === undefined) { 87 | this.alreadyRunning = false; 88 | actionsCore.exportVariable(ENV_DAEMON_DIR, this.daemonDir); 89 | } else { 90 | this.alreadyRunning = process.env[ENV_DAEMON_DIR] !== this.daemonDir; 91 | } 92 | this.addFact(FACT_ALREADY_RUNNING, this.alreadyRunning); 93 | 94 | this.stapleFile("daemon.log", path.join(this.daemonDir, "daemon.log")); 95 | } 96 | 97 | async main(): Promise { 98 | if (this.alreadyRunning) { 99 | actionsCore.warning(TEXT_ALREADY_RUNNING); 100 | return; 101 | } 102 | 103 | if (this.getFeature("warn-magic-nix-cache-eol")?.variant === true) { 104 | await warnOnMnc(); 105 | } 106 | 107 | if (this.nixStoreTrust === "untrusted") { 108 | actionsCore.warning(TEXT_TRUST_UNTRUSTED); 109 | return; 110 | } else if (this.nixStoreTrust === "unknown") { 111 | actionsCore.info(TEXT_TRUST_UNKNOWN); 112 | } 113 | 114 | await this.setUpAutoCache(); 115 | await this.notifyAutoCache(); 116 | } 117 | 118 | async post(): Promise { 119 | // If strict mode is off and there was an error in main, such as the daemon not starting, 120 | // then the post phase is skipped with a warning. 121 | if (!this.strictMode && this.errorInMain) { 122 | actionsCore.warning( 123 | `skipping post phase due to error in main phase: ${this.errorInMain}`, 124 | ); 125 | return; 126 | } 127 | 128 | if (this.alreadyRunning) { 129 | actionsCore.debug(TEXT_ALREADY_RUNNING); 130 | return; 131 | } 132 | 133 | if (this.nixStoreTrust === "untrusted") { 134 | actionsCore.debug(TEXT_TRUST_UNTRUSTED); 135 | return; 136 | } else if (this.nixStoreTrust === "unknown") { 137 | actionsCore.debug(TEXT_TRUST_UNKNOWN); 138 | } 139 | 140 | await this.tearDownAutoCache(); 141 | } 142 | 143 | async setUpAutoCache(): Promise { 144 | const requiredEnv = [ 145 | "ACTIONS_CACHE_URL", 146 | "ACTIONS_RUNTIME_URL", 147 | "ACTIONS_RUNTIME_TOKEN", 148 | ]; 149 | 150 | let anyMissing = false; 151 | for (const n of requiredEnv) { 152 | if (!process.env.hasOwnProperty(n)) { 153 | anyMissing = true; 154 | actionsCore.warning( 155 | `Disabling automatic caching since required environment ${n} isn't available`, 156 | ); 157 | } 158 | } 159 | 160 | this.addFact(FACT_ENV_VARS_PRESENT, !anyMissing); 161 | if (anyMissing) { 162 | return; 163 | } 164 | 165 | if (this.daemonStarted) { 166 | actionsCore.debug("Already started."); 167 | return; 168 | } 169 | 170 | actionsCore.debug( 171 | `GitHub Action Cache URL: ${process.env["ACTIONS_CACHE_URL"]}`, 172 | ); 173 | 174 | const daemonBin = await this.unpackClosure("magic-nix-cache"); 175 | 176 | let runEnv; 177 | if (actionsCore.isDebug()) { 178 | runEnv = { 179 | RUST_LOG: "debug,magic_nix_cache=trace,gha_cache=trace", 180 | RUST_BACKTRACE: "full", 181 | ...process.env, 182 | }; 183 | } else { 184 | runEnv = process.env; 185 | } 186 | 187 | const notifyPort = inputs.getString("startup-notification-port"); 188 | 189 | const notifyPromise = new Promise>((resolveListening) => { 190 | const promise = new Promise(async (resolveQuit) => { 191 | const notifyServer = http.createServer((req, res) => { 192 | if (req.method === "POST" && req.url === "/") { 193 | actionsCore.debug(`Notify server shutting down.`); 194 | res.writeHead(200, { "Content-Type": "application/json" }); 195 | res.end("{}"); 196 | notifyServer.close(() => { 197 | resolveQuit(); 198 | }); 199 | } 200 | }); 201 | 202 | notifyServer.listen(notifyPort, () => { 203 | actionsCore.debug(`Notify server running.`); 204 | resolveListening(promise); 205 | }); 206 | }); 207 | }); 208 | 209 | // Start tailing the daemon log. 210 | const outputPath = `${this.daemonDir}/daemon.log`; 211 | const output = openSync(outputPath, "a"); 212 | const log = tailLog(this.daemonDir); 213 | const netrc = await netrcPath(); 214 | const nixConfPath = `${process.env["HOME"]}/.config/nix/nix.conf`; 215 | const upstreamCache = inputs.getString("upstream-cache"); 216 | const useFlakeHub = getTrinaryInput("use-flakehub"); 217 | const flakeHubCacheServer = inputs.getString("flakehub-cache-server"); 218 | const flakeHubApiServer = inputs.getString("flakehub-api-server"); 219 | const flakeHubFlakeName = inputs.getString("flakehub-flake-name"); 220 | const useGhaCache = getTrinaryInput("use-gha-cache"); 221 | 222 | const daemonCliFlags: string[] = [ 223 | "--startup-notification-url", 224 | `http://127.0.0.1:${notifyPort}`, 225 | "--listen", 226 | this.hostAndPort, 227 | "--upstream", 228 | upstreamCache, 229 | "--diagnostic-endpoint", 230 | (await this.getDiagnosticsUrl())?.toString() ?? "", 231 | "--nix-conf", 232 | nixConfPath, 233 | "--use-gha-cache", 234 | useGhaCache, 235 | "--use-flakehub", 236 | useFlakeHub, 237 | ] 238 | .concat(this.diffStore ? ["--diff-store"] : []) 239 | .concat( 240 | useFlakeHub !== "disabled" 241 | ? [ 242 | "--flakehub-cache-server", 243 | flakeHubCacheServer, 244 | "--flakehub-api-server", 245 | flakeHubApiServer, 246 | "--flakehub-api-server-netrc", 247 | netrc, 248 | "--flakehub-flake-name", 249 | flakeHubFlakeName, 250 | ] 251 | : [], 252 | ); 253 | 254 | const opts: SpawnOptions = { 255 | stdio: ["ignore", output, output], 256 | env: runEnv, 257 | detached: true, 258 | }; 259 | 260 | // Display the final command for debugging purposes 261 | actionsCore.debug("Full daemon start command:"); 262 | actionsCore.debug(`${daemonBin} ${daemonCliFlags.join(" ")}`); 263 | 264 | // Start the server. Once it is ready, it will notify us via the notification server. 265 | const daemon = spawn(daemonBin, daemonCliFlags, opts); 266 | 267 | this.daemonStarted = true; 268 | actionsCore.saveState(STATE_STARTED, STARTED_HINT); 269 | 270 | const pidFile = path.join(this.daemonDir, "daemon.pid"); 271 | await fs.writeFile(pidFile, `${daemon.pid}`); 272 | 273 | actionsCore.info("Waiting for magic-nix-cache to start..."); 274 | 275 | await new Promise((resolve) => { 276 | notifyPromise 277 | // eslint-disable-next-line github/no-then 278 | .then((_value) => { 279 | resolve(); 280 | }) 281 | // eslint-disable-next-line github/no-then 282 | .catch((e: unknown) => { 283 | this.exitMain(`Error in notifyPromise: ${stringifyError(e)}`); 284 | }); 285 | 286 | daemon.on("exit", async (code, signal) => { 287 | let msg: string; 288 | if (signal) { 289 | msg = `Daemon was killed by signal ${signal}`; 290 | } else if (code) { 291 | msg = `Daemon exited with code ${code}`; 292 | } else { 293 | msg = "Daemon unexpectedly exited"; 294 | } 295 | 296 | this.exitMain(msg); 297 | }); 298 | }); 299 | 300 | daemon.unref(); 301 | 302 | actionsCore.info("Launched Magic Nix Cache"); 303 | 304 | log.unwatch(); 305 | } 306 | 307 | private async notifyAutoCache(): Promise { 308 | if (!this.daemonStarted) { 309 | actionsCore.debug("magic-nix-cache not started - Skipping"); 310 | return; 311 | } 312 | 313 | try { 314 | actionsCore.debug(`Indicating workflow start`); 315 | const res: Response = await this.httpClient.post( 316 | `http://${this.hostAndPort}/api/workflow-start`, 317 | ); 318 | 319 | actionsCore.debug( 320 | `Response from POST to /api/workflow-start: (status: ${res.statusCode}, body: ${res.body})`, 321 | ); 322 | 323 | if (res.statusCode !== 200) { 324 | throw new Error( 325 | `Failed to trigger workflow start hook; expected status 200 but got (status: ${res.statusCode}, body: ${res.body})`, 326 | ); 327 | } 328 | 329 | actionsCore.debug(`back from post: ${res.body}`); 330 | } catch (e: unknown) { 331 | this.exitMain(`Error starting the Magic Nix Cache: ${stringifyError(e)}`); 332 | } 333 | } 334 | 335 | async tearDownAutoCache(): Promise { 336 | if (!this.daemonStarted) { 337 | actionsCore.debug("magic-nix-cache not started - Skipping"); 338 | return; 339 | } 340 | 341 | const pidFile = path.join(this.daemonDir, "daemon.pid"); 342 | const pid = parseInt(await fs.readFile(pidFile, { encoding: "ascii" })); 343 | actionsCore.debug(`found daemon pid: ${pid}`); 344 | if (!pid) { 345 | throw new Error("magic-nix-cache did not start successfully"); 346 | } 347 | 348 | const log = tailLog(this.daemonDir); 349 | 350 | try { 351 | actionsCore.debug(`about to post to localhost`); 352 | const res: Response = await this.httpClient.post( 353 | `http://${this.hostAndPort}/api/workflow-finish`, 354 | ); 355 | 356 | actionsCore.debug( 357 | `Response from POST to /api/workflow-finish: (status: ${res.statusCode}, body: ${res.body})`, 358 | ); 359 | 360 | if (res.statusCode !== 200) { 361 | throw new Error( 362 | `Failed to trigger workflow finish hook; expected status 200 but got (status: ${res.statusCode}, body: ${res.body})`, 363 | ); 364 | } 365 | } finally { 366 | actionsCore.debug(`unwatching the daemon log`); 367 | log.unwatch(); 368 | } 369 | 370 | actionsCore.debug(`killing daemon process ${pid}`); 371 | 372 | try { 373 | // Repeatedly signal 0 the daemon to test if it is up. 374 | // If it exits, kill will raise an exception which breaks us out of this control flow and skips the sigterm. 375 | // If magic-nix-cache doesn't exit in 30s, we SIGTERM it. 376 | for (let i = 0; i < 30 * 10; i++) { 377 | process.kill(pid, 0); 378 | await setTimeout(100); 379 | } 380 | 381 | this.addFact(FACT_SENT_SIGTERM, true); 382 | actionsCore.info(`Sending Magic Nix Cache a SIGTERM`); 383 | process.kill(pid, "SIGTERM"); 384 | } catch { 385 | // Perfectly normal to get an exception here, because the process shut down. 386 | } 387 | 388 | if (actionsCore.isDebug()) { 389 | actionsCore.info("Entire log:"); 390 | const entireLog = readFileSync(path.join(this.daemonDir, "daemon.log")); 391 | actionsCore.info(entireLog.toString()); 392 | } 393 | } 394 | 395 | // Exit the workflow during the main phase. If strict mode is set, fail; if not, save the error 396 | // message to the workflow's state and exit successfully. 397 | private exitMain(msg: string): void { 398 | if (this.strictMode) { 399 | actionsCore.setFailed(msg); 400 | } else { 401 | actionsCore.saveState(STATE_ERROR_IN_MAIN, msg); 402 | process.exit(0); 403 | } 404 | } 405 | 406 | // If the main phase threw an error (not in strict mode), this will be a non-empty 407 | // string available in the post phase. 408 | private get errorInMain(): string | undefined { 409 | const state = actionsCore.getState(STATE_ERROR_IN_MAIN); 410 | return state !== "" ? state : undefined; 411 | } 412 | } 413 | 414 | function main(): void { 415 | new MagicNixCacheAction().execute(); 416 | } 417 | 418 | main(); 419 | -------------------------------------------------------------------------------- /src/mnc-warn.ts: -------------------------------------------------------------------------------- 1 | import actionsCore from "@actions/core"; 2 | import glob from "@actions/glob"; 3 | import { stringifyError } from "detsys-ts"; 4 | import * as fs from "node:fs/promises"; 5 | 6 | export async function warnOnMnc(): Promise { 7 | const cwd = process.cwd(); 8 | 9 | const patterns = [ 10 | ".github/actions/*.yaml", 11 | ".github/actions/*.yml", 12 | ".github/workflows/*.yaml", 13 | ".github/workflows/*.yml", 14 | ]; 15 | const globber = await glob.create(patterns.join("\n")); 16 | const files = await globber.glob(); 17 | 18 | for (const file of files) { 19 | try { 20 | const relativeFilePath = file.replace(cwd, "."); 21 | const lines = (await fs.readFile(file, { encoding: "utf8" })).split("\n"); 22 | 23 | for (const lineIdx of lines.keys()) { 24 | const line = lines[lineIdx]; 25 | 26 | if ( 27 | line 28 | .toLowerCase() 29 | .includes("determinatesystems/magic-nix-cache-action") 30 | ) { 31 | const actionPosition = line 32 | .toLowerCase() 33 | .indexOf("determinatesystems/magic-nix-cache-action"); 34 | const actionPositionEnd = 35 | actionPosition + "determinatesystems/magic-nix-cache-action".length; 36 | 37 | const newLine = line.replace( 38 | /DeterminateSystems\/magic-nix-cache-action/gi, 39 | "DeterminateSystems/flakehub-cache-action", 40 | ); 41 | actionsCore.warning( 42 | [ 43 | "Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025.", 44 | "To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead.", 45 | "", 46 | "Replace...", 47 | line, 48 | "", 49 | "...with...", 50 | newLine, 51 | "", 52 | "For more details: https://dtr.mn/magic-nix-cache-eol", 53 | ].join("\n"), 54 | { 55 | title: "Magic Nix Cache is deprecated", 56 | file: relativeFilePath, 57 | startLine: lineIdx + 1, 58 | startColumn: actionPosition, 59 | endColumn: actionPositionEnd, 60 | }, 61 | ); 62 | } 63 | } 64 | } catch (err) { 65 | actionsCore.debug(stringifyError(err)); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 4 | "module": "Node16", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 11 | "resolveJsonModule": true, 12 | "declaration": true 13 | }, 14 | "exclude": ["node_modules", "**/*.test.ts", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | name: "detsys-ts", 5 | entry: ["src/index.ts"], 6 | format: ["esm"], 7 | target: "node20", 8 | bundle: true, 9 | splitting: false, 10 | sourcemap: true, 11 | clean: true, 12 | dts: { 13 | resolve: true, 14 | }, 15 | }); 16 | --------------------------------------------------------------------------------