Skip to content

Commit 3bcb63c

Browse files
authored
Merge pull request #196 from NixOS/nixpkgs-diff-for-forks
Nixpkgs diff for forks
2 parents a8b578d + 19d12a6 commit 3bcb63c

File tree

6 files changed

+192
-68
lines changed

6 files changed

+192
-68
lines changed

.github/workflows/main.yml

+37-20
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,48 @@
11
name: CI
22
on:
3-
pull_request:
3+
# We use pull_request_target such that Nixpkgs diff processing also works,
4+
# because we need repository secrets for that, which pull_request doesn't allow from forks.
5+
# However, it's very important that we don't run code from forks without sandboxing it,
6+
# because that way anybody could potentially extract repository secrets!
7+
pull_request_target:
48
push:
59
branches:
610
- master
711

8-
concurrency:
9-
group: ${{ github.ref }}
10-
cancel-in-progress: true
11-
1212
jobs:
1313
check:
1414
runs-on: ubuntu-latest
1515
steps:
1616
- uses: actions/checkout@v4
17+
if: github.event_name != 'pull_request_target'
1718

18-
- uses: cachix/install-nix-action@v26
19-
20-
- name: reuse lint
21-
run: nix-build -A packages.reuse && result/bin/reuse lint
22-
23-
- name: hlint
24-
run: nix-build -A checks.hlint
19+
- uses: actions/checkout@v4
20+
if: github.event_name == 'pull_request_target'
21+
with:
22+
# To prevent running untrusted code from forks,
23+
# pull_request_target will cause the base branch to be checked out, not the PR branch.
24+
# In our case we check out the PR branch regardless,
25+
# because we're sandboxing all untrusted code with a `nix-build`.
26+
# (and the sandbox is enabled by default at least on Linux)
27+
ref: refs/pull/${{ github.event.pull_request.number }}/merge
2528

26-
- name: treefmt
27-
run: nix-build -A checks.treefmt
29+
- uses: cachix/install-nix-action@v26
2830

29-
- name: build nixfmt
30-
run: nix-build
31-
if: success() || failure()
31+
- uses: cachix/cachix-action@v14
32+
with:
33+
name: nixos-nixfmt
34+
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
3235

33-
- name: run tests
34-
run: nix-shell --run ./test/test.sh
36+
- name: checks
37+
run: nix-build -A ci
3538

3639
nixpkgs-diff:
3740
runs-on: ubuntu-latest
38-
if: github.event_name == 'pull_request'
41+
if: github.event_name == 'pull_request_target'
42+
# Ensures that we don't run two comment-posting workflows at the same time
43+
concurrency:
44+
group: ${{ github.workflow_ref }}-${{ github.event.pull_request.number }}
45+
cancel-in-progress: true
3946
steps:
4047
- name: Find Comment
4148
uses: peter-evans/find-comment@v3
@@ -57,10 +64,20 @@ jobs:
5764
5865
Will be available [here](https://github.com/${{ vars.MACHINE_USER }}/nixpkgs/commits/nixfmt-${{ github.event.pull_request.number }})
5966
67+
# To prevent running untrusted code from forks,
68+
# pull_request_target will cause the base branch to be checked out, not the PR branch.
69+
# This is exactly what we want in this case,
70+
# because the sync-pr.sh script cannot be run sandboxed since it needs to have side effects.
71+
# Instead, the script itself fetches the PR, but then runs its code within sandboxed derivations.
6072
- uses: actions/checkout@v4
6173

6274
- uses: cachix/install-nix-action@v26
6375

76+
- uses: cachix/cachix-action@v14
77+
with:
78+
name: nixos-nixfmt
79+
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
80+
6481
- run: |
6582
./scripts/sync-pr.sh \
6683
https://github.com/${{ github.repository }} \

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
/dist/
44
/dist-newstyle
55
/.ghc.environment.*
6-
/result
6+
/result*
77
/.direnv

default.nix

+63-21
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,18 @@ in
99
let
1010
overlay = self: super: {
1111
haskell = super.haskell // {
12-
packageOverrides = self: super: { nixfmt = self.callCabal2nix "nixfmt" src { }; };
12+
packageOverrides = self: super: { nixfmt = self.callCabal2nix "nixfmt" haskellSource { }; };
1313
};
14+
15+
treefmt = super.treefmt.overrideAttrs (old: {
16+
patches = [
17+
# Makes it work in parallel: https://github.com/numtide/treefmt/pull/282
18+
(self.fetchpatch {
19+
url = "https://github.com/numtide/treefmt/commit/f596795cd24b50f048cc395866bb90a89d99152d.patch";
20+
hash = "sha256-EPn+JAT3aZLSWmpdi9ULZ8o8RvrX+UFp0cQWfBcQgVg=";
21+
})
22+
];
23+
});
1424
};
1525

