Compare commits
9 Commits
8997e996ba
...
88cfad2a69
| Author | SHA1 | Date | |
|---|---|---|---|
| 88cfad2a69 | |||
| 86a9f777ad | |||
| b29e80f3e9 | |||
| e32834ff7f | |||
| bb39587292 | |||
| 712b52a48d | |||
| c6eeea982e | |||
| 6bd1b4466e | |||
| d806d4df0a |
14
CLAUDE.md
14
CLAUDE.md
@@ -85,17 +85,3 @@ When adding or removing a web-facing service, update both:
|
||||
- Always use `--no-link` when running `nix build`
|
||||
- Don't use `nix build --dry-run` unless you only need evaluation — it skips the actual build
|
||||
- Avoid `2>&1` on nix commands — it can cause error output to be missed
|
||||
|
||||
## Git Worktrees
|
||||
|
||||
When the user asks you to "start a worktree" or work in a worktree, **do not create one manually** with `git worktree add`. Instead, tell the user to start a new session with:
|
||||
|
||||
```bash
|
||||
claude --worktree <name>
|
||||
```
|
||||
|
||||
This is the built-in Claude Code worktree workflow. It creates the worktree at `.claude/worktrees/<name>/` with a branch `worktree-<name>` and starts a new Claude session inside it. Cleanup is handled automatically on exit.
|
||||
|
||||
When instructed to work in a git worktree (e.g., via `isolation: "worktree"` on a subagent), you **MUST** do so. If you are unable to create or use a git worktree, you **MUST** stop work immediately and report the failure to the user. Do not fall back to working in the main working tree.
|
||||
|
||||
When applying work from a git worktree back to the main branch, commit in the worktree first, then use `git cherry-pick` from the main working tree to bring the commit over. Do not use `git checkout` or `git apply` to copy files directly. Do **not** automatically apply worktree work to the main branch — always ask the user for approval first.
|
||||
|
||||
@@ -235,7 +235,7 @@ in
|
||||
after = [ "systemd-networkd.service" ];
|
||||
requires = [ "systemd-networkd.service" ];
|
||||
serviceConfig.ExecStartPre = [
|
||||
"+${pkgs.systemd}/lib/systemd/systemd-networkd-wait-online --interface=${cfg.bridgeName}:no-carrier --timeout=60"
|
||||
"+${pkgs.systemd}/lib/systemd/systemd-networkd-wait-online --interface=${cfg.bridgeName}:no-carrier --timeout=180"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
default = "";
|
||||
description = "Extra arguments to pass to curl (e.g. --proxy http://host:port).";
|
||||
};
|
||||
|
||||
ignoredUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "Unit names to skip failure notifications for.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.thisMachine.hasRole."ntfy" {
|
||||
|
||||
@@ -14,6 +14,14 @@ in
|
||||
EnvironmentFile = "/run/agenix/ntfy-token";
|
||||
ExecStart = "${pkgs.writeShellScript "ntfy-failure-notify" ''
|
||||
unit="$1"
|
||||
# Prevent infinite recursion if this service itself fails
|
||||
[[ "$unit" == ntfy-failure@* ]] && exit 0
|
||||
ignored_units=(${lib.concatMapStringsSep " " (u: lib.escapeShellArg u) cfg.ignoredUnits})
|
||||
for ignored in "''${ignored_units[@]}"; do
|
||||
if [[ "$unit" == "$ignored" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
logfile=$(mktemp)
|
||||
trap 'rm -f "$logfile"' EXIT
|
||||
${pkgs.systemd}/bin/journalctl -u "$unit" -n 50 --no-pager -o short > "$logfile" 2>/dev/null \
|
||||
@@ -40,7 +48,7 @@ in
|
||||
mkdir -p $out/lib/systemd/system/service.d
|
||||
cat > $out/lib/systemd/system/service.d/ntfy-on-failure.conf <<'EOF'
|
||||
[Unit]
|
||||
OnFailure=ntfy-failure@%p.service
|
||||
OnFailure=ntfy-failure@%N.service
|
||||
EOF
|
||||
'')
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
let
|
||||
thisMachineIsARunner = config.thisMachine.hasRole."gitea-actions-runner";
|
||||
hostOverlays = config.nixpkgs.overlays;
|
||||
containerName = "gitea-runner";
|
||||
giteaRunnerUid = 991;
|
||||
giteaRunnerGid = 989;
|
||||
@@ -32,6 +33,7 @@ in
|
||||
|
||||
config = { config, lib, pkgs, ... }: {
|
||||
system.stateVersion = "25.11";
|
||||
nixpkgs.overlays = hostOverlays;
|
||||
|
||||
services.gitea-actions-runner.instances.inst = {
|
||||
enable = true;
|
||||
|
||||
@@ -13,6 +13,15 @@ in
|
||||
services.unifi.unifiPackage = pkgs.unifi;
|
||||
services.unifi.mongodbPackage = pkgs.mongodb-7_0;
|
||||
|
||||
# The upstream module sets KillSignal=SIGCONT so systemd doesn't interfere
|
||||
# with UniFi's self-managed shutdown. But UniFi's Java process crashes during
|
||||
# shutdown (Spring context already closed) leaving mongod orphaned in the
|
||||
# cgroup. With the default KillMode=control-group, mongod only gets SIGCONT
|
||||
# (a no-op) and runs until the 5min timeout triggers SIGKILL.
|
||||
# KillMode=mixed sends SIGCONT to the main process but SIGTERM to remaining
|
||||
# children, giving mongod a clean shutdown instead of SIGKILL.
|
||||
systemd.services.unifi.serviceConfig.KillMode = "mixed";
|
||||
|
||||
networking.firewall = lib.mkIf cfg.openMinimalFirewall {
|
||||
allowedUDPPorts = [
|
||||
3478 # STUN
|
||||
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -53,11 +53,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772252645,
|
||||
"narHash": "sha256-SVP3BYv/tY19P7mh0aG2Pgq4M/CynQEnV4y+57Ed91g=",
|
||||
"lastModified": 1772587858,
|
||||
"narHash": "sha256-w0/XBU20BdBeEIJ9i3ecr9Lc6c8uQaXUn/ri+aOsyJk=",
|
||||
"owner": "sadjow",
|
||||
"repo": "claude-code-nix",
|
||||
"rev": "42c9207e79f1e6b8b95b54a64c10452275717466",
|
||||
"rev": "0a5fc14be38fabfcfff18db749b63c9c15726765",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -228,11 +228,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772380461,
|
||||
"narHash": "sha256-O3ukj3Bb3V0Tiy/4LUfLlBpWypJ9P0JeUgsKl2nmZZY=",
|
||||
"lastModified": 1772569491,
|
||||
"narHash": "sha256-bdr6ueeXO1Xg91sFkuvaysYF0mVdwHBpdyhTjBEWv+s=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "f140aa04d7d14f8a50ab27f3691b5766b17ae961",
|
||||
"rev": "924e61f5c2aeab38504028078d7091077744ab17",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -301,11 +301,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772198003,
|
||||
"narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=",
|
||||
"lastModified": 1772542754,
|
||||
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61",
|
||||
"rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -139,7 +139,6 @@
|
||||
src = nixpkgs;
|
||||
patches = [
|
||||
./patches/dont-break-nix-serve.patch
|
||||
./patches/libreoffice-noto-fonts-subset.patch
|
||||
];
|
||||
};
|
||||
patchedNixpkgs = nixpkgs.lib.fix (self: (import "${patchedNixpkgsSrc}/flake.nix").outputs { self = nixpkgs; });
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
networking.hostName = "s0";
|
||||
|
||||
ntfy-alerts.ignoredUnits = [ "logrotate" ];
|
||||
|
||||
# system.autoUpgrade.enable = true;
|
||||
|
||||
nix.gc.automatic = lib.mkForce false; # allow the nix store to serve as a build cache
|
||||
|
||||
@@ -16,6 +16,14 @@ in
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ prev.writableTmpDirAsHomeHook ];
|
||||
});
|
||||
|
||||
# Retry on push failure to work around hyper connection pool race condition.
|
||||
# https://github.com/zhaofengli/attic/pull/246
|
||||
attic-client = prev.attic-client.overrideAttrs (old: {
|
||||
patches = (old.patches or [ ]) ++ [
|
||||
../patches/attic-client-push-retry.patch
|
||||
];
|
||||
});
|
||||
|
||||
# Add --zeroconf-port support to Spotify Connect plugin so librespot
|
||||
# binds to a fixed port that can be opened in the firewall.
|
||||
music-assistant = prev.music-assistant.overrideAttrs (old: {
|
||||
|
||||
143
patches/attic-client-push-retry.patch
Normal file
143
patches/attic-client-push-retry.patch
Normal file
@@ -0,0 +1,143 @@
|
||||
diff --git a/attic/src/api/v1/upload_path.rs b/attic/src/api/v1/upload_path.rs
|
||||
index 5b1231e5..cb90928c 100644
|
||||
--- a/attic/src/api/v1/upload_path.rs
|
||||
+++ b/attic/src/api/v1/upload_path.rs
|
||||
@@ -25,7 +25,7 @@ pub const ATTIC_NAR_INFO_PREAMBLE_SIZE: &str = "X-Attic-Nar-Info-Preamble-Size";
|
||||
/// Regardless of client compression, the server will always decompress
|
||||
/// the NAR to validate the NAR hash before applying the server-configured
|
||||
/// compression again.
|
||||
-#[derive(Debug, Serialize, Deserialize)]
|
||||
+#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UploadPathNarInfo {
|
||||
/// The name of the binary cache to upload to.
|
||||
pub cache: CacheName,
|
||||
diff --git a/client/src/push.rs b/client/src/push.rs
|
||||
index 309bd4b6..f3951d2b 100644
|
||||
--- a/client/src/push.rs
|
||||
+++ b/client/src/push.rs
|
||||
@@ -560,57 +560,83 @@ pub async fn upload_path(
|
||||
);
|
||||
let bar = mp.add(ProgressBar::new(path_info.nar_size));
|
||||
bar.set_style(style);
|
||||
- let nar_stream = NarStreamProgress::new(store.nar_from_path(path.to_owned()), bar.clone())
|
||||
- .map_ok(Bytes::from);
|
||||
|
||||
- let start = Instant::now();
|
||||
- match api
|
||||
- .upload_path(upload_info, nar_stream, force_preamble)
|
||||
- .await
|
||||
- {
|
||||
- Ok(r) => {
|
||||
- let r = r.unwrap_or(UploadPathResult {
|
||||
- kind: UploadPathResultKind::Uploaded,
|
||||
- file_size: None,
|
||||
- frac_deduplicated: None,
|
||||
- });
|
||||
-
|
||||
- let info_string: String = match r.kind {
|
||||
- UploadPathResultKind::Deduplicated => "deduplicated".to_string(),
|
||||
- _ => {
|
||||
- let elapsed = start.elapsed();
|
||||
- let seconds = elapsed.as_secs_f64();
|
||||
- let speed = (path_info.nar_size as f64 / seconds) as u64;
|
||||
+ // Create a new stream for each retry attempt
|
||||
+ let bar_ref = &bar;
|
||||
+ let nar_stream = move || {
|
||||
+ NarStreamProgress::new(store.nar_from_path(path.to_owned()), bar_ref.clone())
|
||||
+ .map_ok(Bytes::from)
|
||||
+ };
|
||||
|
||||
- let mut s = format!("{}/s", HumanBytes(speed));
|
||||
+ let start = Instant::now();
|
||||
+ let mut retries = 0;
|
||||
+ const MAX_RETRIES: u32 = 3;
|
||||
+ const RETRY_DELAY: Duration = Duration::from_millis(250);
|
||||
|
||||
- if let Some(frac_deduplicated) = r.frac_deduplicated {
|
||||
- if frac_deduplicated > 0.01f64 {
|
||||
- s += &format!(", {:.1}% deduplicated", frac_deduplicated * 100.0);
|
||||
+ loop {
|
||||
+ let result = api
|
||||
+ .upload_path(upload_info.clone(), nar_stream(), force_preamble)
|
||||
+ .await;
|
||||
+ match result {
|
||||
+ Ok(r) => {
|
||||
+ let r = r.unwrap_or(UploadPathResult {
|
||||
+ kind: UploadPathResultKind::Uploaded,
|
||||
+ file_size: None,
|
||||
+ frac_deduplicated: None,
|
||||
+ });
|
||||
+
|
||||
+ let info_string: String = match r.kind {
|
||||
+ UploadPathResultKind::Deduplicated => "deduplicated".to_string(),
|
||||
+ _ => {
|
||||
+ let elapsed = start.elapsed();
|
||||
+ let seconds = elapsed.as_secs_f64();
|
||||
+ let speed = (path_info.nar_size as f64 / seconds) as u64;
|
||||
+
|
||||
+ let mut s = format!("{}/s", HumanBytes(speed));
|
||||
+
|
||||
+ if let Some(frac_deduplicated) = r.frac_deduplicated {
|
||||
+ if frac_deduplicated > 0.01f64 {
|
||||
+ s += &format!(", {:.1}% deduplicated", frac_deduplicated * 100.0);
|
||||
+ }
|
||||
}
|
||||
+
|
||||
+ s
|
||||
}
|
||||
+ };
|
||||
|
||||
- s
|
||||
+ mp.suspend(|| {
|
||||
+ eprintln!(
|
||||
+ "✅ {} ({})",
|
||||
+ path.as_os_str().to_string_lossy(),
|
||||
+ info_string
|
||||
+ );
|
||||
+ });
|
||||
+ bar.finish_and_clear();
|
||||
+
|
||||
+ return Ok(());
|
||||
+ }
|
||||
+ Err(e) => {
|
||||
+ if retries < MAX_RETRIES {
|
||||
+ retries += 1;
|
||||
+ mp.suspend(|| {
|
||||
+ eprintln!(
|
||||
+ "❕ {}: Upload failed, retrying ({}/{})...",
|
||||
+ path.as_os_str().to_string_lossy(),
|
||||
+ retries,
|
||||
+ MAX_RETRIES
|
||||
+ );
|
||||
+ });
|
||||
+ tokio::time::sleep(RETRY_DELAY).await;
|
||||
+ continue;
|
||||
}
|
||||
- };
|
||||
|
||||
- mp.suspend(|| {
|
||||
- eprintln!(
|
||||
- "✅ {} ({})",
|
||||
- path.as_os_str().to_string_lossy(),
|
||||
- info_string
|
||||
- );
|
||||
- });
|
||||
- bar.finish_and_clear();
|
||||
+ mp.suspend(|| {
|
||||
+ eprintln!("❌ {}: {}", path.as_os_str().to_string_lossy(), e);
|
||||
+ });
|
||||
+ bar.finish_and_clear();
|
||||
|
||||
- Ok(())
|
||||
- }
|
||||
- Err(e) => {
|
||||
- mp.suspend(|| {
|
||||
- eprintln!("❌ {}: {}", path.as_os_str().to_string_lossy(), e);
|
||||
- });
|
||||
- bar.finish_and_clear();
|
||||
- Err(e)
|
||||
+ return Err(e);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
Fix notoSubset glob for noto-fonts >= 2026.02.01.
|
||||
|
||||
noto-fonts switched from variable fonts (NotoSansArabic[wdth,wght].ttf)
|
||||
to static fonts (NotoSansArabic.ttf). The old glob pattern only matched
|
||||
files with brackets in the name, causing the cp to fail.
|
||||
|
||||
--- a/pkgs/applications/office/libreoffice/default.nix
|
||||
+++ b/pkgs/applications/office/libreoffice/default.nix
|
||||
@@ -191,7 +191,7 @@
|
||||
runCommand "noto-fonts-subset" { } ''
|
||||
mkdir -p "$out/share/fonts/noto/"
|
||||
${concatMapStrings (x: ''
|
||||
- cp "${noto-fonts}/share/fonts/noto/NotoSans${x}["*.[ot]tf "$out/share/fonts/noto/"
|
||||
+ cp "${noto-fonts}/share/fonts/noto/NotoSans${x}"*.[ot]tf "$out/share/fonts/noto/"
|
||||
'') suffixes}
|
||||
'';
|
||||
Reference in New Issue
Block a user