74 Commits

Author SHA1 Message Date
Oleks 24a75ee8d5 bump woodpecker-peek: dark-theme color palette
ci/woodpecker/push/amd64 Pipeline was successful
ci/woodpecker/push/arm64 Pipeline was successful
2026-06-06 13:54:43 +03:00
Oleks e534dfcff6 bump woodpecker-peek: textarea log + drawer expand
ci/woodpecker/push/amd64 Pipeline was canceled
ci/woodpecker/push/arm64 Pipeline was canceled
2026-06-06 12:59:28 +03:00
Oleks f77f3b7f11 bump woodpecker-peek: workflow group headers
ci/woodpecker/push/arm64 Pipeline was canceled
ci/woodpecker/push/amd64 Pipeline was canceled
2026-06-06 12:51:59 +03:00
Oleks cdbeebf5e7 bump woodpecker-peek: follow/maximize log knobs
ci/woodpecker/push/arm64 Pipeline was canceled
ci/woodpecker/push/amd64 Pipeline was canceled
2026-06-06 12:23:40 +03:00
Oleks e311bb1066 bump woodpecker-peek: preserve log disclosure state across rebuilds
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was canceled
2026-06-06 00:45:56 +03:00
Oleks 2aadba34e2 bump woodpecker-peek: base64 log fix + cancel button
ci/woodpecker/push/amd64 Pipeline failed
ci/woodpecker/push/arm64 Pipeline was canceled
2026-06-05 23:49:46 +03:00
Oleks 7f2979d7ff bump woodpecker-peek: fix step log PID vs database ID
ci/woodpecker/push/arm64 Pipeline was canceled
ci/woodpecker/push/amd64 Pipeline was canceled
2026-06-05 23:31:16 +03:00
Oleks f8053537a9 feat(antigravity): update to v2.0 split; expose ide, ide-no-fhs, cli; add all to CI warming
ci/woodpecker/push/amd64 Pipeline failed
ci/woodpecker/push/arm64 Pipeline was canceled
2026-06-05 12:34:25 +03:00
Oleks 550c1d9f44 style: auto-format from pre-push hooks
ci/woodpecker/push/amd64 Pipeline failed
ci/woodpecker/push/arm64 Pipeline failed
2026-06-04 22:49:50 +03:00
Oleks 55594fd632 refactor(s390x): export rustc-symlink overlay + add formatter; refresh pins
Lift the s390x rustc symlink_file patch overlay into overlays.s390xRustcSymlink
so nixos-ci consumes one definition (patch travels with it) instead of
duplicating it. Add nixfmt-rfc-style formatter. Refresh fleet-pins + parity
inputs to HEAD so the lock collapses to a single nixpkgs (1c3fe55).
2026-06-04 22:49:48 +03:00
Oleks b797aefb28 refactor(ci): retrofit onto parity-lib mkAtticClosurePublish (#200)
ci/woodpecker/push/amd64 Pipeline failed
ci/woodpecker/push/arm64 Pipeline was successful
Replace the bespoke ci/publish.py attic-push logic with parity-lib's
mkAtticClosurePublish builder (attic-closure archetype, cluster#104,
emmett#44). Adds the parity input (locked at d265a79) and wires the
per-arch package closures through builders.mkAtticClosurePublish, with
the endpoint (nix-cache-upload.oleks.space) and passEntry
(infra/attic/ci_token) overridden so the attic push is byte-for-byte the
pre-parity behaviour.

.woodpecker/{amd64,arm64}.yaml thinned to PUBLISH=1 nix run .#publish /
.#publish-aarch64-linux so CI and a local run share one audited impl.
Dead ci/publish.py + ci/build.py removed.

pipeline-doctor: 9 passed / 0 failed / 0 warned.
2026-06-03 10:58:06 +03:00
Oleks e8050f9dfd ci: echo build arch (uname -m) as first line of every step for visibility
ci/woodpecker/push/arm64 Pipeline failed
ci/woodpecker/push/amd64 Pipeline failed
2026-06-02 13:42:35 +03:00
Oleks 0fd4cfe83d style: auto-format from pre-push hooks
ci/woodpecker/push/amd64 Pipeline failed
ci/woodpecker/push/arm64 Pipeline failed
2026-06-02 09:22:45 +03:00
Oleks 9d80f47625 ci: local-pipeline parity via stage/publish flake apps (cluster#192, emmett#44)
attic-closure archetype: no parity-lib builder exists for attic pushes, so
wrap the existing per-arch package build in ci/publish.py (woodpecker-peek
pattern) and expose `nix run .#{stage,publish}-amd64` + `.#publish`.

Two-halves rule: STAGE nix-builds every package in the arch list into the
local store (emmett-buildable); PUBLISH additionally attic-pushes each
closure. Local runs DRY-RUN unless --push/PUBLISH=1; CI sets PUBLISH=1.

The .woodpecker/{amd64,arm64}.yaml now call the same ci/publish.py so CI
and local runs can't drift. arm64 stays node-bound (no emmett cross path),
so it has no local-parity app. ci/build.py becomes a forwarding shim.
2026-06-02 09:22:39 +03:00
Oleks ef13a18b4c docs: gitea-local-fork end-to-end publishing flow
ci/woodpecker/push/arm64 Pipeline failed
ci/woodpecker/push/amd64 Pipeline failed
Rescued from the second flake-hub checkout (~/projects/flake-hub) before
that working copy was removed. Documents the path from local Gitea fork
to the attic binary cache.
2026-05-27 14:36:03 +03:00
Oleks 26bdad6d1b Wire oleks/mcp-chrome into flake-hub
ci/woodpecker/push/amd64 Pipeline failed
ci/woodpecker/push/arm64 Pipeline was canceled
Adds mcp-chrome as a flake input (mirroring the woodpecker-peek pattern)
and re-exposes two packages: mcp-chrome-wasm-simd (proven green, ~22 s)
and mcp-chrome-extension (KNOWN-BROKEN under nix-daemon; exposed for
local builds but kept out of the CI matrix to avoid red pipelines).

CI warms attic with mcp-chrome-wasm-simd on x86_64-linux and aarch64-linux
only; s390x cross and Darwin are out of scope.

Closes oleks/mcp-chrome#5.
2026-05-27 10:15:36 +03:00
Oleks b9d13a4814 flake: update woodpecker-peek (go 1.25 directive, fixes flake-hub build)
ci/woodpecker/push/arm64 Pipeline failed
ci/woodpecker/push/amd64 Pipeline failed
2026-05-24 18:53:34 +03:00
Oleks ac58ade9da add woodpecker-peek input + cache to attic via CI
ci/woodpecker/push/amd64 Pipeline was canceled
ci/woodpecker/push/arm64 Pipeline was canceled
Re-exposes oleks/woodpecker-peek as packages.<sys>.woodpecker-peek for
x86_64-linux and aarch64-linux, and adds it to ci/build.py so the
amd64/arm64 workflows push the closure to attic-infra-cache-k3s-1.
Consumers (emmett) then set services.woodpecker-peek.package = pkgs.woodpecker-peek
and pull the cached binary instead of rebuilding.
2026-05-24 18:16:23 +03:00
Oleks cb369d3be3 metamcp: observability patch — W3C traceparent + Prometheus /metrics (cluster#49, cluster#50)
ci/woodpecker/push/arm64 Pipeline failed
ci/woodpecker/push/amd64 Pipeline failed
Adds a postPatch step that applies metamcp-observability.patch to
the upstream metatool-ai/metamcp v2.4.22 source before pnpm build.
The patch:

  * Drops in apps/backend/src/lib/observability/{trace,metrics}.ts:
    AsyncLocalStorage trace context + parser/synthesizer per the W3C
    contract (Specs/mcp-request-id), plus a hand-rolled
    Counter/Histogram so we don't have to touch pnpm-lock.yaml /
    pnpmDeps hash (no new npm dependency).

  * Wires a top-level express middleware in apps/backend/src/index.ts
    that binds trace context, observes mcp_hop_duration_seconds on
    response close, and counts mcp_cancellation_total when the
    downstream client hangs up mid-response.

  * Adds /metrics to the Express app and last-resort process traps
    (unhandledRejection / uncaughtException) feeding
    mcp_uncaught_throw_total — the smoking-gun signal from
    cluster#44.

  * Patches process-managed-transport.send() to inject
    params._meta.traceparent on every outbound JSON-RPC bound for a
    stdio child (MCP _meta convention, Specs/mcp-request-id).

Retire when this lands upstream.
2026-05-24 05:34:47 +03:00
Oleks b38558cdcf added stalewood
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
2026-05-22 23:16:19 +03:00
Oleks ca88d13535 nix-deps: re-expose github:manelinux/nix-deps through the hub
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
Adds nix-deps as an input following our nixpkgs and re-exports its
package via mkPackages (gated to native x86_64/aarch64, since its
flake only emits eachDefaultSystem and would break the s390x cross).

Also adds .gitignore for build result symlinks.
2026-05-19 16:32:05 +03:00
oleks 577274b382 chore(gitea-local-fork): bump to v1.26.0-unstable-2026-05-17.1
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
Pin to oleks/main c45ea82 (PR #20: push SSE update on issue
close/reopen for project boards, fixes #19). Only src rev/hash/version
change; no go.mod or pnpm-lock changes so vendorHash and pnpmDeps stay
valid.
2026-05-17 21:05:55 +03:00
Claude 25bf426ae5 chore(gitea-local-fork): bump to v1.26.0-unstable-2026-05-17
ci/woodpecker/push/amd64 Pipeline failed
ci/woodpecker/push/arm64 Pipeline failed
Pin to oleks/main ad46f6c (PR #18: scope project-issue move to its own
project, fixes #17). Only src rev/hash/version change; no go.mod or
pnpm-lock changes so vendorHash and pnpmDeps stay valid.
2026-05-17 17:12:50 +03:00
Oleks 55d9a62c33 chore(gitea-local-fork): bump to v1.26.0-unstable-2026-05-16.1
Adds SSE toast notifications for project-board + milestone events
(#15). rev d4de99f96b. Frontend-only change; pnpmDeps.hash +
vendorHash verified unchanged.
2026-05-16 14:43:07 +03:00
Oleks bf0095dcd2 chore(gitea-local-fork): bump to v1.26.0-unstable-2026-05-16
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
Clean release pin (drops the -debug instrumentation build). Carries
the project-board SSE detach-ctx fix (#8) + milestone progress SSE
(#14). rev 9f588d3dd3 = squash-merge on oleks/main. pnpmDeps.hash +
vendorHash verified unchanged.
2026-05-16 10:28:33 +03:00
Oleks c4aa88d677 debug(gitea-local-fork): pin to SSE detach-ctx fix (bfc10289e6)
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
Temporary debug pin: rev bfc10289e6 carries the publish-path
instrumentation + the fix routing the async publish goroutine
through graceful.ShutdownContext() instead of the request ctx.
Will repin to a clean tagged release once verified.
2026-05-15 23:52:09 +03:00
Oleks 16b6f1ed21 chore(gitea-local-fork): bump to v1.26.0-unstable-2026-05-15.1
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline failed
Pin to oleks/main @ 9c1699feb5 — adds PR #7 (SSE push updates for
project board pages). pnpm-lock.yaml and go.mod are unchanged from
the previous pin so pnpmDeps.hash and vendorHash stay valid.
2026-05-15 22:23:01 +03:00
Oleks a9d70c58b1 fix(gitea-local-fork): refresh pnpmDeps.hash for v1.26.0-unstable-2026-05-15
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline failed
The previous pin's pnpm-lock.yaml drifted between snapshots; the
cached fetchPnpmDeps output was stale on hosts without a warmed
store, causing ERR_PNPM_NO_OFFLINE_TARBALL on remote builders.
Recomputed via nix build with empty hash, captured the 'got:'
value.
2026-05-15 17:15:07 +03:00
Oleks 0e20e87361 chore(gitea-local-fork): bump to v1.26.0-unstable-2026-05-15
ci/woodpecker/push/arm64 Pipeline failed
ci/woodpecker/push/amd64 Pipeline failed
Picks up oleks/gitea#2 (user-scope and org-scope project board REST
endpoints under /api/v1/users/{username}/projects/... and
/api/v1/orgs/{org}/projects/...) plus the missing MoveProjectIssue
test for repo scope.
2026-05-15 16:38:54 +03:00
Oleks 2af7732077 style: auto-format from pre-push hooks
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
2026-05-15 13:02:47 +03:00
Oleks 7d58ed23a4 metamcp: package v2.4.22 as a NixOS-deployable service
Single-derivation pnpm/Turbo monorepo build producing:
  - metamcp           full orchestrator (waits for PG, runs drizzle
                      migrations, launches backend :12009 + frontend :12008)
  - metamcp-backend   bare backend launcher
  - metamcp-frontend  Next.js standalone server.js launcher

Notes:
  - Upstream pins packageManager: pnpm@9.0.0; rewrite to match the nixpkgs
    pnpm (Turbo requires the field but won't fight a matching version).
  - Frontend uses next.config 'output: standalone'; we copy .next/static
    and public/ into the standalone tree since Next doesn't.
  - HOSTNAME defaults to 0.0.0.0 (override with METAMCP_HOSTNAME) — Next
    standalone otherwise inherits the system hostname and is unreachable
    on 127.0.0.1.
2026-05-15 13:02:47 +03:00
Oleks cf54f1c94f ci: build gitea-local-fork and push to attic
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
Add the renamed gitea-local-fork derivation to the Woodpecker
build matrix on x86_64 and aarch64 (the only platforms the
derivation supports — see flake.nix). Resulting closure is
pushed to attic-infra-cache-k3s-1 so subsequent
`just gitea-run` invocations resolve from cache rather than
recompile Go 1.26.3 locally.
2026-05-13 13:39:46 +03:00
Oleks ef014a9116 chore: rename gitea-projects-api → gitea-local-fork
The fork tracked by this derivation is no longer a single-feature
branch ("feat/projects-api") but the integration tip of Oleks's
local gitea fork ($HOME/projects/gitea, branch oleks/main), carrying
upstream/main + PR #37518 Projects REST API + a CI gate + fork-local
commits. Reflect the broader scope in the package and attribute name,
and document the local fork path + branch in the derivation header.
2026-05-13 13:39:39 +03:00
Oleks bbf81d6ac5 ci: split workflow per arch to fix cross-arch PVC scheduling
ci/woodpecker/push/arm64 Pipeline was successful
ci/woodpecker/push/amd64 Pipeline was successful
A single-workflow with steps spanning amd64 and arm64 shares one
PVC. The clone step binds the PV to whichever arch ran first, and
the other arch's pod is then permanently Unschedulable (node
affinity mismatch on the PV). Splitting into separate workflow
files gives each arch its own clone, its own PVC, and its own node
binding.
2026-05-12 21:27:34 +03:00
Oleks d5101ea3fe gitea-projects-api: refresh to 1.26.0-unstable-2026-05-12
ci/woodpecker/push/woodpecker Pipeline failed
Pin to gitea fork rev 235248620e05 (tag v1.26.0-unstable-2026-05-12).
Refreshed by nix-update via passthru.updateScript.
2026-05-12 16:21:54 +03:00
Oleks 3dda8347e4 style: auto-format from pre-push hooks
ci/woodpecker/push/woodpecker Pipeline was successful
2026-05-12 00:02:47 +03:00
Oleks 949f0d2153 gitea-projects-api: refresh to 1.26.0-unstable-2026-05-11
Pin to gitea fork rev 20f31b8967a4 (tag v1.26.0-unstable-2026-05-11).
Refreshed by nix-update via passthru.updateScript.
2026-05-12 00:02:46 +03:00
Oleks 45c8bc03e8 gitea-projects-api: support nix-update, add justfile targets
Refactor so `src` is a direct `fetchgit` (instead of a wrapper drv), and
move the package.json engine-strip into a `frontendSrc` derivation that
only fetchPnpmDeps sees. nix-update needs to introspect `src.url` and
`src.rev`; the previous wrapper hid them.

Expose `frontend.pnpmDeps` via passthru so nix-update finds the third
hash. Now `just gitea-update` does the full cycle:
  1. git ls-remote → latest commit on feat/projects-api
  2. set src.hash / pnpmDeps.hash / goModules.vendorHash to fakeHash
  3. nix-build each to capture real hashes
  4. nom build the package for final verification

nix-update rewrites version to nixpkgs `<tag>-unstable-<date>` style. Lose
the descriptive "-projects-api" suffix in the version, but pname is
unchanged so store paths still read `gitea-projects-api-*`.
2026-05-11 22:25:05 +03:00
Oleks c462cee781 gitea-projects-api: package fork carrying upstream PR #37518
Builds oleks/gitea feat/projects-api (Gitea 1.27.0-dev + Projects REST API)
as `nix build .#gitea-projects-api`. Exposes `out` (binary) and `data`
(templates, options, frontend bundle, locale files) matching the layout
nixpkgs' `services.gitea` module expects.

Notes:
- Pins Go 1.26.3 (built from upstream src) because the fork's go.mod
  requires it, while pinned nixpkgs only has 1.26.0.
- Patches package.json to drop engines.pnpm before fetchPnpmDeps runs:
  gitea wants pnpm >= 11, but nixpkgs only packages pnpm 10. The
  pnpm-lock.yaml is v9 (forward-compatible) so pnpm 10 produces the
  same install closure.
- Platforms: x86_64-linux, aarch64-linux (skipped on s390x cross since
  the frontend pnpm step has no s390x toolchain).
2026-05-11 20:32:36 +03:00
Oleks 80c05b582e ci: make env probes non-fatal, swap free for /proc/meminfo
ci/woodpecker/push/woodpecker Pipeline was successful
Pipeline #41 died with exit 127 on `free -h` — procps isn't in the
nix-ci image. New info() helper runs the command and ignores the exit
code, so missing tools no longer abort the build. Also switched to
/proc/meminfo since it's always available on Linux.
2026-04-29 11:44:50 +03:00
Oleks d95501ed01 style: auto-format from pre-push hooks
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-29 11:38:24 +03:00
Oleks 65101ed034 ci: skip google-antigravity builds
google-antigravity pulls in google-chrome, which transitively builds
liberation-fonts; fontforge segfaults while generating the .ttf files
(pipeline #40). Package definitions stay in the flake for local
builds — re-enable in CI once upstream fontforge is fixed.
2026-04-29 11:38:22 +03:00
Oleks d971b72ba4 ci: stream build logs and add env context
setup.sh now traces each command (set -ex) so /etc/hosts, nix.conf,
and netrc setup are visible in pipeline logs.

build.py replaces capture() with a streaming build() helper for
nix builds: stderr is inherited (live --print-build-logs output)
while stdout is captured for the out path. Also dumps nix version,
uname, disk, and memory at the start so failures have context.
2026-04-29 11:16:57 +03:00
Oleks 44972326e9 ci: drop s390x build, rename steps to amd64/arm64
ci/woodpecker/push/woodpecker Pipeline failed
s390x had no dedicated builder and ran as a cross-compile pinned to
amd64 via nodeSelector, colliding with the x86_64-linux step on the
same node — Woodpecker's k8s backend couldn't create the per-step
secret twice and the workflow failed with either "secrets already
exists" or "Canceled". Disable until a real s390x builder is wired up.

Step names now match the kubernetes.io/arch label they target; the
ci/build.py argument keeps the Nix system tuple (x86_64-linux,
aarch64-linux).
2026-04-29 10:49:55 +03:00
Oleks df96ba5c89 feat: add xontrib-direnv (xonsh-direnv 1.6.5)
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-26 16:51:13 +03:00
Oleks e3e8c6f5e3 feat: add xontribs, hyprspace, and gcc15-fixes overlay
ci/woodpecker/push/woodpecker Pipeline failed
- packages/xontribs.nix: xontrib-prompt-starship, -broot, -term-integrations
  wheels for use with `programs.xonsh.extraPackages` (or xonsh.override)
- packages/hyprspace.nix + hyprspace flake input (flake=false): rebuild
  plugin against the consumer's hyprland; exposed via overlays.hyprspace
- overlays/gcc15-fixes.nix: hotdoc/kitty/libsecret/xdg-desktop-portal/afdko
  workarounds so fleet nodes on the same pin can opt in with one line
- flake.nix: lift overlays out of eachSystem to the root (overlays.default
  was previously nested per-system, which doesn't match flake schema)
2026-04-23 23:25:03 +03:00
Oleks 4e2bb71ed5 feat: add xontribs, hyprspace, and gcc15-fixes overlay
ci/woodpecker/push/woodpecker Pipeline failed
- packages/xontribs.nix: xontrib-prompt-starship, -broot, -term-integrations
  wheels for use with `programs.xonsh.extraPackages` (or xonsh.override)
- packages/hyprspace.nix + hyprspace flake input (flake=false): rebuild
  plugin against the consumer's hyprland; exposed via overlays.hyprspace
- overlays/gcc15-fixes.nix: hotdoc/kitty/libsecret/xdg-desktop-portal/afdko
  workarounds so fleet nodes on the same pin can opt in with one line
- flake.nix: lift overlays out of eachSystem to the root (overlays.default
  was previously nested per-system, which doesn't match flake schema)
2026-04-23 20:18:35 +03:00
Oleks 7599dbd981 flake.lock: bump fleet-pins for Nix 2.31 narHash
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-23 00:52:42 +03:00
Oleks 21684819ec ci: fix pod labels — use tag/branch instead of ref (slashes invalid)
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 18:46:04 +02:00
Oleks 3c4fcfc3ed fix: also remove_dir_all in symlink_file for directory-type conflicts
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 18:42:54 +02:00
Oleks 0af380cd9a ci: add commit-ref and pipeline-number pod labels for log tracing
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 17:46:06 +02:00
Oleks 9a4b7eaa11 style: auto-format from pre-push hooks
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 14:39:37 +02:00
Oleks 0d46839c04 fix: override both rustc-unwrapped and rustc wrapper for symlink_file patch to propagate 2026-03-16 14:39:36 +02:00
Oleks 4b5e5eec2d fix: override rustc-unwrapped (not wrapper) for symlink_file patch to actually apply
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 14:32:36 +02:00
Oleks cc57704fe0 fix: use double-quoted sed to avoid Nix string escaping issue in rustc patch
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 13:36:29 +02:00
Oleks cfa685cf91 fix: patch symlink_file method body for rustc 1.93 s390x cross-compile
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 00:21:11 +02:00
Oleks 8e628dad9c feat: export mold for s390x cross-compilation
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-16 00:07:54 +02:00
Oleks ce394ed7b8 style: auto-format from pre-push hooks
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-15 20:19:54 +02:00
Oleks cb45365285 feat: export rustc, cargo, rustfmt, sccache for s390x
Adds rustc bootstrap patch to fix symlink_file "File exists" panic
during s390x cross-compilation (lib.rs:1823). Removes existing
destination before creating symlink.
2026-03-15 20:19:53 +02:00
oleks 7da15f25ff docs: add README for flake-hub
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-15 18:51:16 +02:00
Oleks 80fb557b73 fix: export nix 2.28 (with s390x overlays) instead of nix 2.31 (broken)
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-15 16:35:49 +02:00
Oleks bf0191b68e feat: export nix package for s390x cross-compilation
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-15 15:51:12 +02:00
Oleks b319fe443b style: auto-format from pre-push hooks
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-15 14:51:21 +02:00
Oleks 547d804a81 try: rewrite CI script as Python — xonsh .xsh parser can't handle dict/list literals 2026-03-15 14:51:20 +02:00
Oleks f81e3807a3 try: use dict.get instead of ternary to avoid xonsh parser bug
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-15 14:41:15 +02:00
Oleks 5fe57dc74c try: fix xonsh syntax error — inline conditional to avoid if-without-else parsing bug
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-15 14:27:46 +02:00
Oleks 4a50fe3f31 ci: limit clone to shallow depth without tags
ci/woodpecker/push/woodpecker Pipeline failed
Adds PLUGIN_TAGS=false and PLUGIN_DEPTH=1 to plugin-git clone steps
to avoid fetching all tags on every CI run.
2026-03-15 13:44:03 +02:00
Oleks 2828abdfd4 try: remove stale .woodpecker.yaml that shadowed .woodpecker.yml
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-15 13:41:07 +02:00
Oleks 1bc3afe912 try: remove when filter to debug missing push pipelines 2026-03-15 13:30:17 +02:00
Oleks 486dee4b30 try: trigger CI with repaired webhook 2026-03-15 13:28:24 +02:00
Oleks 67098eb3b7 try: re-trigger CI after webhook repair 2026-03-15 13:27:59 +02:00
Oleks 42380de028 try: trigger CI for attic-client + geesefs build 2026-03-15 13:27:38 +02:00
Oleks 20740d8735 Add ruff.toml to exclude .xsh files from ruff linting
Ruff cannot parse xonsh syntax ($(), @()), so xonsh scripts
must be excluded via force-exclude.
2026-03-15 13:10:00 +02:00
Oleks 49fd58b363 Add attic-client (s390x) and geesefs to flake-hub, rewrite CI in xonsh
Move attic-client s390x cross-compilation from building/s390x/attic-client-s390x
and geesefs from building/s390x/geesefs-s390x into flake-hub. Replace ci/build.sh
with ci/build.xsh. All packages now built and pushed to attic via the existing
Woodpecker pipeline on push to main.
2026-03-15 13:09:05 +02:00
22 changed files with 2281 additions and 123 deletions
+2
View File
@@ -0,0 +1,2 @@
result
result-*
-18
View File
@@ -1,18 +0,0 @@
when:
- event: tag
ref: "refs/tags/v*"
steps:
- name: build
image: git.oleks.space/oleks/nix-ci:latest
environment:
GITEA_CLONE_TOKEN:
from_secret: gitea_clone_token
commands:
- nixos-ci-entrypoint true
- nix build .#xonsh --no-link --print-out-paths
- nix build .#hello-world --no-link --print-out-paths
backend_options:
kubernetes:
nodeSelector:
kubernetes.io/hostname: howard2404
-64
View File
@@ -1,64 +0,0 @@
# Build flake-hub packages for all architectures and push to attic
clone:
- name: clone
image: woodpeckerci/plugin-git
environment:
CI_NETRC_MACHINE: git.oleks.space
CI_NETRC_USERNAME: oleks
CI_NETRC_PASSWORD:
from_secret: gitea_clone_token
when:
- event: push
branch: main
# Build packages for each architecture in parallel
steps:
- name: build-x86_64-linux
image: git.oleks.space/oleks/nix-ci:latest
depends_on: []
environment:
ATTIC_TOKEN:
from_secret: attic_token
GITEA_CLONE_TOKEN:
from_secret: gitea_clone_token
backend_options:
kubernetes:
nodeSelector:
kubernetes.io/arch: amd64
commands:
- sh ci/setup.sh
- sh ci/build.sh x86_64-linux
- name: build-aarch64-linux
image: git.oleks.space/oleks/nix-ci:latest
depends_on: []
environment:
ATTIC_TOKEN:
from_secret: attic_token
GITEA_CLONE_TOKEN:
from_secret: gitea_clone_token
backend_options:
kubernetes:
nodeSelector:
kubernetes.io/arch: arm64
commands:
- sh ci/setup.sh
- sh ci/build.sh aarch64-linux
- name: build-s390x-linux
image: git.oleks.space/oleks/nix-ci:latest
depends_on: []
environment:
ATTIC_TOKEN:
from_secret: attic_token
GITEA_CLONE_TOKEN:
from_secret: gitea_clone_token
backend_options:
kubernetes:
nodeSelector:
kubernetes.io/arch: amd64
commands:
- sh ci/setup.sh
- sh ci/build.sh s390x-linux
+44
View File
@@ -0,0 +1,44 @@
# Build flake-hub packages for x86_64-linux and push to attic.
# Separate workflow per arch — sharing one PVC across arches makes
# the second pod permanently Unschedulable (PV node affinity binds
# to the first arch's node).
when:
- event: push
branch: main
clone:
- name: clone
image: woodpeckerci/plugin-git
environment:
CI_NETRC_MACHINE: git.oleks.space
CI_NETRC_USERNAME: oleks
CI_NETRC_PASSWORD:
from_secret: gitea_clone_token
PLUGIN_TAGS: "false"
PLUGIN_DEPTH: "1"
steps:
- name: build-amd64
image: git.oleks.space/oleks/nix-ci:latest
environment:
ATTIC_TOKEN:
from_secret: attic_token
GITEA_CLONE_TOKEN:
from_secret: gitea_clone_token
backend_options:
kubernetes:
nodeSelector:
kubernetes.io/arch: amd64
labels:
commit-tag: "${CI_COMMIT_TAG}"
commit-branch: "${CI_COMMIT_BRANCH}"
pipeline-number: "${CI_PIPELINE_NUMBER}"
commands:
- echo "▸ arch=$(uname -m)"
- sh ci/setup.sh
# Same front door as a local `nix run .#publish -- --publish`. The app
# is parity-lib's mkAtticClosurePublish (attic-closure archetype); CI and
# local share one audited impl. PUBLISH=1 makes it actually push (local
# runs dry-run).
- PUBLISH=1 nix run .#publish
+42
View File
@@ -0,0 +1,42 @@
# Build flake-hub packages for aarch64-linux and push to attic.
# Separate workflow per arch — see amd64.yaml for rationale.
when:
- event: push
branch: main
clone:
- name: clone
image: woodpeckerci/plugin-git
environment:
CI_NETRC_MACHINE: git.oleks.space
CI_NETRC_USERNAME: oleks
CI_NETRC_PASSWORD:
from_secret: gitea_clone_token
PLUGIN_TAGS: "false"
PLUGIN_DEPTH: "1"
steps:
- name: build-arm64
image: git.oleks.space/oleks/nix-ci:latest
environment:
ATTIC_TOKEN:
from_secret: attic_token
GITEA_CLONE_TOKEN:
from_secret: gitea_clone_token
backend_options:
kubernetes:
nodeSelector:
kubernetes.io/arch: arm64
labels:
commit-tag: "${CI_COMMIT_TAG}"
commit-branch: "${CI_COMMIT_BRANCH}"
pipeline-number: "${CI_PIPELINE_NUMBER}"
commands:
- echo "▸ arch=$(uname -m)"
- sh ci/setup.sh
# NODE-BOUND LEG (emmett#44, cluster#192): aarch64-linux can't be built on
# emmett (linux/amd64) and these native packages have no cross path, so this
# leg must run on an aarch64 node (the arch nodeSelector above). Same
# parity-lib attic-closure front door as amd64, only the arch app differs.
- PUBLISH=1 nix run .#publish-aarch64-linux
+39
View File
@@ -0,0 +1,39 @@
# flake-hub
Personal Nix flake hub — custom packages and overlays
## Overview
Personal Nix flake hub for publishing custom packages and overlays.
Provides packages for x86_64-linux and aarch64-linux.
## Build
```bash
nix build .#<package-name>
```
## Antigravity
`google-antigravity` (FHS-wrapped) and `google-antigravity-no-fhs` are
re-exposed from
[jacopone/antigravity-nix](https://github.com/jacopone/antigravity-nix)
(MIT). Upstream auto-updates daily via its own GitHub Actions; we just bump
the flake input and CI caches the build to attic.
When the in-app "new version available" banner appears (or you just want to
refresh the pin):
```bash
just antigravity-update # nix flake update antigravity-nix
git commit -am "antigravity: bump"
git push # CI rebuilds and pushes to attic
```
Then, on the consumer:
```bash
cd ~/projects/servers/emmett
nix flake update oleks-nixpkgs
nix run .#deploy
```
-22
View File
@@ -1,22 +0,0 @@
#!/bin/sh
# Build all flake-hub packages and push to attic
set -e
ARCH="$1"
ATTIC_CACHE="attic-infra-cache-k3s-1"
ATTIC_SERVER="https://nix-cache-upload.oleks.space"
echo "=== Building flake-hub packages for ${ARCH} ==="
# Setup attic
attic=$(nix build --inputs-from . nixpkgs#attic-client --print-out-paths --no-link)/bin/attic
"${attic}" login ci "${ATTIC_SERVER}" "${ATTIC_TOKEN}"
echo "Building packages..."
out=$(nix build ".#packages.${ARCH}.hello-world" --print-build-logs --print-out-paths --no-link)
"${attic}" push "${ATTIC_CACHE}" "${out}"
out=$(nix build ".#packages.${ARCH}.xonsh" --print-build-logs --print-out-paths --no-link)
"${attic}" push "${ATTIC_CACHE}" "${out}"
echo "✅ Build completed for ${ARCH}"
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/sh
# Bootstrap nix environment for CI (runs inside nixos/nix:latest)
set -e
set -ex
# Direct to armer public IP — bypass Cloudflare upload size limits
# Hairpin NAT on armer handles redirect for pods running on armer itself
+174
View File
@@ -0,0 +1,174 @@
# Publishing `gitea-local-fork`
End-to-end flow for taking a change in Oleks's local gitea fork
(`/home/oleks/projects/gitea`, branch `oleks/main`) all the way to a
binary in the `attic-infra-cache-k3s-1` cache, ready for any consumer
to fetch instead of recompiling Go 1.26.3 locally.
## Step-by-step
### 1. Develop in the gitea fork
```bash
cd ~/projects/gitea
# work on oleks/main (the fork integration tip)
git commit -m "..."
git push oleks oleks/main:main
```
Lands on `git.oleks.space/oleks/gitea` at `refs/heads/main`.
This step alone does **not** trigger any CI on flake-hub — the binding
contract is the flake-hub pin commit in step 2.
### 2. Bump the pin in flake-hub
```bash
cd ~/projects/nix-customs/flake-hub
just gitea-update
```
Under the hood, `scripts/update-gitea-local-fork.sh`:
1. **Wipes stale unstable tags**`git push oleks :refs/tags/v*-unstable-*`
so `nix-update` sees only clean semver tags (`v1.26.0` etc.) when
constructing the new version.
2. **Bumps the pin**
```bash
nix run nixpkgs#nix-update -- \
--flake gitea-local-fork \
--version=branch=main \
--build
```
Updates `rev`, `vendorHash`, `pnpmDeps` hash, and `version` (formatted
as `<latest-semver-tag>-unstable-<commit-date>`).
3. **Tags the gitea fork** at the new rev with `v<version>` and pushes
the tag. **This is the load-bearing step:** the tag makes the rev
reachable forever, surviving any future rebase / force-push of
`oleks/main`.
4. **Commits + pushes flake-hub** — the bumped `packages/gitea-local-fork.nix`
lands on `git.oleks.space/oleks/flake-hub` `main`.
### 3. Woodpecker picks up the flake-hub push
Pipeline auto-triggers from `.woodpecker/amd64.yaml` and `.woodpecker/arm64.yaml`
running in parallel. Each workflow:
1. Clones `oleks/flake-hub` using the `gitea_clone_token` secret.
2. Runs `ci/setup.sh` — configures `/etc/hosts` to pin `armer`
directly (bypassing Cloudflare upload-size limits via hairpin NAT)
and writes the trusted substituters / public keys to `/etc/nix/nix.conf`.
3. Runs `ci/build.py <arch>` — for each package (including
`gitea-local-fork` on `x86_64-linux` and `aarch64-linux`):
```bash
nix build .#packages.<arch>.gitea-local-fork --print-build-logs ...
attic push attic-infra-cache-k3s-1 <closure>
```
Authenticates the push with `ATTIC_TOKEN` against
`https://nix-cache-upload.oleks.space`.
### 4. Closure lives in attic
After both arch workflows succeed, the closure is queryable from:
- **Cache URL:** `https://nix-cache-custom.oleks.space/attic-infra-cache-k3s-1`
- **Public key:** `attic-infra-cache-k3s-1:qYSNK3DmttQXCFqn1t50qoWGtQNPRFWq9mgQjD05DeU=`
### 5. Consume from cache
Anyone (or any host) can pull the binary instead of building:
<!-- markdownlint-disable MD013 -->
```bash
just gitea-run
# or, equivalently:
nix run \
--extra-substituters "https://nix-cache-custom.oleks.space/attic-infra-cache-k3s-1" \
--extra-trusted-public-keys "attic-infra-cache-k3s-1:qYSNK3DmttQXCFqn1t50qoWGtQNPRFWq9mgQjD05DeU=" \
git+https://git.oleks.space/oleks/flake-hub#gitea-local-fork
```
<!-- markdownlint-enable MD013 -->
## Diagram
```text
┌────────────────────────────────────────────────────────────────────────┐
│ 1. ~/projects/gitea (branch oleks/main) │
│ ─ git push oleks oleks/main:main ─► oleks/gitea refs/heads/main │
└────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ 2. ~/projects/nix-customs/flake-hub │
│ ─ just gitea-update │
│ a. wipe v*-unstable-* tags on fork │
│ b. nix-update → bump rev/hashes/version │
│ c. tag fork at new rev: v<version> ─► push │
│ d. commit + push flake-hub origin/main │
└────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ 3. ci.oleks.space pipeline auto-fires │
│ amd64 + arm64 (parallel): │
│ clone → ci/setup.sh → ci/build.py <arch> │
│ ─ nix build .#packages.<arch>.gitea-local-fork │
│ ─ attic push attic-infra-cache-k3s-1 <closure> │
└────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ 4. attic-infra-cache-k3s-1 │
│ URL: https://nix-cache-custom.oleks.space/attic-infra-cache-k3s-1 │
└────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ 5. Consumer: just gitea-run / nix run …#gitea-local-fork │
│ ─ closure pulled from cache, no recompile │
└────────────────────────────────────────────────────────────────────────┘
```
## Secrets
Both are Woodpecker repo secrets on `oleks/flake-hub` (never in code):
- `gitea_clone_token` — clone flake-hub source; netrc for `fetchgit` of
the fork.
- `attic_token` — authenticate `attic push` against
`nix-cache-upload.oleks.space`.
## Why the dance with tags in step 2c
The fork's `oleks/main` branch is rebaseable. `fetchgit` in the
derivation resolves by `rev`, so the rev must remain reachable from
*some* ref on the remote. Tags are immutable. The
`v<version>` tag created at update time becomes the load-bearing
reference: even if `oleks/main` later gets force-pushed and the rev
falls off the branch, the tag keeps it alive (and Gitea won't GC it).
## Failure points to watch
- **Step 2c — tag push rejected.** Check `oleks` remote auth in your
shell; the pre-push hook also runs lint.
- **Step 3 build — first cold build ≈1015 min/arch, later ≈12 min.**
Normal; Go 1.26.3 compiles from source.
- **Step 3 attic — push hangs or 413.** `ci/setup.sh` pins armer in
`/etc/hosts` to bypass the Cloudflare 100 MB upload cap; confirm the
pin survived.
- **Step 5 consume — cache miss, recompile locally.** Check the closure
landed (`attic info` or browse the cache URL) and that the public key
matches.
## Reference
- Derivation: `packages/gitea-local-fork.nix`
- Flake attr: `flake.nix` → `packages.<system>.gitea-local-fork`
- Update script: `scripts/update-gitea-local-fork.sh` *(untracked, local-only)*
- Pipelines: `.woodpecker/amd64.yaml`, `.woodpecker/arm64.yaml`
- Build driver: `ci/build.py`
- Justfile recipes: `just gitea-build`, `just gitea-update`, `just gitea-run`
Generated
+517 -9
View File
@@ -1,5 +1,26 @@
{
"nodes": {
"antigravity-nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780561919,
"narHash": "sha256-joj1ZN1q3dBtaPuYXvvFuXJzKt1r8g5FwsO9wgIOhkI=",
"owner": "jacopone",
"repo": "antigravity-nix",
"rev": "dafe38f5bf01ff5e4e105b18d2be9e3d754b6345",
"type": "github"
},
"original": {
"owner": "jacopone",
"repo": "antigravity-nix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@@ -18,6 +39,159 @@
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_4": {
"inputs": {
"systems": "systems_4"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_5": {
"inputs": {
"systems": "systems_5"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_6": {
"inputs": {
"systems": "systems_6"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"fleet": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"nixpkgs-armer": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-bim": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-ci": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-emmett": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-howard": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-mermaid": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-mermaid-gpu": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-micron": [
"parity",
"fleet",
"nixpkgs"
],
"nixpkgs-projects": [
"parity",
"fleet",
"nixpkgs"
]
},
"locked": {
"lastModified": 1780368078,
"narHash": "sha256-tLzA5XveUF4PfuKNz3KuhmVhuME3PX5zvtFa17hhQPU=",
"ref": "refs/heads/main",
"rev": "1626405d46ff3595b91c9e2d3ed9399f67c18b83",
"revCount": 15,
"type": "git",
"url": "https://git.oleks.space/oleks/fleet-pins"
},
"original": {
"type": "git",
"url": "https://git.oleks.space/oleks/fleet-pins"
}
},
"fleet-pins": {
"inputs": {
"nixpkgs": "nixpkgs",
@@ -45,6 +219,10 @@
"fleet-pins",
"nixpkgs"
],
"nixpkgs-mermaid-gpu": [
"fleet-pins",
"nixpkgs"
],
"nixpkgs-micron": [
"fleet-pins",
"nixpkgs"
@@ -55,11 +233,11 @@
]
},
"locked": {
"lastModified": 1772635961,
"narHash": "sha256-4uQsEqeDCbZp4g5Qk4lb7PDBYcFykHr57FynIc4m/a0=",
"lastModified": 1780368078,
"narHash": "sha256-tLzA5XveUF4PfuKNz3KuhmVhuME3PX5zvtFa17hhQPU=",
"ref": "main",
"rev": "513059f6a1b4cea423e600a142a1e6fe8b8e3e4f",
"revCount": 7,
"rev": "1626405d46ff3595b91c9e2d3ed9399f67c18b83",
"revCount": 15,
"type": "git",
"url": "https://git.oleks.space/oleks/fleet-pins"
},
@@ -69,30 +247,262 @@
"url": "https://git.oleks.space/oleks/fleet-pins"
}
},
"fleet_2": {
"inputs": {
"nixpkgs": "nixpkgs_3",
"nixpkgs-armer": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-bim": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-ci": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-emmett": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-howard": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-mermaid": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-mermaid-gpu": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-micron": [
"woodpecker-peek",
"fleet",
"nixpkgs"
],
"nixpkgs-projects": [
"woodpecker-peek",
"fleet",
"nixpkgs"
]
},
"locked": {
"lastModified": 1780368078,
"narHash": "sha256-tLzA5XveUF4PfuKNz3KuhmVhuME3PX5zvtFa17hhQPU=",
"ref": "refs/heads/main",
"rev": "1626405d46ff3595b91c9e2d3ed9399f67c18b83",
"revCount": 15,
"type": "git",
"url": "https://git.oleks.space/oleks/fleet-pins"
},
"original": {
"type": "git",
"url": "https://git.oleks.space/oleks/fleet-pins"
}
},
"hyprspace": {
"flake": false,
"locked": {
"lastModified": 1764820995,
"narHash": "sha256-IMa4mvkF0w7OAy+yEzPFYs2an332K30lf5qfUOAS9Cw=",
"owner": "KZDKM",
"repo": "Hyprspace",
"rev": "0467be86b18cfc324fab04afbd40fe9ef80f7fa9",
"type": "github"
},
"original": {
"owner": "KZDKM",
"repo": "Hyprspace",
"rev": "0467be86b18cfc324fab04afbd40fe9ef80f7fa9",
"type": "github"
}
},
"mcp-chrome": {
"inputs": {
"flake-utils": "flake-utils_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1779817034,
"narHash": "sha256-uRiqIPmTGeTCeoQtCz+CuC2NXNEv8xoP696HgYrA8fU=",
"ref": "main",
"rev": "8b5ec6cedfed0d46eff8ee4f6bf3912fd33acc22",
"revCount": 210,
"type": "git",
"url": "https://git.oleks.space/oleks/mcp-chrome"
},
"original": {
"ref": "main",
"type": "git",
"url": "https://git.oleks.space/oleks/mcp-chrome"
}
},
"nix-deps": {
"inputs": {
"flake-utils": "flake-utils_4",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1778863463,
"narHash": "sha256-TynOVZBZHiBZAjZbW2sQfaVEh5UXi4UgZ3X0pcu4nYE=",
"owner": "manelinux",
"repo": "nix-deps",
"rev": "5fdd0979c6254969eeb34ff49da2fa73c02972f3",
"type": "github"
},
"original": {
"owner": "manelinux",
"repo": "nix-deps",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1771848320,
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
}
},
"parity": {
"inputs": {
"flake-utils": "flake-utils_5",
"fleet": "fleet",
"nixpkgs": [
"parity",
"fleet",
"nixpkgs-ci"
]
},
"locked": {
"lastModified": 1780602315,
"narHash": "sha256-msVp9pBMP+I/n0+GGADG1XQembM3F19fUIh79rxqkgs=",
"ref": "refs/heads/main",
"rev": "089bd03264d848f8391d9d53d79da0fd97ed1857",
"revCount": 13,
"type": "git",
"url": "https://git.oleks.space/oleks/parity-lib"
},
"original": {
"type": "git",
"url": "https://git.oleks.space/oleks/parity-lib"
}
},
"parity_2": {
"inputs": {
"flake-utils": "flake-utils_6",
"fleet": [
"woodpecker-peek",
"fleet"
],
"nixpkgs": [
"woodpecker-peek",
"nixpkgs"
]
},
"locked": {
"lastModified": 1780472546,
"narHash": "sha256-cx+821qtyNZNe9t3ab8qImmeg/rxVzPpZIS45SflrI0=",
"ref": "refs/heads/main",
"rev": "d265a79ddb84b297364e6cc3638c9f6b5dc583d7",
"revCount": 10,
"type": "git",
"url": "https://git.oleks.space/oleks/parity-lib"
},
"original": {
"type": "git",
"url": "https://git.oleks.space/oleks/parity-lib"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"antigravity-nix": "antigravity-nix",
"flake-utils": "flake-utils_2",
"fleet-pins": "fleet-pins",
"hyprspace": "hyprspace",
"mcp-chrome": "mcp-chrome",
"nix-deps": "nix-deps",
"nixpkgs": [
"fleet-pins",
"nixpkgs-projects"
],
"parity": "parity",
"stalewood": "stalewood",
"woodpecker-peek": "woodpecker-peek"
}
},
"stalewood": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1779473107,
"narHash": "sha256-PjX2So2b4xPuXZCkgTb0eXnV9wIELQ7D0EPlDWD0NBw=",
"owner": "retif",
"repo": "stalewood",
"rev": "415f3de6c23f662dabceaf0cba540a289c609174",
"type": "github"
},
"original": {
"owner": "retif",
"repo": "stalewood",
"type": "github"
}
},
"systems": {
@@ -109,6 +519,104 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_6": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"woodpecker-peek": {
"inputs": {
"fleet": "fleet_2",
"nixpkgs": [
"nixpkgs"
],
"parity": "parity_2"
},
"locked": {
"lastModified": 1780743249,
"narHash": "sha256-qVrrS69mzq3Fo8+pFtFukDogAl+2qQl5ib/7DJi1oRw=",
"ref": "main",
"rev": "8db46bec6c1dfed1c411df6b139d60c87d739696",
"revCount": 31,
"type": "git",
"url": "https://git.oleks.space/oleks/woodpecker-peek"
},
"original": {
"ref": "main",
"type": "git",
"url": "https://git.oleks.space/oleks/woodpecker-peek"
}
}
},
"root": "root",
+341 -9
View File
@@ -5,6 +5,57 @@
fleet-pins.url = "git+https://git.oleks.space/oleks/fleet-pins?ref=main";
nixpkgs.follows = "fleet-pins/nixpkgs-projects";
flake-utils.url = "github:numtide/flake-utils";
# Shared per-archetype parity publish-app builders (cluster#104, emmett#44).
# flake-hub is an ATTIC-CLOSURE repo: it builds package closures and warms
# the Attic cache with them — there is NO registry artifact.
# mkAtticClosurePublish makes that archetype explicit to pipeline-doctor.
parity.url = "git+https://git.oleks.space/oleks/parity-lib";
# Hyprspace source; no flake.nix in the repo so we consume it as raw src.
# Pin tracks the last v0.52-compatible commit of Hyprspace.
hyprspace = {
url = "github:KZDKM/Hyprspace/0467be86b18cfc324fab04afbd40fe9ef80f7fa9";
flake = false;
};
# Google Antigravity packaging. Upstream auto-updates daily; we re-expose
# the overlay and build into our attic cache so emmett pulls from there.
antigravity-nix = {
url = "github:jacopone/antigravity-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# nix-deps: "see the real cost of installing packages on NixOS".
# Re-exposed through our overlay so CI builds it into attic.
nix-deps = {
url = "github:manelinux/nix-deps";
inputs.nixpkgs.follows = "nixpkgs";
};
# stalewood — find/reap merged git worktrees. Ships its own flake;
# re-expose its package (mirrors the nix-deps pattern).
stalewood = {
url = "github:retif/stalewood";
inputs.nixpkgs.follows = "nixpkgs";
};
# woodpecker-peek — tray app for Woodpecker CI (on git.oleks.space).
# Re-exposed so flake-hub CI warms attic and emmett pulls cached.
woodpecker-peek = {
url = "git+https://git.oleks.space/oleks/woodpecker-peek?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
# mcp-chrome — Chrome MCP server + extension (English-localized fork of
# hangwin/mcp-chrome). Re-exposes the from-source wasm-simd worker (proven
# green, ~22 s) and the full chrome-mcp-extension build. The extension
# target is KNOWN-BROKEN under nix-daemon at this pin (see oleks/mcp-chrome
# issue #1 close comment); expect attic to miss it until that's resolved.
mcp-chrome = {
url = "git+https://git.oleks.space/oleks/mcp-chrome?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
@@ -13,6 +64,13 @@
nixpkgs,
fleet-pins,
flake-utils,
parity,
hyprspace,
antigravity-nix,
nix-deps,
stalewood,
woodpecker-peek,
mcp-chrome,
...
}:
let
@@ -27,15 +85,171 @@
"s390x-linux" = "s390x-linux";
};
mkPackages = pkgs: {
hello-world = pkgs.callPackage ./packages/hello-world.nix { };
xonsh = pkgs.callPackage ./packages/xonsh.nix {
xonsh-unwrapped = import ./packages/xonsh-unwrapped.nix {
inherit (pkgs) lib python3Packages fetchFromGitHub;
mkPackages =
pkgs:
let
sys = pkgs.stdenv.hostPlatform.system;
# Antigravity ships a Google-provided x86_64/aarch64 Linux binary.
# Skip it for cross targets (e.g. s390x) where it can't run anyway.
supportsAntigravity = sys == "x86_64-linux" || sys == "aarch64-linux";
xontribs = import ./packages/xontribs.nix {
inherit (pkgs) python3Packages fetchurl;
};
in
{
hello-world = pkgs.callPackage ./packages/hello-world.nix { };
geesefs = pkgs.callPackage ./packages/geesefs.nix { };
metamcp = pkgs.callPackage ./packages/metamcp.nix { };
xonsh = pkgs.callPackage ./packages/xonsh.nix {
xonsh-unwrapped = import ./packages/xonsh-unwrapped.nix {
inherit (pkgs) lib python3Packages fetchFromGitHub;
};
};
}
# Native-only packages — skip on s390x cross (gitea's pnpm step and
# cgo+sqlite link don't cross-compile cleanly).
// nixpkgs.lib.optionalAttrs (sys == "x86_64-linux" || sys == "aarch64-linux") {
gitea-local-fork =
let
# Our fork's go.mod requires Go 1.26.3; nixpkgs at this pin has
# only 1.26.0 (and unstable has 1.26.2). Bump the package's src.
go = pkgs.go_1_26.overrideAttrs (_: rec {
version = "1.26.3";
src = pkgs.fetchurl {
url = "https://go.dev/dl/go${version}.src.tar.gz";
hash = "sha256-HGRoddCqh5kTMYTtV895/yS97+jIggRwYCqdPW2Rkrg=";
};
});
in
pkgs.callPackage ./packages/gitea-local-fork.nix {
buildGoModule = pkgs.buildGoModule.override { inherit go; };
};
}
# Xontribs: pass into `programs.xonsh.extraPackages` or
# `pkgs.xonsh.override { extraPackages = ps: [...]; }`.
// xontribs
# Antigravity: re-expose jacopone/antigravity-nix's outputs so emmett
# consumes a single flake-hub input and our CI builds into attic.
// nixpkgs.lib.optionalAttrs supportsAntigravity {
inherit (antigravity-nix.packages.${sys})
google-antigravity
google-antigravity-no-fhs
google-antigravity-ide
google-antigravity-ide-no-fhs
google-antigravity-cli
;
# nix-deps' flake only outputs eachDefaultSystem (no s390x), so
# gate it on the same native x86_64/aarch64 condition.
nix-deps = nix-deps.packages.${sys}.default;
# stalewood — re-exposed from its own flake. No s390x output,
# so it rides the same native-only gate.
stalewood = stalewood.packages.${sys}.default;
# woodpecker-peek — same pattern; consumers (emmett) read the
# attic-cached binary via flake-hub's overlay, then set
# services.woodpecker-peek.package = pkgs.woodpecker-peek;.
woodpecker-peek = woodpecker-peek.packages.${sys}.default;
# mcp-chrome — Rust→wasm worker (proven green) plus the full
# chrome-mcp-extension build. The latter is KNOWN-BROKEN under
# nix-daemon at this pin; flake-hub CI will miss the cache on it
# until upstream resolves that. wasm-simd alone is what consumers
# actually pull cached today (commits 9534234, b276465 in
# oleks/mcp-chrome sync the built wasm back into the tree).
mcp-chrome-wasm-simd = mcp-chrome.packages.${sys}.wasm-simd;
mcp-chrome-extension = mcp-chrome.packages.${sys}.chrome-mcp-extension;
};
# Overlay providing Hyprspace. Requires `pkgs.hyprland` to be present
# (consumer applies the Hyprland flake's overlay first). Kept out of
# `mkPackages` because standalone `nix build .#hyprspace` has no
# Hyprland in scope.
hyprspaceOverlay = final: _prev: {
hyprspace = final.callPackage ./packages/hyprspace.nix {
src = hyprspace;
};
};
# Rustc bootstrap: symlink_file panics with "File exists" during
# s390x cross-compilation. Multiple call sites use t!(symlink_file(...)).
# Patch the symlink_file method body to remove an existing dest first.
# Exported as overlays.s390xRustcSymlink so nixos-ci consumes this single
# definition (and the patch travels with it) instead of duplicating it.
s390xRustcSymlinkOverlay =
final: prev:
let
patchedRustcUnwrapped = prev.rustc-unwrapped.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
./patches/rustc-symlink-file-eexist.patch
];
});
in
{
rustc-unwrapped = patchedRustcUnwrapped;
rustc = prev.rustc.override {
rustc-unwrapped = patchedRustcUnwrapped;
};
};
# Overlays needed for s390x cross-compilation of attic-client
s390xOverlays = [
# OpenSSL s390x assembly uses z10 instructions (cijne) that the
# nix-bootstrapped assembler doesn't recognize
(final: prev: {
openssl = prev.openssl.overrideAttrs (old: {
configureFlags = (old.configureFlags or [ ]) ++ [ "no-asm" ];
});
})
# musl doesn't support s390x long double (IBM double-double format:
# LDBL_MANT_DIG=106, sizeof=16). Make musl appear unavailable so
# busybox-sandbox-shell uses glibc static instead.
(final: prev: {
busybox-sandbox-shell = prev.busybox-sandbox-shell.override {
musl = prev.musl // {
meta = prev.musl.meta // {
platforms = [ ];
};
};
};
})
# libarchive tests fail in k8s pod sandboxes
(final: prev: {
libarchive = prev.libarchive.overrideAttrs (_: {
doCheck = false;
});
})
# nix 2.28 is a direct dependency of attic-client — disable tests
# and fix missing RPATH for boost/zstd/libarchive
(final: prev: {
nixVersions = prev.nixVersions // {
nix_2_28 = prev.nixVersions.nix_2_28.overrideAttrs (old: {
doCheck = false;
doInstallCheck = false;
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.patchelf ];
postFixup = (old.postFixup or "") + ''
for rpath in ${final.boost}/lib ${final.zstd.out}/lib ${final.libarchive.out}/lib; do
patchelf --add-rpath "$rpath" $out/bin/nix
for lib in $out/lib/lib*.so; do
[ -f "$lib" ] && patchelf --add-rpath "$rpath" "$lib"
done
done
'';
});
};
})
# LLVM test failures in CI pod environment — override libllvm
# (not llvm) so it propagates through rustc bootstrap chain
(final: prev: {
llvmPackages = prev.llvmPackages // {
libllvm = prev.llvmPackages.libllvm.overrideAttrs (old: {
doCheck = false;
doInstallCheck = false;
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ prev.gitMinimal ];
});
};
})
# Rustc symlink_file "File exists" fix (defined + exported above).
s390xRustcSymlinkOverlay
];
# Native builds
native = flake-utils.lib.eachSystem buildSystems (
system:
@@ -48,7 +262,7 @@
default = packages.hello-world;
};
overlays.default = final: prev: mkPackages final;
formatter = pkgs.nixfmt-rfc-style;
devShells.default = pkgs.mkShell {
name = "oleks-hub-shell";
@@ -69,6 +283,11 @@
builtins.map (
target:
let
targetOverlays =
{
"s390x-linux" = s390xOverlays;
}
.${target} or [ ];
pkgs = import nixpkgs {
system = "x86_64-linux";
crossSystem.config =
@@ -78,14 +297,33 @@
}
.${target}
}.config;
overlays = targetOverlays;
};
packages = mkPackages pkgs;
crossOnlyPackages =
{
"s390x-linux" = {
inherit (pkgs)
attic-client
rustc
cargo
rustfmt
sccache
mold
;
nix = pkgs.nixVersions.nix_2_28;
};
}
.${target} or { };
in
{
name = target;
value = packages // {
default = packages.hello-world;
};
value =
packages
// crossOnlyPackages
// {
default = packages.hello-world;
};
}
) (builtins.attrNames crossTargets)
);
@@ -93,5 +331,99 @@
native
// {
packages = (native.packages or { }) // cross;
overlays = {
default = final: _prev: mkPackages final;
gcc15-fixes = import ./overlays/gcc15-fixes.nix;
hyprspace = hyprspaceOverlay;
# Single home for the s390x rustc symlink_file patch overlay so
# consumers (nixos-ci) don't re-declare it + the patch file.
s390xRustcSymlink = s390xRustcSymlinkOverlay;
};
# `nix run .#<app>` — local-parity entrypoints (emmett#44, cluster#192,
# ATTIC-CLOSURE archetype, cluster#104). The stage/publish/push-staged
# apps come straight from parity-lib's mkAtticClosurePublish, so CI and a
# local run share one audited implementation and cannot drift. There is NO
# registry artifact — these apps build the package closures and warm the
# Attic cache with them.
#
# TWO HALVES (emmett#44): STAGE `nix build`s every package in the arch's
# list into the local /nix store (cluster-independent, runs on emmett);
# PUBLISH additionally `attic push`es each closure to the cache that lives
# next to the cluster. Local runs DRY-RUN (stage + show the pushes) unless
# `--publish`/PUBLISH=1.
#
# nix run .#stage-x86_64-linux stage amd64 closures, no publish
# nix run .#publish stage amd64 then push if PUBLISH=1
# nix run .#publish -- --publish actually push to attic
# nix run .#push-staged replay already-staged amd64 paths
#
# arm64 leg: aarch64-linux cannot be built on emmett (linux/amd64) and the
# native packages have no cross path, so it MUST run on an aarch64 node
# (.woodpecker/arm64.yaml runs `PUBLISH=1 nix run .#publish-aarch64-linux`).
#
# The package set per arch mirrors flake-hub's CI warm list (was
# ci/publish.py packages_for): the always-on core plus arch-conditional
# extras. Cross-only/known-broken targets stay out (see comments).
apps =
let
# The Attic cache token lives at `pass infra/attic/ci_token` and the
# cache is reached via the armer hairpin endpoint — preserve both so
# the push is byte-for-byte the pre-parity behaviour.
atticEndpoint = "https://nix-cache-upload.oleks.space";
atticPass = "infra/attic/ci_token";
# Package names to warm into Attic for a given native arch.
# Native arches only (amd64/arm64); mcp-chrome-extension stays OUT
# (known-broken under nix-daemon at this pin, see oleks/mcp-chrome #1).
packageNamesFor =
arch:
[
"hello-world"
"geesefs"
"xonsh"
]
++ nixpkgs.lib.optionals (arch == "x86_64-linux" || arch == "aarch64-linux") [
"woodpecker-peek"
"mcp-chrome-wasm-simd"
"gitea-local-fork"
"google-antigravity"
"google-antigravity-no-fhs"
"google-antigravity-ide"
"google-antigravity-ide-no-fhs"
"google-antigravity-cli"
];
drvsFor = arch: map (n: self.packages.${arch}.${n}) (packageNamesFor arch);
buildersFor = arch: parity.lib.mkParityBuilders (import nixpkgs { system = arch; });
atticAppsFor =
arch:
(buildersFor arch).mkAtticClosurePublish {
drvs = drvsFor arch;
inherit arch;
endpoint = atticEndpoint;
passEntry = atticPass;
};
amd64Apps = atticAppsFor "x86_64-linux";
arm64Apps = atticAppsFor "aarch64-linux";
in
nixpkgs.lib.genAttrs buildSystems (system:
# amd64 is the emmett-buildable arch: expose its stage/publish/
# push-staged plus the top-level `publish`. Also surface the arm64
# stage/publish under their arch-suffixed names for the node-bound
# CI leg (.woodpecker/arm64.yaml).
{
inherit (amd64Apps)
"stage-x86_64-linux"
"publish-x86_64-linux"
"publish"
"push-staged"
;
"stage-aarch64-linux" = arm64Apps."stage-aarch64-linux";
"publish-aarch64-linux" = arm64Apps."publish-aarch64-linux";
});
};
}
+50
View File
@@ -0,0 +1,50 @@
default:
@just --list
# ── Antigravity ────────────────────────────────────────────
# Run antigravity (FHS-wrapped) from the attic cache
antigravity-run:
nix run \
--extra-substituters "https://nix-cache-custom.oleks.space/attic-infra-cache-k3s-1" \
--extra-trusted-public-keys "attic-infra-cache-k3s-1:qYSNK3DmttQXCFqn1t50qoWGtQNPRFWq9mgQjD05DeU=" \
git+https://git.oleks.space/oleks/flake-hub#google-antigravity
# Run antigravity (non-FHS variant) from the attic cache
antigravity-run-no-fhs:
nix run \
--extra-substituters "https://nix-cache-custom.oleks.space/attic-infra-cache-k3s-1" \
--extra-trusted-public-keys "attic-infra-cache-k3s-1:qYSNK3DmttQXCFqn1t50qoWGtQNPRFWq9mgQjD05DeU=" \
git+https://git.oleks.space/oleks/flake-hub#google-antigravity-no-fhs
# Build antigravity (FHS-wrapped)
antigravity-build:
nix build .#google-antigravity --print-build-logs
# Build antigravity (non-FHS variant)
antigravity-build-no-fhs:
nix build .#google-antigravity-no-fhs --print-build-logs
# Pull the latest jacopone/antigravity-nix pin
antigravity-update:
nix flake update antigravity-nix
# ── gitea-local-fork ───────────────────────────────────────
# Builds Oleks's local gitea fork (/home/oleks/projects/gitea, branch oleks/main).
# Build the gitea fork (compiles Go 1.26.3 first time; ~5-8 min cold)
gitea-build:
nix build .#gitea-local-fork --print-build-logs
# Refresh gitea-local-fork: bump rev/hashes/version, retag the gitea fork, push both repos
gitea-update:
nix run nixpkgs#nix-update -- \
--flake gitea-local-fork \
--use-update-script
# Run gitea from the attic-cached build (no local recompile)
gitea-run:
nix run \
--extra-substituters "https://nix-cache-custom.oleks.space/attic-infra-cache-k3s-1" \
--extra-trusted-public-keys "attic-infra-cache-k3s-1:qYSNK3DmttQXCFqn1t50qoWGtQNPRFWq9mgQjD05DeU=" \
git+https://git.oleks.space/oleks/flake-hub#gitea-local-fork -- --help
+43
View File
@@ -0,0 +1,43 @@
# Narrow fixes for packages that fail under the fleet's pinned nixpkgs
# with GCC 15 / current upstream breakage. Remove entries as upstream fixes
# land so consumers can drop them on the next pin bump.
final: prev: {
# hotdoc's bundled cmark declares `project(cmark VERSION 0.28.3)` without
# LANGUAGES, which makes CMake probe CXX; GCC 15 fails the probe. Adding
# LANGUAGES C skips the CXX check.
hotdoc = prev.hotdoc.overrideAttrs (old: {
postPatch = (old.postPatch or "") + ''
substituteInPlace cmark/CMakeLists.txt \
--replace-fail "project(cmark VERSION 0.28.3)" "project(cmark VERSION 0.28.3 LANGUAGES C)"
'';
});
# kitty's bundled base64 lib has incompatible pointer types that GCC 15
# promotes from warning to error.
kitty = prev.kitty.overrideAttrs (old: {
env = (old.env or { }) // {
NIX_CFLAGS_COMPILE = (old.env.NIX_CFLAGS_COMPILE or "") + " -Wno-error=incompatible-pointer-types";
};
});
# Skip D-Bus-dependent checks that fail under remote-builder sandboxes.
libsecret = prev.libsecret.overrideAttrs (_: {
doCheck = false;
});
xdg-desktop-portal = prev.xdg-desktop-portal.overrideAttrs (_: {
doCheck = false;
});
# afdko has two pre-existing test failures that block
# nototools → noto-fonts-color-emoji.
pythonPackagesExtensions = (prev.pythonPackagesExtensions or [ ]) ++ [
(pyFinal: pyPrev: {
afdko = pyPrev.afdko.overrideAttrs (old: {
disabledTests = (old.disabledTests or [ ]) ++ [
"test_non_varying_glyphs_bug356"
"test_ufo_contentsplist_parsing"
];
});
})
];
}
+29
View File
@@ -0,0 +1,29 @@
{
lib,
buildGoModule,
fetchFromGitHub,
}:
buildGoModule rec {
pname = "geesefs";
version = "0.43.5";
src = fetchFromGitHub {
owner = "yandex-cloud";
repo = "geesefs";
rev = "v${version}";
hash = "sha256-cfeL7fnxS+UFUlRVLiO09GHuEOvkiH5PkKcoH+jNRhY=";
};
proxyVendor = true;
vendorHash = "sha256-p+shpYrPxYLXpW6A4a/5qM90KH+pcMCqZOPoYTE77f0=";
subPackages = [ "." ];
meta = {
description = "FUSE FS implementation over S3";
homepage = "https://github.com/yandex-cloud/geesefs";
license = [ lib.licenses.mit ];
platforms = lib.platforms.unix;
};
}
+169
View File
@@ -0,0 +1,169 @@
{
lib,
buildGoModule,
fetchgit,
makeWrapper,
git,
bash,
coreutils,
gzip,
nodejs,
openssh,
fetchPnpmDeps,
pnpmConfigHook,
pnpm,
stdenv,
sqliteSupport ? true,
}:
# Custom Gitea built from Oleks's local fork.
#
# Fork repo (local working copy): /home/oleks/projects/gitea
# Fork remote: https://git.oleks.space/oleks/gitea.git
# Tracked branch: oleks/main (the fork's integration tip;
# formerly `feat/projects-api`, renamed
# 2026-05-13. Carries upstream/main +
# PR #37518 Projects REST API + a few
# fork-local commits.)
# Pin discipline: nix-update bumps `rev` and tags the fork
# at `v<version>` (see passthru.updateScript
# below) so the rev stays reachable even
# after future force-pushes / rebases.
#
# Mirrors the structure of nixpkgs' gitea derivation (frontend pnpm sub-drv
# + buildGoModule with go:embed of public/) but with our fork as src.
let
pname = "gitea-local-fork";
version = "1.26.0-unstable-2026-05-17.1";
src = fetchgit {
url = "https://git.oleks.space/oleks/gitea.git";
rev = "c45ea82fd1408bdb7170308080e0eb0ffdea85b2";
hash = "sha256-AZKXcweNi6JfEgpfuj+agBNLWrQ50fTpxUalNv3XC9A=";
};
# Gitea's package.json requires pnpm >= 11; nixpkgs ships pnpm 10. The
# lockfile is v9 (forward-compatible), so strip the engine constraint and
# the packageManager preference before any pnpm tool sees the source.
# We only patch the source the frontend sees; the Go build doesn't care.
frontendSrc = stdenv.mkDerivation {
pname = "${pname}-frontend-src";
inherit version src;
dontConfigure = true;
dontBuild = true;
installPhase = ''
cp -R . $out
chmod -R +w $out
${nodejs}/bin/node -e '
const fs = require("fs");
const path = process.argv[1];
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
if (pkg.engines) delete pkg.engines.pnpm;
delete pkg.packageManager;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
' "$out/package.json"
'';
};
frontend = stdenv.mkDerivation (finalAttrs: {
pname = "${pname}-frontend";
inherit version;
src = frontendSrc;
pnpmDeps = fetchPnpmDeps {
inherit (finalAttrs) pname version src;
fetcherVersion = 3;
hash = "sha256-TisyRChMRFm5GgdW2wXPHS7NIPu04SSd97oVEgKJ2dI=";
};
nativeBuildInputs = [
nodejs
pnpmConfigHook
pnpm
];
buildPhase = ''
make frontend
'';
installPhase = ''
mkdir -p $out
cp -R public $out/
'';
});
in
buildGoModule {
inherit pname version src;
proxyVendor = true;
vendorHash = "sha256-TxLmmeMinazGmIx94iQvpsncWKXb+42ddZcxbwayVgk=";
outputs = [
"out"
"data"
];
subPackages = [ "." ];
nativeBuildInputs = [ makeWrapper ];
tags = lib.optionals sqliteSupport [
"sqlite"
"sqlite_unlock_notify"
];
ldflags = [
"-s"
"-w"
"-X main.Version=${version}"
"-X 'main.Tags=${
lib.concatStringsSep " " (
lib.optionals sqliteSupport [
"sqlite"
"sqlite_unlock_notify"
]
)
}'"
];
postInstall = ''
mkdir $data
ln -s ${frontend}/public $data/public
cp -R ./{templates,options} $data
mkdir -p $out
cp -R ./options/locale $out/locale
wrapProgram $out/bin/gitea \
--prefix PATH : ${
lib.makeBinPath [
bash
coreutils
git
gzip
openssh
]
}
'';
passthru = {
# Expose hashes nix-update needs to find when iterating.
inherit (frontend) pnpmDeps;
# Custom update flow: refresh hashes + retag the gitea fork + push.
# Invoke with `nix-update --use-update-script gitea-local-fork` or
# `just gitea-update` (which passes `--use-update-script`).
updateScript = ../scripts/update-gitea-local-fork.sh;
};
meta = {
description = "Gitea fork with Projects REST API (upstream PR #37518)";
homepage = "https://git.oleks.space/oleks/gitea";
license = lib.licenses.mit;
mainProgram = "gitea";
platforms = [
"x86_64-linux"
"aarch64-linux"
];
};
}
+22
View File
@@ -0,0 +1,22 @@
# Rebuilds the Hyprspace plugin against the consumer's Hyprland. Requires
# `pkgs.hyprland` to already be in scope (consumers apply the upstream
# Hyprland flake overlay first). `src` comes from the hyprspace flake
# input pinned in ../flake.nix.
{
gcc14Stdenv,
hyprland,
src,
}:
gcc14Stdenv.mkDerivation {
pname = "Hyprspace";
version = "unstable-${src.shortRev or "dirty"}";
inherit src;
inherit (hyprland) nativeBuildInputs;
buildInputs = [ hyprland ] ++ hyprland.buildInputs;
dontUseCmakeConfigure = true;
installFlags = [ "PREFIX=$(out)" ];
postInstall = ''
mv $out/lib/Hyprspace.so $out/lib/libHyprspace.so
'';
}
+441
View File
@@ -0,0 +1,441 @@
diff -urN metamcp-2.4.22.orig/apps/backend/src/index.ts metamcp-2.4.22/apps/backend/src/index.ts
--- a/apps/backend/src/index.ts 2026-05-23 23:45:36.313040968 +0300
+++ b/apps/backend/src/index.ts 2026-05-23 23:50:43.286804066 +0300
@@ -1,6 +1,14 @@
import express from "express";
import { auth } from "./auth";
+import {
+ mcpCancellationTotal,
+ mcpHopDurationSeconds,
+ mcpTraceparentSynthesizedTotal,
+ mcpUncaughtThrowTotal,
+ renderMetricsExposition,
+} from "./lib/observability/metrics";
+import { bindFromHeader, HOP_NAME, traceLog, traceStore } from "./lib/observability/trace";
import { initializeIdleServers } from "./lib/startup";
import mcpProxyRouter from "./routers/mcp-proxy";
import oauthRouter from "./routers/oauth";
@@ -9,6 +17,42 @@
const app = express();
+// cluster#49 — bind W3C trace context for every request and stamp every
+// downstream log via traceStore.run(). cluster#50 — observe wall time
+// per hop and count cancellations.
+app.use((req, res, next) => {
+ const raw = req.headers.traceparent as string | undefined;
+ const { ctx, event } = bindFromHeader(raw);
+ if (event === "traceparent_synthesized") {
+ mcpTraceparentSynthesizedTotal.inc({ hop: HOP_NAME });
+ }
+ const t0 = process.hrtime.bigint();
+ let status: "success" | "error" | "cancelled" = "success";
+ res.on("close", () => {
+ // res.writableEnded is true when we finished writing; false here
+ // means the downstream client hung up mid-response.
+ if (!res.writableEnded) {
+ status = "cancelled";
+ mcpCancellationTotal.inc({ hop: HOP_NAME, source: "downstream" });
+ }
+ const dt = Number(process.hrtime.bigint() - t0) / 1e9;
+ mcpHopDurationSeconds.observe(
+ { hop: HOP_NAME, tool_name: "", status },
+ dt,
+ );
+ });
+ traceStore.run(ctx, () => {
+ if (event) traceLog("info", `event=${event}`);
+ next();
+ });
+});
+
+// cluster#50 — Prometheus exposition. Schema in Specs/mcp-metric-schema.
+app.get("/metrics", (_req, res) => {
+ res.set("Content-Type", "text/plain; version=0.0.4");
+ res.send(renderMetricsExposition());
+});
+
// Global JSON middleware for non-proxy routes
app.use((req, res, next) => {
if (req.path.startsWith("/mcp-proxy/") || req.path.startsWith("/metamcp/")) {
@@ -108,3 +152,39 @@
status: "ok",
});
});
+
+// cluster#50 — Express error tail. Any unhandled error in middleware /
+// handlers funnels here; we count it so the uncaught-throw alert fires.
+app.use(
+ (
+ err: unknown,
+ _req: express.Request,
+ res: express.Response,
+ next: express.NextFunction,
+ ) => {
+ mcpUncaughtThrowTotal.inc({ hop: HOP_NAME, component: "express" });
+ traceLog("error", "event=uncaught_throw", {
+ error: err instanceof Error ? err.message : String(err),
+ });
+ if (res.headersSent) return next(err);
+ res.status(500).json({ error: "internal error" });
+ },
+);
+
+// cluster#50 — last-resort process traps so unhandled rejections /
+// uncaught exceptions still feed the alert. Counting only; existing
+// behaviour (log + continue) is preserved.
+process.on("unhandledRejection", (err) => {
+ mcpUncaughtThrowTotal.inc({ hop: HOP_NAME, component: "unhandledRejection" });
+ traceLog("error", "event=uncaught_throw", {
+ component: "unhandledRejection",
+ error: err instanceof Error ? err.message : String(err),
+ });
+});
+process.on("uncaughtException", (err) => {
+ mcpUncaughtThrowTotal.inc({ hop: HOP_NAME, component: "uncaughtException" });
+ traceLog("error", "event=uncaught_throw", {
+ component: "uncaughtException",
+ error: err.message,
+ });
+});
diff -urN metamcp-2.4.22.orig/apps/backend/src/lib/observability/metrics.ts metamcp-2.4.22/apps/backend/src/lib/observability/metrics.ts
--- a/apps/backend/src/lib/observability/metrics.ts 1970-01-01 03:00:00.000000000 +0300
+++ b/apps/backend/src/lib/observability/metrics.ts 2026-05-23 23:49:42.080145070 +0300
@@ -0,0 +1,160 @@
+// Hand-rolled Prometheus metrics for the metamcp aggregator.
+//
+// We avoid adding `prom-client` to package.json so the pnpm lockfile and
+// the Nix `pnpmDeps` hash stay untouched (cluster#50). This module
+// emits the canonical schema from Specs/mcp-metric-schema:
+// mcp_hop_duration_seconds (histogram)
+// mcp_cancellation_total (counter)
+// mcp_uncaught_throw_total (counter)
+// mcp_traceparent_synthesized_total (counter)
+//
+// Exposition is text format v0.0.4. The /metrics endpoint is registered
+// in index.ts.
+
+type LabelSet = Record<string, string>;
+
+function escapeLabel(v: string): string {
+ return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
+}
+
+function renderLabels(labels: LabelSet): string {
+ const parts: string[] = [];
+ for (const k of Object.keys(labels).sort()) {
+ parts.push(`${k}="${escapeLabel(labels[k])}"`);
+ }
+ return parts.length ? `{${parts.join(",")}}` : "";
+}
+
+function labelKey(labels: LabelSet): string {
+ // Stable key for the labels map. Sort keys so the order is deterministic.
+ const ks = Object.keys(labels).sort();
+ return ks.map((k) => `${k}=${labels[k]}`).join("\x1f");
+}
+
+export class Counter {
+ private values = new Map<string, { labels: LabelSet; value: number }>();
+
+ constructor(
+ public readonly name: string,
+ public readonly help: string,
+ public readonly labelNames: readonly string[],
+ ) {}
+
+ inc(labels: LabelSet, by: number = 1): void {
+ const key = labelKey(labels);
+ const cur = this.values.get(key);
+ if (cur) {
+ cur.value += by;
+ } else {
+ this.values.set(key, { labels: { ...labels }, value: by });
+ }
+ }
+
+ render(): string {
+ const lines: string[] = [];
+ lines.push(`# HELP ${this.name} ${this.help}`);
+ lines.push(`# TYPE ${this.name} counter`);
+ for (const { labels, value } of this.values.values()) {
+ lines.push(`${this.name}${renderLabels(labels)} ${value}`);
+ }
+ return lines.join("\n");
+ }
+}
+
+export class Histogram {
+ private series = new Map<
+ string,
+ {
+ labels: LabelSet;
+ bucketCounts: number[]; // counts per bucket boundary (cumulative on render)
+ sum: number;
+ count: number;
+ }
+ >();
+
+ constructor(
+ public readonly name: string,
+ public readonly help: string,
+ public readonly labelNames: readonly string[],
+ public readonly buckets: readonly number[],
+ ) {}
+
+ observe(labels: LabelSet, value: number): void {
+ const key = labelKey(labels);
+ let s = this.series.get(key);
+ if (!s) {
+ s = {
+ labels: { ...labels },
+ bucketCounts: new Array(this.buckets.length).fill(0),
+ sum: 0,
+ count: 0,
+ };
+ this.series.set(key, s);
+ }
+ for (let i = 0; i < this.buckets.length; i++) {
+ if (value <= this.buckets[i]) s.bucketCounts[i] += 1;
+ }
+ s.sum += value;
+ s.count += 1;
+ }
+
+ render(): string {
+ const lines: string[] = [];
+ lines.push(`# HELP ${this.name} ${this.help}`);
+ lines.push(`# TYPE ${this.name} histogram`);
+ for (const s of this.series.values()) {
+ for (let i = 0; i < this.buckets.length; i++) {
+ const le = String(this.buckets[i]);
+ const lbls = renderLabels({ ...s.labels, le });
+ lines.push(`${this.name}_bucket${lbls} ${s.bucketCounts[i]}`);
+ }
+ const lblsInf = renderLabels({ ...s.labels, le: "+Inf" });
+ lines.push(`${this.name}_bucket${lblsInf} ${s.count}`);
+ lines.push(`${this.name}_sum${renderLabels(s.labels)} ${s.sum}`);
+ lines.push(`${this.name}_count${renderLabels(s.labels)} ${s.count}`);
+ }
+ return lines.join("\n");
+ }
+}
+
+// --- Schema instances --------------------------------------------------------
+
+const DURATION_BUCKETS = [
+ 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300,
+];
+
+export const mcpHopDurationSeconds = new Histogram(
+ "mcp_hop_duration_seconds",
+ "Wall time spent in this hop processing one MCP request",
+ ["hop", "tool_name", "status"],
+ DURATION_BUCKETS,
+);
+
+export const mcpCancellationTotal = new Counter(
+ "mcp_cancellation_total",
+ "Cancellations observed per hop",
+ ["hop", "source"],
+);
+
+export const mcpUncaughtThrowTotal = new Counter(
+ "mcp_uncaught_throw_total",
+ "Uncaught exceptions per component (today's smoking gun, see cluster#44)",
+ ["hop", "component"],
+);
+
+export const mcpTraceparentSynthesizedTotal = new Counter(
+ "mcp_traceparent_synthesized_total",
+ "Inbound MCP requests missing a traceparent header",
+ ["hop"],
+);
+
+export function renderMetricsExposition(): string {
+ return (
+ [
+ mcpHopDurationSeconds.render(),
+ mcpCancellationTotal.render(),
+ mcpUncaughtThrowTotal.render(),
+ mcpTraceparentSynthesizedTotal.render(),
+ ].join("\n") + "\n"
+ );
+}
diff -urN metamcp-2.4.22.orig/apps/backend/src/lib/observability/trace.ts metamcp-2.4.22/apps/backend/src/lib/observability/trace.ts
--- a/apps/backend/src/lib/observability/trace.ts 1970-01-01 03:00:00.000000000 +0300
+++ b/apps/backend/src/lib/observability/trace.ts 2026-05-23 23:49:11.700817887 +0300
@@ -0,0 +1,127 @@
+// W3C Trace Context propagation for the metamcp aggregator.
+//
+// Implements the contract from cluster#45 (Specs/mcp-request-id):
+// traceparent header: 00-<trace-id:32-hex>-<span-id:16-hex>-<flags:2-hex>
+// Bound per-request via AsyncLocalStorage. Inbound HTTP header is read by
+// the express middleware in index.ts; outbound JSON-RPC to stdio children
+// reads currentTraceparentForUpstream() and injects it under
+// params._meta.traceparent (process-managed-transport.send).
+
+import { AsyncLocalStorage } from "node:async_hooks";
+import { randomBytes } from "node:crypto";
+
+export const HOP_NAME = "metamcp";
+
+export interface TraceCtx {
+ trace_id: string;
+ span_id: string;
+ parent_span_id: string;
+ hop: string;
+ tool_name: string;
+}
+
+const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
+
+export const traceStore = new AsyncLocalStorage<TraceCtx>();
+
+export function newTraceId(): string {
+ return randomBytes(16).toString("hex");
+}
+
+export function newSpanId(): string {
+ return randomBytes(8).toString("hex");
+}
+
+export type ParsedTraceparent = {
+ trace_id: string;
+ parent_span_id: string;
+ malformed: boolean;
+};
+
+export function parseTraceparent(raw: string | undefined): ParsedTraceparent {
+ if (typeof raw !== "string") {
+ return { trace_id: "", parent_span_id: "", malformed: false };
+ }
+ const m = TRACEPARENT_RE.exec(raw.trim());
+ if (!m) {
+ return { trace_id: newTraceId(), parent_span_id: "", malformed: true };
+ }
+ return { trace_id: m[1], parent_span_id: m[2], malformed: false };
+}
+
+export type BindResult = {
+ ctx: TraceCtx;
+ // One of: "" (normal), "traceparent_synthesized", "traceparent_malformed".
+ // Caller emits the reserved event log so the dashboard can count it.
+ event: "" | "traceparent_synthesized" | "traceparent_malformed";
+};
+
+export function bindFromHeader(
+ raw: string | undefined,
+ tool_name: string = "",
+): BindResult {
+ let event: BindResult["event"] = "";
+ let trace_id = "";
+ let parent_span_id = "";
+ if (raw == null) {
+ trace_id = newTraceId();
+ event = "traceparent_synthesized";
+ } else {
+ const p = parseTraceparent(raw);
+ trace_id = p.trace_id;
+ parent_span_id = p.parent_span_id;
+ if (p.malformed) event = "traceparent_malformed";
+ }
+ const ctx: TraceCtx = {
+ trace_id,
+ span_id: newSpanId(),
+ parent_span_id,
+ hop: HOP_NAME,
+ tool_name,
+ };
+ return { ctx, event };
+}
+
+// Build a traceparent literal for outbound calls. Our span-id becomes the
+// downstream hop's parent_span_id (standard W3C propagation). Falls back
+// to synthesizing a fresh context when called outside any bound request
+// (e.g. session-pool keepalive paths that originate inside the server).
+export function currentTraceparentForUpstream(): string {
+ let ctx = traceStore.getStore();
+ if (!ctx || !ctx.trace_id) {
+ ctx = {
+ trace_id: newTraceId(),
+ span_id: newSpanId(),
+ parent_span_id: "",
+ hop: HOP_NAME,
+ tool_name: "",
+ };
+ }
+ return `00-${ctx.trace_id}-${ctx.span_id}-01`;
+}
+
+// Single structured log line stamped with the current trace fields. Use
+// from any code path that has a bound trace context. Outside a context,
+// emits with empty fields so existing call sites do not crash.
+export function traceLog(
+ level: "info" | "warn" | "error",
+ msg: string,
+ extra: Record<string, unknown> = {},
+): void {
+ const ctx = traceStore.getStore();
+ const rec: Record<string, unknown> = {
+ ts: Date.now() / 1000,
+ level,
+ msg,
+ hop: HOP_NAME,
+ trace_id: ctx?.trace_id ?? "",
+ span_id: ctx?.span_id ?? "",
+ parent_span_id: ctx?.parent_span_id ?? "",
+ tool_name: ctx?.tool_name ?? "",
+ ...extra,
+ };
+ const line = JSON.stringify(rec);
+ if (level === "error") console.error(line);
+ else if (level === "warn") console.warn(line);
+ else console.log(line);
+}
diff -urN metamcp-2.4.22.orig/apps/backend/src/lib/stdio-transport/process-managed-transport.ts metamcp-2.4.22/apps/backend/src/lib/stdio-transport/process-managed-transport.ts
--- a/apps/backend/src/lib/stdio-transport/process-managed-transport.ts 2026-05-23 23:45:36.315112630 +0300
+++ b/apps/backend/src/lib/stdio-transport/process-managed-transport.ts 2026-05-23 23:51:05.898047455 +0300
@@ -6,6 +6,7 @@
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
import spawn from "cross-spawn";
+import { currentTraceparentForUpstream } from "../observability/trace";
import { ReadBuffer, serializeMessage } from "./shared";
export type StdioServerParameters = {
@@ -279,7 +280,32 @@
throw new Error("Not connected");
}
- const json = serializeMessage(message);
+ // cluster#49 — propagate W3C trace context to the stdio child via
+ // params._meta.traceparent (the MCP spec's standard escape hatch
+ // for transport-agnostic metadata). Only stamp on messages with
+ // params (notifications/requests); JSON-RPC responses ride back
+ // up so don't need it. Idempotent: existing _meta is preserved.
+ const tracedMessage = (() => {
+ const m = message as JSONRPCMessage & {
+ params?: Record<string, unknown>;
+ };
+ if (!m || typeof m !== "object" || !("params" in m)) return message;
+ if (m.params == null || typeof m.params !== "object") return message;
+ const meta = (m.params as Record<string, unknown>)._meta;
+ const traceparent = currentTraceparentForUpstream();
+ return {
+ ...m,
+ params: {
+ ...m.params,
+ _meta: {
+ ...(typeof meta === "object" && meta !== null ? meta : {}),
+ traceparent,
+ },
+ },
+ } as JSONRPCMessage;
+ })();
+
+ const json = serializeMessage(tracedMessage);
if (this._process.stdin.write(json)) {
resolve();
} else {
+209
View File
@@ -0,0 +1,209 @@
{
lib,
stdenv,
fetchFromGitHub,
makeWrapper,
nodejs_20,
pnpm_10,
fetchPnpmDeps,
pnpmConfigHook,
postgresql,
}:
# MetaMCP — MCP aggregator/orchestrator.
#
# Upstream is a pnpm monorepo (Turbo) with two apps:
# - apps/backend : Express/tRPC API, built with tsup -> dist/index.js
# port 12009 (internal), runs drizzle-kit migrations at boot
# - apps/frontend : Next.js 15, port 12008 (public), launched via `next start`
#
# Both require runtime node_modules (Next.js non-standalone; drizzle-kit is
# a devDep invoked at runtime by docker-entrypoint.sh). We ship the whole
# built tree under $out/lib/metamcp and provide launcher scripts.
#
# The upstream Dockerfile patches Next.js' proxy timeout (30s -> 600s) by
# sed-editing two files inside node_modules/.pnpm/next@.../...; we replicate
# that in postBuild so the behaviour matches the official image.
#
# Build is single-derivation: pnpm fetch -> pnpm build -> install. First
# build prints the right pnpmDeps hash; paste it back here and rebuild.
let
pname = "metamcp";
version = "2.4.22";
src = fetchFromGitHub {
owner = "metatool-ai";
repo = "metamcp";
rev = "v${version}";
hash = "sha256-EEb3RUjsaJ5ZSHSIkAxfdV/BAjZEAvw3rtjALM4RpSc=";
};
in
stdenv.mkDerivation (finalAttrs: {
inherit pname version src;
pnpmDeps = fetchPnpmDeps {
inherit pname version src;
fetcherVersion = 3;
hash = "sha256-nHHLLu5wBzzP4i/oTnOkuIiPQvvvBAIIVtKdfpDiXQw=";
};
nativeBuildInputs = [
nodejs_20
pnpm_10
pnpmConfigHook
makeWrapper
];
# Upstream pins `packageManager: pnpm@9.0.0`; nixpkgs ships pnpm 10. The
# lockfile is v9 (forward-compatible). Rewrite the pin to the pnpm we
# actually have so pnpm 10 doesn't try to network-fetch pnpm@9 and Turbo
# (which *requires* the field) still finds it.
postPatch = ''
${nodejs_20}/bin/node -e '
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
pkg.packageManager = "pnpm@${pnpm_10.version}";
if (pkg.engines) delete pkg.engines.pnpm;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
'
# cluster#49, cluster#50 observability patch: W3C traceparent
# propagation (HTTP header params._meta.traceparent on stdio
# children) + hand-rolled Prometheus metrics on /metrics, no new
# dependency so pnpmDeps stays untouched. Two new source files
# under apps/backend/src/lib/observability/ and minimal edits to
# apps/backend/src/index.ts and process-managed-transport.ts.
# Retire when this lands upstream.
patch -p1 < ${./metamcp-observability.patch}
'';
# pnpmConfigHook places node_modules; build the workspace with Turbo.
buildPhase = ''
runHook preBuild
# Match the upstream Dockerfile sed-patch on Next.js proxy timeout.
# Files live under the pnpm virtual store; glob to tolerate minor
# next/react version bumps on later tags.
for f in \
node_modules/.pnpm/next@*/node_modules/next/dist/server/lib/router-utils/proxy-request.js \
node_modules/.pnpm/next@*/node_modules/next/dist/esm/server/lib/router-utils/proxy-request.js; do
if [ -f "$f" ]; then
sed -i -e "s/30000/600000/" "$f"
fi
done
pnpm build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib/metamcp $out/bin
# Ship the whole built workspace. This is bulky but reliable:
# `next start` needs .next + node_modules + package.json side-by-side,
# and the backend's runtime invokes `pnpm exec drizzle-kit migrate`.
cp -r \
apps \
packages \
node_modules \
package.json \
pnpm-workspace.yaml \
pnpm-lock.yaml \
turbo.json \
$out/lib/metamcp/
# Next.js standalone post-processing: `output: "standalone"` produces
# apps/frontend/.next/standalone/apps/frontend/server.js but does NOT
# copy .next/static or public/ into the standalone tree. Do it here
# so the runtime UI has its CSS, JS chunks and static assets.
SA=$out/lib/metamcp/apps/frontend/.next/standalone/apps/frontend
cp -r $out/lib/metamcp/apps/frontend/.next/static $SA/.next/static
cp -r $out/lib/metamcp/apps/frontend/public $SA/public
# Launcher: re-implementation of upstream's docker-entrypoint.sh.
# Cleaner than substituteInPlace-ing the upstream script (the original
# has overlapping `/app` substrings that break naive replacement) and
# gives us a single place to keep the orchestration logic in sync.
cat > $out/bin/metamcp <<EOF
#!${stdenv.shell}
set -e
export PATH=${
lib.makeBinPath [
nodejs_20
pnpm_10
postgresql # for pg_isready
]
}:\$PATH
ROOT=$out/lib/metamcp
: "\''${POSTGRES_HOST:=127.0.0.1}"
: "\''${POSTGRES_PORT:=5432}"
: "\''${POSTGRES_USER:=postgres}"
echo "Waiting for PostgreSQL at \$POSTGRES_HOST:\$POSTGRES_PORT..."
until pg_isready -h "\$POSTGRES_HOST" -p "\$POSTGRES_PORT" -U "\$POSTGRES_USER"; do
sleep 2
done
echo "Running drizzle migrations..."
cd "\$ROOT/apps/backend"
pnpm exec drizzle-kit migrate
echo "Starting backend on :12009..."
PORT=12009 node "\$ROOT/apps/backend/dist/index.js" &
BACKEND_PID=\$!
sleep 3
kill -0 \$BACKEND_PID 2>/dev/null || { echo "Backend died"; exit 1; }
echo "Starting frontend on :12008..."
cd "\$ROOT/apps/frontend/.next/standalone/apps/frontend"
# Next.js standalone uses \$HOSTNAME as the bind address. The shell
# inherits HOSTNAME=<system-hostname>, leaving the server unreachable
# on 127.0.0.1 force 0.0.0.0 unless overridden via METAMCP_HOSTNAME.
PORT=12008 HOSTNAME="\''${METAMCP_HOSTNAME:-0.0.0.0}" node server.js &
FRONTEND_PID=\$!
sleep 3
kill -0 \$FRONTEND_PID 2>/dev/null || { kill \$BACKEND_PID 2>/dev/null; echo "Frontend died"; exit 1; }
trap 'kill \$BACKEND_PID \$FRONTEND_PID 2>/dev/null || true' TERM INT
wait \$BACKEND_PID \$FRONTEND_PID
EOF
chmod +x $out/bin/metamcp
# Direct sub-launchers (handy for systemd "two-unit" deployments if
# you ever want to skip the bundled orchestrator).
makeWrapper ${nodejs_20}/bin/node $out/bin/metamcp-backend \
--add-flags "$out/lib/metamcp/apps/backend/dist/index.js" \
--set NODE_ENV production
cat > $out/bin/metamcp-frontend <<EOF
#!${stdenv.shell}
cd $out/lib/metamcp/apps/frontend/.next/standalone/apps/frontend
exec env HOSTNAME="\''${METAMCP_HOSTNAME:-0.0.0.0}" ${nodejs_20}/bin/node server.js "\$@"
EOF
chmod +x $out/bin/metamcp-frontend
runHook postInstall
'';
# The pnpm/turbo build writes into $HOME; give it a writable one.
preBuild = ''
export HOME=$TMPDIR
'';
meta = {
description = "MetaMCP MCP aggregator, orchestrator, middleware, gateway";
homepage = "https://github.com/metatool-ai/metamcp";
license = lib.licenses.mit;
mainProgram = "metamcp";
platforms = [
"x86_64-linux"
"aarch64-linux"
];
};
})
+50
View File
@@ -0,0 +1,50 @@
{
python3Packages,
fetchurl,
}:
let
mkWheel =
{
pname,
version,
url,
sha256,
}:
python3Packages.buildPythonPackage {
inherit pname version;
src = fetchurl { inherit url sha256; };
format = "wheel";
doCheck = false;
dontCheckRuntimeDeps = true;
};
in
{
xontrib-prompt-starship = mkWheel {
pname = "xontrib-prompt-starship";
version = "0.3.7";
url = "https://files.pythonhosted.org/packages/94/71/968450a151d003bb80ed6fbfb190f22cfac076cbd4442dc58b48c5545644/xontrib_prompt_starship-0.3.7-py3-none-any.whl";
sha256 = "12a213fc454c9547c6426d19c4a43982be48500019378267bdab465d0749dccd";
};
xontrib-broot = mkWheel {
pname = "xontrib-broot";
version = "0.2.1";
url = "https://files.pythonhosted.org/packages/a8/97/a595ef322a40bcfaf885af8538d48cd0c506b8e06e3834458909c6b9bf90/xontrib_broot-0.2.1-py3-none-any.whl";
sha256 = "5eee2af0740fcc0355937b1365a15bcfebbe7c045215f0a597a519ce05cca96f";
};
xontrib-term-integrations = mkWheel {
pname = "xontrib-term-integrations";
version = "0.2.0";
url = "https://files.pythonhosted.org/packages/0e/df/76f0d98fb67267124f498b0da6b598939e85d5ef9aad49c3476929395ea0/xontrib_term_integrations-0.2.0-py3-none-any.whl";
sha256 = "cc55f4a1885361349d1a5a39aa6f44abc080e318355335bc14e7ea28c5b30776";
};
xontrib-direnv = mkWheel {
pname = "xonsh-direnv";
version = "1.6.5";
url = "https://files.pythonhosted.org/packages/f4/c3/47e5e0fa9db04f8d42272d8e1ffe9367e1643da7c24b944757fc1b18369a/xonsh_direnv-1.6.5-py3-none-any.whl";
sha256 = "e67e5c4f4328d9c8ee388476c83c1eb86d75d1b53b8595a4a6022641e7fb0214";
};
}
+11
View File
@@ -0,0 +1,11 @@
--- a/src/bootstrap/src/lib.rs
+++ b/src/bootstrap/src/lib.rs
@@ -1990,7 +1990,7 @@ impl Build {
use std::os::unix::fs::symlink as symlink_file;
#[cfg(windows)]
use std::os::windows::fs::symlink_file;
- if !self.config.dry_run() { symlink_file(src.as_ref(), link.as_ref()) } else { Ok(()) }
+ if !self.config.dry_run() { let _ = std::fs::remove_file(link.as_ref()); let _ = std::fs::remove_dir_all(link.as_ref()); symlink_file(src.as_ref(), link.as_ref()) } else { Ok(()) }
}
/// Returns if config.ninja is enabled, and checks for ninja existence,
+2
View File
@@ -0,0 +1,2 @@
force-exclude = true
extend-exclude = ["*.xsh"]
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Update flow for gitea-local-fork driven by passthru.updateScript.
# Invoked by `nix-update --use-update-script gitea-local-fork`.
#
# Source fork: /home/oleks/projects/gitea
# Source remote: https://git.oleks.space/oleks/gitea.git (named `oleks` locally)
# Tracked branch: oleks/main (formerly `feat/projects-api`, renamed 2026-05-13)
#
# nix-update with --version=branch=X constructs the new version as
# `<latest-tag>-unstable-<date>` where <latest-tag> is the most recent
# tag on the remote. If we leave an old `v1.26.0-unstable-<date>` tag
# from a previous run on the gitea fork, nix-update will pick that
# as <latest-tag> and we end up with double suffixes. So:
#
# 0. Delete any prior `*-unstable-*` tag on the gitea fork (local + remote).
# 1. Run nix-update default flow: it now sees only clean semver tags
# (v1.26.0 etc.), constructs `1.26.0-unstable-<commit-date>`.
# 2. Tag the gitea fork at the new rev with v<version>, push the tag.
# 3. Commit flake-hub's package.nix change, push to origin.
set -euo pipefail
FLAKE_HUB="${FLAKE_HUB:-$HOME/projects/nix-customs/flake-hub}"
GITEA_REPO="${GITEA_REPO:-$HOME/projects/gitea}"
PKG_FILE="$FLAKE_HUB/packages/gitea-local-fork.nix"
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
# ── 0. Wipe prior unstable tags from gitea fork ───────────────────────────
cd "$GITEA_REPO"
log "Cleaning up previous unstable tags on gitea fork (so nix-update sees only clean semver tags)"
mapfile -t PREV_TAGS < <(git ls-remote --tags oleks 'refs/tags/v*-unstable-*' |
awk '{print $2}' |
sed 's|refs/tags/||;s|\^{}$||' |
sort -u)
for tag in "${PREV_TAGS[@]}"; do
[ -z "$tag" ] && continue
log " deleting $tag"
git tag -d "$tag" 2>/dev/null || true
git push oleks ":refs/tags/$tag" 2>/dev/null || true
done
# ── 1. nix-update default flow ────────────────────────────────────────────
# Also normalize the in-file version to its bare semver baseline so
# nix-update doesn't see a stale unstable suffix from the local file.
cd "$FLAKE_HUB"
log "Normalizing in-file version baseline"
sed -i -E 's/(version = "[^"]+?)-unstable-[0-9]{4}-[0-9]{2}-[0-9]{2}"/\1"/' "$PKG_FILE"
log "Running nix-update against oleks/main tip on the gitea fork"
nix run nixpkgs#nix-update -- \
--flake gitea-local-fork \
--version=branch=main \
--build
# ── 2. Tag gitea fork ─────────────────────────────────────────────────────
NEW_REV=$(grep -oP 'rev = "\K[a-f0-9]{40}' "$PKG_FILE" | head -1)
NEW_VER=$(grep -oP 'version = "\K[^"]+' "$PKG_FILE" | head -1)
TAG="v${NEW_VER}"
log "New version: $NEW_VER (rev ${NEW_REV:0:12})"
cd "$GITEA_REPO"
if git rev-parse --verify --quiet "refs/tags/$TAG" >/dev/null; then
log "Tag $TAG already exists locally — skipping"
else
log "Tagging gitea fork: $TAG at ${NEW_REV:0:12}"
git -c commit.gpgsign=false tag -a "$TAG" "$NEW_REV" \
-m "Pinned by nix-customs/flake-hub gitea-local-fork derivation"
fi
log "Pushing tag to oleks remote"
git push oleks "$TAG" 2>/dev/null || log " tag already on remote"
# ── 3. Commit + push flake-hub changes ────────────────────────────────────
cd "$FLAKE_HUB"
if git diff --quiet -- "$PKG_FILE"; then
log "flake-hub: no changes to commit"
else
log "flake-hub: committing version + hashes bump"
git -c commit.gpgsign=false add "$PKG_FILE"
git -c commit.gpgsign=false commit -m "gitea-local-fork: refresh to $NEW_VER
Pin to gitea fork rev ${NEW_REV:0:12} (tag $TAG).
Refreshed by nix-update via passthru.updateScript."
fi
if git rev-parse '@{u}' >/dev/null 2>&1; then
log "flake-hub: pushing"
git push
else
log "flake-hub: no upstream tracking — skipping push"
fi
log "Done. New version $NEW_VER live at:"
log " gitea tag: https://git.oleks.space/oleks/gitea/src/tag/$TAG"
log " derivation: $PKG_FILE"