1626
pkgs = import nixpkgs {
@@ -23,15 +33,27 @@ let
2333
};
2434

2535
inherit (pkgs) haskell lib;
36+
fs = lib.fileset;
37+
38+
allFiles = fs.gitTracked ./.;
2639

27-
src = lib.fileset.toSource {
40+
# Used for source-wide checks
41+
source = fs.toSource {
2842
root = ./.;
29-
fileset = lib.fileset.unions [
30-
./nixfmt.cabal
31-
./src
32-
./main
33-
./LICENSE
34-
];
43+
fileset = allFiles;
44+
};
45+
46+
haskellSource = fs.toSource {
47+
root = ./.;
48+
# Limit to only files needed for the Haskell build
49+
fileset = fs.intersection allFiles (
50+
fs.unions [
51+
./nixfmt.cabal
52+
./src
53+
./main
54+
./LICENSE
55+
]
56+
);
3557
};
3658

3759
build = lib.pipe pkgs.haskellPackages.nixfmt [
@@ -53,13 +75,39 @@ let
5375
# Haskell formatter
5476
programs.fourmolu.enable = true;
5577
};
78+
79+
checks = {
80+
inherit build;
81+
hlint = pkgs.build.haskell.hlint haskellSource;
82+
reuse = pkgs.stdenvNoCC.mkDerivation {
83+
name = "nixfmt-reuse";
84+
src = source;
85+
nativeBuildInputs = with pkgs; [ reuse ];
86+
buildPhase = "reuse lint";
87+
installPhase = "touch $out";
88+
};
89+
tests = pkgs.stdenvNoCC.mkDerivation {
90+
name = "nixfmt-tests";
91+
src = fs.toSource {
92+
root = ./.;
93+
fileset = fs.intersection allFiles ./test;
94+
};
95+
nativeBuildInputs = with pkgs; [
96+
shellcheck
97+
build
98+
];
99+
patchPhase = "patchShebangs .";
100+
buildPhase = "./test/test.sh";
101+
installPhase = "touch $out";
102+
};
103+
treefmt = treefmtEval.config.build.check source;
104+
};
56105
in
57106
build
58107
// {
59-
packages = {
60-
nixfmt = build;
61-
inherit (pkgs) reuse;
62-
};
108+
packages.nixfmt = build;
109+
110+
inherit pkgs;
63111

64112
shell = pkgs.haskellPackages.shellFor {
65113
packages = p: [ p.nixfmt ];
@@ -74,13 +122,7 @@ build
74122
];
75123
};
76124

77-
checks = {
78-
hlint = pkgs.build.haskell.hlint src;
79-
treefmt = treefmtEval.config.build.check (
80-
lib.fileset.toSource {
81-
root = ./.;
82-
fileset = lib.fileset.gitTracked ./.;
83-
}
84-
);
85-
};
125+
inherit checks;
126+
127+
ci = pkgs.linkFarm "ci" checks;
86128
}

