Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,100 @@ jobs:
exit 1
fi

test-repeat-run:
name: "Test repeated runs on one machine"
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
os:
- "ubuntu-latest"
- "ubuntu-slim"
- "macos-latest"

steps:
- name: "Checkout"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6


- name: "Install Flox (first run)"
uses: "./"
with:
disable-metrics: "true"

# Hosted runners start each job on a fresh VM, so reaching the state a
# self-hosted runner is in after one job means recreating it by hand: the
# token this action recorded has expired, and a runner that has been
# through an older version also carries a block written into nix.conf.
- name: "Age the recorded configuration"
run: |
sudo sed -i.bak \
's|^access-tokens = .*|access-tokens = github.com=ghs_expired000000000000000000000000000|' \
"$(sudo ls /etc/nix/install-flox-action-*.conf)"
printf '\n# Added by install-flox-action\naccess-tokens = github.com=ghs_legacy00000000000000000000000000\n' \
| sudo tee -a /etc/nix/nix.conf >/dev/null

- name: "Install Flox (second run)"
id: second
uses: "./"
with:
disable-metrics: "true"

# Each invocation writes its own file, and nix.conf reads the last
# include last, so the winning file is the one the second run wrote.
- name: "Verify the token in the winning file was refreshed"
env:
EXPECTED: "access-tokens = github.com=${{ github.token }}"
run: |
winner="$(sudo grep -oE '^!include install-flox-action-\S+\.conf' /etc/nix/nix.conf \
| tail -1 | awk '{print $2}')"
echo "winning file: $winner"
sudo grep -qF "$EXPECTED" "/etc/nix/$winner"
! sudo grep -q 'ghs_expired' "/etc/nix/$winner"

- name: "Verify the legacy block was removed"
run: |
! sudo grep -q 'ghs_legacy' /etc/nix/nix.conf
! sudo grep -q '# Added by install-flox-action' /etc/nix/nix.conf

- name: "Verify every include line points at a file that exists"
run: |
sudo grep -oE '^!include install-flox-action-\S+\.conf' /etc/nix/nix.conf \
| awk '{print $2}' | while read -r name; do
sudo test -f "/etc/nix/$name" \
|| { echo "dangling include: $name"; exit 1; }
done

- name: "Verify no include line is duplicated"
run: |
dupes="$(sudo grep -oE '^!include install-flox-action-\S+\.conf' /etc/nix/nix.conf \
| sort | uniq -d)"
if [[ -n "$dupes" ]]; then
echo "duplicated include lines:"; echo "$dupes"
sudo cat /etc/nix/nix.conf
exit 1
fi

- name: "Verify the second run skipped installation"
run: |
if [[ "${{ steps.second.outputs.flox-preinstalled }}" != "true" ]]; then
echo "Expected flox-preinstalled to be 'true'"
exit 1
fi

- name: "Test: flox"
run: |
flox --version
cd $(mktemp -d)
flox init
flox install hello
flox activate -- hello

- name: "Test: nix can still read its configuration"
run: nix --extra-experimental-features nix-command config show >/dev/null

test-act:
name: "Test with nektos/act"
runs-on: "ubuntu-latest"
Expand Down Expand Up @@ -242,6 +336,7 @@ jobs:
- "test-action"
- "test-new-inputs"
- "test-existing-nix"
- "test-repeat-run"
- "test-act"

steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ jobs:
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7

with:
file_pattern: "dist/index.js badges/coverage.svg"
file_pattern: "dist/ badges/coverage.svg"
commit_message: "chore(deps): Update dist/"
32 changes: 27 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,42 @@ npm run package # Bundle with ncc to dist/

## Architecture

The action is a JavaScript GitHub Action (node20 runtime) that:
The action is a JavaScript GitHub Action (node24 runtime) that:

1. **Entry**: `src/index.js` → calls `main.run()`
1. **Entry**: `src/index.js`, which runs `main.run()` or, in the job's post step, `cleanup.run()`
2. **Core logic**: `src/main.js`
- `getDownloadUrl()` - Determines platform-specific download URL based on OS, arch, and package manager (dpkg vs rpm)
- `run()` - Main entry: checks for existing Nix (fails if found), then downloads/installs flox
3. **Installation**: `scripts/install-flox.sh` - Bash script handling download (curl with retries) and platform-specific installation (rpm/dpkg/installer)
- `run()` - Main entry: decides how flox gets installed, configures Nix and flox, emits outputs
3. **Nix config**: `src/nixconf.js` - Paths, the legacy-block stripper, and the root-owned reads and writes shared with the post step
4. **Post step**: `src/cleanup.js` - Removes the config file holding the job's token
5. **Installation**: `scripts/install-flox.sh` - Bash script handling download (curl with retries) and platform-specific installation (rpm/dpkg/installer)

