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
63 changes: 10 additions & 53 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,60 +32,17 @@ jobs:
fetch-depth: 0
ref: ${{ github.head_ref }}

- name: "Find environment"
- name: "Find environments"
id: "envs"
run: |
envs_per_system="["
envs_only="["

update_all=${{ github.event_name == 'schedule' && 'true' || '' }}
BASE_SHA="${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'HEAD~1' }}"
if git diff --name-only $BASE_SHA HEAD -- | grep -E "flake.nix|flake.lock|.github" ; then
echo "detected major change"
update_all=true
fi

while IFS= read manifest_path; do
env_path=$(realpath -s $(dirname $manifest_path)/../..)
rel_env_path="${env_path#$PWD/}"
echo "env_path=$env_path"
echo "rel_env_path=$rel_env_path"
if [ -f "$env_path/test.sh" ] && [ "$update_all" == "true" ] || ( git diff --name-only $BASE_SHA HEAD | grep -q "$rel_env_path/" ; ); then
name=$(basename $env_path)

num_of_services=$(yq -oy '.services | length' $manifest_path)
start_services="true"
if [ "$num_of_services" -eq 0 ]; then
start_services="false"
fi

readarray systems < <(yq e -o=j -I=0 '.options.systems[]' $manifest_path)
comma_per_system=""
if [ "$envs_per_system" != "[" ]; then comma_per_system=","; fi
for system in "${systems[@]}"; do
system=$(echo $system | xargs)
envs_per_system="$envs_per_system$comma_per_system{\"example\":\"$name\",\"system\":\"$system\",\"start_services\":$start_services}"
comma_per_system=","
done

comma_only=""
if [ "$envs_only" != "[" ]; then comma_only=","; fi
envs_only="$envs_only$comma_only\"$name\""
fi
done <<< "$(find $PWD -maxdepth 4 -name manifest.toml)"
envs_per_system="$envs_per_system]"
envs_only="$envs_only]"

echo "-- envs_per_system ---------"
echo "$envs_per_system" | jq
echo "----------------------------"

echo "-- envs_only ---------------"
echo "$envs_only" | jq
echo "----------------------------"

echo "envs_per_system=$envs_per_system" >> "$GITHUB_OUTPUT"
echo "envs_only=$envs_only" >> "$GITHUB_OUTPUT"
env:
BASE_SHA: >-
${{ github.event_name == 'pull_request'
&& github.event.pull_request.base.sha
|| 'HEAD~1' }}
UPDATE_ALL: >-
${{ github.event_name == 'schedule'
&& 'true' || '' }}
run: bash scripts/discover-envs.sh

test:
name: "Test '${{ matrix.example }}' example on '${{ matrix.system }}'"
Expand Down
14 changes: 14 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Floxenvs local development recipes.
# Requires: just (added to devShell in flake.nix)

# Discover environments and output CI matrix JSON
discover-envs:
bash scripts/discover-envs.sh

# Table of all environments with metadata
list-envs:
bash scripts/discover-envs.sh --list

# Validate all manifests parse correctly
validate:
bash scripts/discover-envs.sh --validate
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
builtins.map mkFloxEnvApp environmentsWithTest
);
devShells.default = pkgs.mkShell {
packages = [];
packages = [ pkgs.just ];
};
}
);
Expand Down
200 changes: 200 additions & 0 deletions scripts/discover-envs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#!/usr/bin/env bash
#
# Discover flox environments and produce CI matrix JSON.
#
# Parses manifest.lock (JSON) instead of manifest.toml to
# avoid brittle TOML parsing. Only needs coreutils, git, jq
# (all present on ubuntu-latest).
#
# Environment variables (inputs):
# BASE_SHA - git diff base (default: HEAD~1)
# UPDATE_ALL - set to "true" to include all envs
# GITHUB_OUTPUT - if set, write CI outputs there
#
# Usage:
# bash scripts/discover-envs.sh # JSON matrices
# bash scripts/discover-envs.sh --list # table view
# bash scripts/discover-envs.sh --validate # check manifests
#
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"

# ── Lock file helpers ─────────────────────────────────────

# Check if environment has services defined in lock file.
has_services() {
local lock="$1"
jq -e '.manifest.services // {} | length > 0' "$lock" \
>/dev/null 2>&1
}

# Extract systems list from lock file (one per line).
get_systems() {
local lock="$1"
jq -r '.manifest.options.systems // [] | .[]' "$lock"
}

# ── Environment discovery ──────────────────────────────────