scripts/sync-pr-support.nix

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
This file supports the ./sync-pr.sh script with some Nix code.
3+
Note that we are not using the nixfmt from the tree of this file,
4+
instead the nixfmt to use is passed into the necessary function.
5+
This way we don't need to rely on this internal file being stable.
6+
*/
7+
let
8+
pkgs = (import ../default.nix { }).pkgs;
9+
inherit (pkgs) lib;
10+
in
11+
{
12+
# Filters a repo into a store path only containing its Git-tracked Nix files
13+
repoNixFiles =
14+
{ repo }:
15+
lib.fileset.toSource {
16+
root = repo;
17+
fileset = lib.fileset.intersection (lib.fileset.gitTracked repo) (
18+
lib.fileset.fileFilter (file: file.hasExt "nix") repo
19+
);
20+
};
21+
22+
# Returns a derivation that contains the passed store path (e.g. from the above function)
23+
# but with all Nix files formatted with the given nixfmt
24+
formattedGitRepo =
25+
{ storePath, nixfmtPath }:
26+
let
27+
nixfmt = (import nixfmtPath { }).packages.nixfmt;
28+
in
29+
pkgs.runCommand "formatted"
30+
{
31+
nativeBuildInputs = with pkgs; [
32+
treefmt
33+
nixfmt
34+
];
35+
treefmtConfig = ''
36+
[formatter.nixfmt-rfc-style]
37+
command = "nixfmt"
38+
includes = [ "*.nix" ]
39+
'';
40+
passAsFile = [ "treefmtConfig" ];
41+
}
42+
''
43+
cp -r --no-preserve=mode ${builtins.storePath storePath} $out
44+
treefmt \
45+
--config-file "$treefmtConfigPath" \
46+
--tree-root "$out" \
47+
--no-cache
48+
'';
49+
}

scripts/sync-pr.sh

+35-25
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# - It only minimally depends on GitHub: All operations are done using Git directly
55
# - Reuse of previous work: Formatting all of Nixpkgs takes some time, we don't want to recompute it if not necessary
66
# - Handles force pushes gracefully and linearises merge commits
7-
#
7+
# - Runs all external code in Nix derivations, so this is safe to use for PRs from forks too
88

99
set -euo pipefail
1010

@@ -24,6 +24,8 @@ nixpkgsUrl=$3
2424
nixpkgsUpstreamUrl=https://github.com/NixOS/nixpkgs
2525
nixpkgsMirrorBranch=nixfmt-$nixfmtPrNumber
2626

27+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
28+
2729
tmp=$(mktemp -d)
2830
cd "$tmp"
2931
trap 'rm -rf "$tmp"' exit
@@ -47,7 +49,7 @@ isLinear() {
4749
}
4850

4951
step "Fetching nixfmt pull request and creating a branch for the head commit"
50-
git init nixfmt
52+
git init nixfmt -b unused
5153
git -C nixfmt fetch "$nixfmtUrl" "refs/pull/$nixfmtPrNumber/merge"
5254
nixfmtBaseCommit=$(git -C nixfmt rev-parse FETCH_HEAD^1)
5355
nixfmtHeadCommit=$(git -C nixfmt rev-parse FETCH_HEAD^2)
@@ -95,7 +97,7 @@ bodyForCommitIndex() {
9597
}
9698

9799
step "Fetching upstream Nixpkgs commit history"
98-
git init --bare nixpkgs.git
100+
git init --bare nixpkgs.git -b unused
99101

100102
git -C nixpkgs.git remote add upstream "$nixpkgsUpstreamUrl"
101103
# This makes sure that we don't actually have to fetch any contents, otherwise we'd wait forever!
@@ -179,9 +181,12 @@ else
179181
echo "Checking whether commit with index $(( nixpkgsCommitCount + 1 )) ($nixpkgsCommit) in nixpkgs corresponds to commit with index $nixpkgsCommitCount ($nixfmtCommit) in nixfmt"
180182

181183
# We generate the bodies of the commits to contain the nixfmt commit so we can check against it here to verify it's the same
182-
body=$(git -C nixpkgs.git log -1 "$nixpkgsCommit" --pretty=%B)
183-
expectedBody=$(bodyForCommitIndex "$nixpkgsCommitCount")
184-
if [[ "$body" == "$expectedBody" ]]; then
184+
# Note that `--format=format:` makes it so that there's no extra newline
185+
git -C nixpkgs.git log -1 "$nixpkgsCommit" --format=format:%B > "$tmp/body"
186+
bodyForCommitIndex "$nixpkgsCommitCount" > "$tmp/expected-body"
187+
188+
# Ignore case so that we don't need to be worried about things like NixOS vs nixos in the URL
189+
if diff --ignore-case "$tmp/body" "$tmp/expected-body"; then
185190
echo "It does!"
186191
else
187192
echo "It does not, this indicates a force push was done"
@@ -199,13 +204,23 @@ else
199204
fi
200205

201206

202-
git init nixpkgs
207+
git init nixpkgs -b unused
203208
git -C nixpkgs config user.name "GitHub Actions"
204209
git -C nixpkgs config user.email "[email protected]"
205210