**Key behaviors**:
- Fails if Nix is already installed (flox includes its own Nix)
- Installation is chosen by looking for `flox` first, then `nix`. Already present means skip, unless `force-reinstall` or a mismatched `version` pin says otherwise; a foreign Nix with no flox means `nix profile install`; neither means the platform package
- Nix settings go in a per-job file under `/etc/nix/`, named for the run and pulled in by a matching `!include` line, so repeated runs on one machine stay correct and concurrent jobs do not delete each other's tokens
- Config file writes go over stdin, never in a command line: `@actions/exec` echoes commands into the job log, and `nix.conf` may hold a token the action did not write and cannot mask
- Supports channels: `stable`, `qa`, `nightly`, or commit hash
- Download and installation retries are configurable via `retries` input
- Sets `FLOX_DISABLE_METRICS` env var and configures flox accordingly

## Where CI cannot help

A green CI run does not cover three things, so treat it as insufficient evidence
whenever a change touches them:

- **Behavior on a machine that already ran the action.** Every hosted-runner job
starts on a fresh VM, so anything involving state left behind by a previous run
is invisible to CI.
- **rpm.** The matrix is Ubuntu and macOS; the rpm branch of
`scripts/install-flox.sh` is never executed there.
- **Real job tokens across jobs.** The token GitHub grants a job dies with it, and
CI cannot show what a later job on the same machine inherits.

`verification/` holds by-hand checks for exactly these, with a README explaining
what each answers and what it needs. Reach for them rather than assuming; the
rpm flags in `install-flox.sh` were once wrong in a way that passed review twice
and could only be settled by running it.

## Testing

Tests are in `src/index.test.js` using Jest. The action is also tested end-to-end in CI across Ubuntu/macOS with stable/nightly channels.
Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,23 @@ jobs:
| `use-cache` | Cache the downloaded flox package to speed up subsequent runs | `"true"` |
| `github-token` | GitHub token for Nix flake rate limiting | `${{ github.token }}` |
| `trusted-environments` | Comma-separated FloxHub envs to trust (e.g. `owner/env1,owner/env2`) | `""` |
| `extra-nix-config` | Additional lines to append to `/etc/nix/nix.conf` | `""` |
| `extra-nix-config` | Additional Nix settings, one per line, written to the file this action owns | `""` |
| `extra-substituters` | Space-separated Nix binary cache URLs | `""` |
| `extra-substituter-keys` | Space-separated public keys for extra substituters | `""` |
| `proxy` | HTTP/HTTPS/SOCKS5 proxy URL for network requests | `""` |
| `disable-upgrade-notifications` | Suppress flox upgrade notifications in CI output | `"true"` |
| `write-summary` | Write a Flox installation summary to the job summary page | `"false"` |
| `extra-flox-config` | Key=value pairs for `flox config --set`, one per line | `""` |
| `force-reinstall` | Reinstall flox even when it is already present on the runner | `"false"` |

## 📤 Outputs

| Output | Description |
|--------|-------------|
| `flox-version` | The installed flox version string |
| `flox-path` | Absolute path to the flox binary |
| `nix-detected` | Whether Nix was pre-installed (`true`/`false`) |
| `nix-detected` | Whether a Nix binary was found on `PATH` (`true`/`false`) |
| `flox-preinstalled` | Whether flox was already present and installation was skipped (`true`/`false`) |

### Example with custom inputs