find_locks() {
find "$REPO_ROOT" -maxdepth 4 -path '*/.flox/env/manifest.lock' \
-not -path '*/_worktrees/*' \
-not -path '*/remote/*' \
-not -path '*/.git/*' \
| sort
}

# Resolve lock path to the env directory (two levels up
# from .flox/env/manifest.lock).
lock_to_env() {
local lock="$1"
dirname "$(dirname "$(dirname "$lock")")"
}

# ── Modes ──────────────────────────────────────────────────

mode_validate() {
local warnings=0
while IFS= read -r lock; do
local env_path
env_path="$(lock_to_env "$lock")"
local name
name="$(basename "$env_path")"

if ! jq empty "$lock" 2>/dev/null; then
echo "FAIL: $name - invalid JSON in manifest.lock"
warnings=$((warnings + 1))
continue
fi

local systems
systems="$(get_systems "$lock")"
if [ -z "$systems" ]; then
echo "WARN: $name - no systems found (skipped in CI)"
continue
fi

local svc="false"
if has_services "$lock"; then
svc="true"
fi

echo "OK: $name systems=$(echo "$systems" | tr '\n' ',' | sed 's/,$//') services=$svc"
done < <(find_locks)

if [ "$warnings" -gt 0 ]; then
echo ""
echo "$warnings lock file(s) have issues"
exit 1
fi
echo ""
echo "All lock files valid."
}

mode_list() {
printf "%-25s %-8s %-40s\n" "ENVIRONMENT" "SERVICES" "SYSTEMS"
printf "%-25s %-8s %-40s\n" "-----------" "--------" "-------"
while IFS= read -r lock; do
local env_path
env_path="$(lock_to_env "$lock")"
local name
name="$(basename "$env_path")"

local svc="no"
if has_services "$lock"; then
svc="yes"
fi

local systems
systems="$(get_systems "$lock" | tr '\n' ',' | sed 's/,$//')"

printf "%-25s %-8s %-40s\n" "$name" "$svc" "$systems"
done < <(find_locks)
}

mode_discover() {
local base_sha="${BASE_SHA:-HEAD~1}"
local update_all="${UPDATE_ALL:-}"

# Check for major changes that force full rebuild
if [ -z "$update_all" ]; then
if git -C "$REPO_ROOT" diff --name-only "$base_sha" HEAD -- \
| grep -qE 'flake\.nix|flake\.lock|\.github'; then
echo "detected major change" >&2
update_all=true
fi
fi

local envs_per_system=()
local envs_only=()

while IFS= read -r lock; do
local env_path
env_path="$(lock_to_env "$lock")"
local name
name="$(basename "$env_path")"
local rel_env_path
rel_env_path="${env_path#"$REPO_ROOT"/}"

echo "env_path=$env_path" >&2
echo "rel_env_path=$rel_env_path" >&2

# Include env if: (has test.sh AND update_all) OR git changed
local include=false
if [ -f "$env_path/test.sh" ] && [ "$update_all" = "true" ]; then
include=true
elif git -C "$REPO_ROOT" diff --name-only "$base_sha" HEAD \
| grep -q "$rel_env_path/"; then
include=true
fi

if [ "$include" = "true" ]; then
local start_services="false"
if has_services "$lock"; then
start_services="true"
fi

local systems
systems="$(get_systems "$lock")"

while IFS= read -r sys; do
[ -z "$sys" ] && continue
envs_per_system+=("{\"example\":\"$name\",\"system\":\"$sys\",\"start_services\":$start_services}")
done <<< "$systems"

envs_only+=("\"$name\"")
fi
done < <(find_locks)

# Build JSON arrays
local per_system_json
per_system_json="[$(IFS=,; echo "${envs_per_system[*]:-}")]"
local only_json
only_json="[$(IFS=,; echo "${envs_only[*]:-}")]"

echo "-- envs_per_system ---------" >&2
echo "$per_system_json" | jq . >&2
echo "----------------------------" >&2

echo "-- envs_only ---------------" >&2
echo "$only_json" | jq . >&2
echo "----------------------------" >&2

# Output for CI or stdout
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "envs_per_system=$per_system_json" >> "$GITHUB_OUTPUT"
echo "envs_only=$only_json" >> "$GITHUB_OUTPUT"
else
echo "envs_per_system=$per_system_json"
echo "envs_only=$only_json"
fi
}

# ── Main ───────────────────────────────────────────────────

case "${1:-}" in
--validate) mode_validate ;;
--list) mode_list ;;
*) mode_discover ;;
esac
Loading