206211
step "Fetching contents of Nixpkgs base commit $nixpkgsBaseCommit"
207212
# This is needed because for every commit we reset Nixpkgs to the base branch before formatting
208-
git -C nixpkgs fetch --no-tags --depth 1 "$nixpkgsUpstreamUrl" "$nixpkgsBaseCommit"
213+
git -C nixpkgs fetch --no-tags --depth 1 "$nixpkgsUpstreamUrl" "$nixpkgsBaseCommit":base
214+
215+
step "Checking out Nixpkgs at the base commit"
216+
git -C nixpkgs checkout base
217+
218+
# Because we run the formatter in a Nix derivation, we need to get its input files into the Nix store.
219+
# Since they never change, it would be wasteful to import them multiple times for each nixfmt run.
220+
# So instead we just import them once and reuse that result throughout
221+
step "Importing Nix files from base commit into the Nix store"
222+
baseStorePath=$(nix-instantiate --eval --read-write-mode "$SCRIPT_DIR/sync-pr-support.nix" -A repoNixFiles.outPath --arg repo "$PWD/nixpkgs" | tr -d '"')
223+
echo "$baseStorePath"
209224

210225
step "Fetching contents of the starting commit and updating the mirror branch"
211226
# This is only needed so we can push the resulting diff after formatting
@@ -234,9 +249,6 @@ update() {
234249

235250
step "Checking out nixfmt at $nixfmtCommit"
236251
git -C nixfmt checkout -q "$nixfmtCommit"
237-
238-
step "Building nixfmt"
239-
nix-build nixfmt
240252
}
241253

242254
# Format Nixpkgs with a specific nixfmt version and push the result.
@@ -247,21 +259,21 @@ next() {
247259

248260
update "$index"
249261

250-
if [[ -n "$appliedNixfmtPath" && "$appliedNixfmtPath" == "$(realpath result)" ]]; then
251-
echo "The nixfmt store path didn't change, saving ourselves a formatting"
252-
else
253-
step "Checking out Nixpkgs at the base commit"
254-
git -C nixpkgs checkout "$nixpkgsBaseCommit" -- .
262+
step "Checking out Nixpkgs at the base commit"
263+
git -C nixpkgs checkout base -- .
255264

256-
step "Running nixfmt on nixpkgs"
257-
if ! time xargs -r -0 -P"$(nproc)" -n1 -a <(find nixpkgs -type f -name '*.nix' -print0) result/bin/nixfmt; then
258-
echo -e "\e[31mFailed to run nixfmt on some files\e[0m"
259-
exit 1
260-
fi
261-
git -C nixpkgs add -A
265+
step "Running nixfmt on nixpkgs in a derivation"
262266

263-
appliedNixfmtPath=$(realpath result)
267+
# This uses always the same sync-pr-support.nix file from the same nixfmt branch that this script is in,
268+
# but doesn't use anything else from that nixfmt branch. Instead the nixfmtPath is used for the formatting.
269+
if ! nix-build "$SCRIPT_DIR/sync-pr-support.nix" -A formattedGitRepo --arg storePath "$baseStorePath" --arg nixfmtPath "$PWD/nixfmt"; then
270+
echo -e "\e[31mFailed to run nixfmt on some files\e[0m"
271+
exit 1
264272
fi
273+
step "Syncing changes into the Nixpkgs tree"
274+
# We need to move the changed files in result/ back into the tree (without messing up the permissions and other files)
275+
rsync --archive --no-perms result/ nixpkgs
276+
git -C nixpkgs add -A
265277

266278
step "Committing the formatted result"
267279
git -C nixpkgs commit --allow-empty -m "$(bodyForCommitIndex "$index")"
@@ -271,14 +283,12 @@ next() {
271283
nixpkgsCommitCount=$(( nixpkgsCommitCount + 1 ))
272284
}
273285

274-
appliedNixfmtPath=
275286
if (( nixpkgsCommitCount == 0 )); then
276287
# If we don't have a base-formatted Nixpkgs commit yet, create it
277288
next 0
278289
else
279290
# Otherwise, just build the nixfmt that was used for the current commit, such that we know the store path
280291
update "$(( nixpkgsCommitCount - 1 ))"
281-
appliedNixfmtPath=$(realpath result)
282292
fi
283293

284294
while (( nixpkgsCommitCount - 1 < nixfmtCommitCount )); do

0 commit comments

Comments
 (0)