Expand Down Expand Up @@ -126,6 +128,26 @@ When Nix is already present on the runner (e.g. from [cachix/install-nix-action]

> **Note:** The `use-cache` input has no effect in this path — there is no installer package to cache when installing via an existing Nix.

## 🖥️ Self-hosted and other persistent runners

GitHub-hosted runners start every job on a fresh machine. Self-hosted runners, and larger runners with a persistent disk, do not: whatever the previous job installed is still there when the next one starts. This action accounts for that in two ways.

**The runner needs `sudo` and `xz` present first.** Both are pre-dependencies of the flox `deb` and `rpm`, and `sudo` is needed regardless of how flox is installed, since this action uses it to write the Nix configuration. GitHub's hosted images carry both; a minimal self-hosted machine may not, in which case installation fails on the missing dependency and retries until it gives up. Install them as part of provisioning the runner.

**It looks for flox before it looks for Nix.** The flox packages ship their own Nix and symlink it into `/usr/bin`, so a runner that has already installed flox has a `nix` on `PATH` that this action put there. Checking for `flox` first tells the two situations apart. When flox is already present the installation is skipped, `flox-preinstalled` is set to `true`, and the run costs nothing beyond the configuration steps. Set `force-reinstall: true` to install the channel's current release on every run instead, or pin `version` to reinstall whenever the pinned version is newer than the one already installed.

**Downgrading in place fails with an error.** Flox brings its own Nix, and a Nix store cannot be read by a Nix older than the one that last wrote it, so installing an older flox over a newer one leaves a machine that breaks at first use rather than at install time. No package manager refuses the swap on those grounds, so the action checks before installing and stops. To move a runner back to an older version, remove flox and `/nix` from it and install again. A reference with no version ordering, a commit-hash channel for instance, cannot be checked this way; those are allowed through with a warning.

**It writes its Nix configuration fresh for every job.** The action writes its settings to a file under `/etc/nix/` named for the job that owns them, `install-flox-action-<run>-<attempt>-<id>.conf`, and adds one matching `!include` line to `/etc/nix/nix.conf`. A new file each job means the recorded `github-token` is never the expired one from a previous job, and it means a machine running several jobs at once gives each its own file: a shared one would let the first job to finish delete a token another job is still using. The post step removes this job's file and, with it, any include line whose file is gone, so a job killed before its post step ran does not leave litter behind. A stale token is worse than no token: Nix falls back to anonymous, rate-limited access when none is configured, but fails outright with `HTTP error 401` when it finds one that has expired.

An `access-tokens` line already present in `nix.conf` is treated as yours and left alone; the action will not write its own token over it, and says so in the log when it defers. If that line is a leftover workaround rather than a token you manage, remove it: an expired token there is worse than none, because Nix fails with `HTTP error 401` instead of falling back to anonymous access.

> **Note:** The token GitHub grants a job is written to a root-owned but world-readable file, because Nix has to be able to read it. A post-job step removes that file when the job ends. On a persistent runner, any job running in between can read it, so prefer a token scoped no wider than the job needs.

Versions before this one appended their settings directly into `nix.conf` beneath an `# Added by install-flox-action` comment. On the first run of a newer version that comment goes, along with the `access-tokens`, `extra-trusted-substituters`, and `extra-trusted-public-keys` lines below it, since those are the ones that go stale. Anything else in the old block came from your `extra-nix-config` and is left where it is.

Reinstalling installs the platform package, so on a runner that has both flox and an unrelated Nix, `force-reinstall` and a mismatched `version` pin both run the package installer rather than `nix profile install`. If that is not what you want on such a machine, leave both unset and the run will skip installation entirely.

## 🚀 Caching

This action involves two distinct caching layers.
Expand Down
26 changes: 23 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ branding:
inputs:

version:
description: "Select a specific version from a channel."
description: >-
Select a specific version from a channel. Installing a
version older than the one already on the runner fails:
flox brings its own Nix, which cannot read a store
written by a newer one.
default: ""

channel:
Expand Down Expand Up @@ -47,7 +51,9 @@ inputs:

extra-nix-config:
description: >-
Additional lines to append to /etc/nix/nix.conf.
Additional Nix settings, one per line. Written to a
per-job file under /etc/nix/ that nix.conf includes
and the job's post step removes.
default: ""

extra-substituters:
Expand Down Expand Up @@ -90,14 +96,28 @@ inputs:
description: "Cache the downloaded flox package to speed up subsequent runs"
default: "true"

force-reinstall:
description: >-
Reinstall flox even when it is already present on the
runner. Only affects runners whose disk survives the
job, where flox may remain from an earlier run.
Installs the platform package, so on a runner that also
has an unrelated Nix, prefer leaving this unset.
default: "false"

outputs:
flox-version:
description: "The installed flox version"
flox-path:
description: "Absolute path to the flox binary"
nix-detected:
description: "Whether Nix was pre-installed (true/false)"
description: "Whether a Nix binary was found on PATH (true/false)"
flox-preinstalled:
description: >-
Whether flox was already present and installation was
skipped (true/false)

runs:
using: 'node24'
main: 'dist/index.js'
post: 'dist/index.js'
2 changes: 1 addition & 1 deletion badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading