Skip to content

Commit c537abe

Browse files
authored
Merge pull request #28 from BotCoder254/feat/log-chunk-sections
Use Docker archive API for source/artifacts, anonymous volumes for /w…
2 parents 4886ddd + 02d1dc0 commit c537abe

6 files changed

Lines changed: 341 additions & 82 deletions

File tree

‎CLAUDE.md‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,16 @@ source. Runners (`runner/` crate) hold one outbound WS (`/runner/ws`, Bearer tok
8484
SHA-256-hashed registration token shown once; heartbeat cadence comes from `hello_ack`),
8585
connect to Docker via `connect_with_defaults` (local socket/pipe OR remote daemon via
8686
`DOCKER_HOST` + `DOCKER_TLS_VERIFY` + `DOCKER_CERT_PATH`, rustls), execute each job in a
87-
hardened container via bollard (temp workspace bind-mounted at /workspace, tarball checkout
88-
with traversal-safe extraction that also drops symlink/hardlink entries, one keep-alive
87+
hardened container via bollard (daemon-managed anonymous volume at /workspace — NOT a host
88+
bind mount, so a runner that itself runs in a container sharing the host docker.sock still
89+
gets a populated /workspace; source is streamed IN via the Docker archive API/`docker cp`
90+
after the container starts, tarball repackaged into a traversal-safe tar that strips
91+
GitHub's top-level dir and drops symlink/hardlink entries; artifacts are streamed back OUT
92+
of `.overup/artifacts/` via the same archive API, one keep-alive
8993
container with `no-new-privileges` always plus cap-drop ALL by default, memory/CPU/pids
9094
limits, bridge|none|isolated per-job network, optional non-root user + read-only rootfs,
91-
steps as `exec` with `<shell> -c`, SIGKILL on cancel/timeout, 5 s Docker stats sampling for
95+
steps as `exec` with `<shell> -c`, SIGKILL on cancel/timeout, container removed with its
96+
anonymous volume, 5 s Docker stats sampling for
9297
CPU/memory/net/blkio metrics reported with `job_result`), and stream seq-numbered log
9398
chunks. `services/log_hub.rs` is the single log write path: mask FIRST (checkout tokens +
9499
confidential-looking env values; a per-job carry buffer catches secrets split across chunk
@@ -403,7 +408,8 @@ upgrades), `dashmap` (RunnerHub connection registry + LogHub broadcast/mask maps
403408
endpoint, region `auto`, presigned URLs; isolated in `services/r2.rs`), and the local
404409
`protocol` crate (shared WS message types + HMAC helpers). The `runner/` crate adds
405410
`bollard` 0.21 (Docker Engine API: image pull, container lifecycle, exec streams),
406-
`tokio-tungstenite` (rustls), `tar` + `flate2` (traversal-safe tarball extraction),
411+
`tokio-tungstenite` (rustls), `tar` + `flate2` + `bytes` (repackaging the source tarball
412+
into a traversal-safe tar streamed into the container via the Docker archive API),
407413
`tempfile` (per-job workspaces). Deliberately NOT used: `octocrab` (the reqwest helper in
408414
`services/github_app.rs` suffices), `git2` (tarball checkout via the API instead of cloning
409415
— heavy native dep on Windows).
@@ -500,9 +506,13 @@ endpoint, region `auto`, presigned URLs; isolated in `services/r2.rs`), and the
500506
- List filters are validated before SQL: status/conclusion/trigger allow-lists, length caps
501507
on branch/search/timestamps, and free-text search runs through a server-built ILIKE
502508
pattern with `\`, `%`, `_` escaped — user text only ever matches literally
503-
- Execution isolation (reference runner): per-job temp workspaces (deleted after), tarball
504-
extraction drops non-`Normal` path components AND symlink/hardlink entries (traversal
505-
defense — a link target can't be validated by path checks), 1 GiB tarball cap, jobs run
509+
- Execution isolation (reference runner): /workspace is a daemon-managed anonymous volume
510+
(removed with the container), NOT a host bind — source is repackaged into a
511+
traversal-safe tar (top-level dir stripped, non-`Normal` path components AND
512+
symlink/hardlink entries dropped — a link target can't be validated by path checks) and
513+
streamed IN over the Docker archive API after the container starts (so a containerized
514+
runner sharing the host docker.sock still sees a populated /workspace); artifacts are
515+
streamed back OUT of `.overup/artifacts/` the same way, 1 GiB tarball cap, jobs run
506516
in Docker containers with `no-new-privileges` always set, **capabilities dropped by
507517
default** (`RUNNER_CAP_DROP=false` to opt out), memory/CPU/pids limits
508518
(`RUNNER_JOB_MEMORY_BYTES`/`RUNNER_JOB_NANO_CPUS`/`RUNNER_JOB_PIDS_LIMIT`, 0 disables),

‎backend/.env.example‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,15 @@ RUNNER_JOB_SIGNING_KEY=
6262

6363
# Container image for jobs that declare no container and no known runs-on
6464
# label. The catthehacker/ubuntu:act-* family (what nektos/act and Gitea
65-
# Actions default to) is GitHub-runner-compatible with Node/npm/yarn,
66-
# Python, git, build-essential etc. preinstalled (~500 MB compressed). For
67-
# a specific toolchain, jobs pin their own image in workflow YAML with the
68-
# GitHub `container:` key, e.g. `container: rust:1`, `container: node:22`,
69-
# `container: golang:1.23`.
65+
# Actions default to) is the MEDIUM GitHub-runner-compatible image with
66+
# Node/npm/yarn, Python, git, build-essential etc. preinstalled
67+
# (~500 MB compressed). It does NOT ship every toolchain — notably no Rust
68+
# (`cargo: not found`) or Go — so a job needing one pins its own image in
69+
# workflow YAML with the GitHub `container:` key, e.g. `container: rust:1`,
70+
# `container: node:22`, `container: golang:1.23`. (`uses: actions/setup-*`
71+
# steps are not executed; overup emits a plan notice pointing at the
72+
# equivalent `container:` pin. `uses: actions/checkout` is a no-op — the
73+
# runner checks the repository out into /workspace automatically.)
7074
DEFAULT_JOB_IMAGE=catthehacker/ubuntu:act-latest
7175

7276
# --- Secrets Management (optional but recommended) ---------------------------

‎backend/src/services/pipeline_plan.rs‎

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,20 @@ pub fn build_plans(raw_content: &str, default_image: &str) -> Result<Vec<Planned
105105
for (index, step) in list.iter().enumerate() {
106106
if let Some(uses) = step.get("uses").and_then(Value::as_str) {
107107
let uses = sanitize_for_notice(uses);
108-
match setup_action_hint(&uses) {
109-
Some(hint) => notices.push(format!(
108+
if is_checkout_action(&uses) {
109+
// Checkout is NOT skipped — the runner fetches the
110+
// repository automatically before the job runs. Saying
111+
// "not supported; skipped" wrongly implies no source.
112+
notices.push(format!(
113+
"step `uses: {uses}` is a no-op here — your repository is checked out automatically before the job runs"
114+
));
115+
} else if let Some(hint) = setup_action_hint(&uses) {
116+
notices.push(format!(
110117
"step `uses: {uses}` is not executed; the default job image ships common toolchains — pin this one by adding `{hint}` to the job"
111-
)),
112-
None => notices
113-
.push(format!("step `uses: {uses}` is not supported yet; skipped")),
118+
));
119+
} else {
120+
notices
121+
.push(format!("step `uses: {uses}` is not supported yet; skipped"));
114122
}
115123
continue;
116124
}
@@ -243,6 +251,14 @@ fn sanitize_for_notice(value: &str) -> String {
243251
value.chars().filter(|c| !c.is_control()).take(100).collect()
244252
}
245253

254+
/// Whether a `uses:` step is a repository checkout. These are a no-op on
255+
/// overup because the runner checks the repository out automatically before
256+
/// the job runs — so the notice must not imply the source is missing.
257+
fn is_checkout_action(uses: &str) -> bool {
258+
let action = uses.split('@').next().unwrap_or(uses);
259+
matches!(action, "actions/checkout")
260+
}
261+
246262
/// Well-known `setup-*` actions mapped to the per-job `container:` image
247263
/// that provides the same toolchain. `uses:` steps are not executed on
248264
/// overup, so the notice points authors at the mechanism that is.
@@ -466,4 +482,22 @@ jobs:
466482
assert_eq!(setup_action_hint("dtolnay/rust-toolchain@stable"), Some("container: rust:1"));
467483
assert_eq!(setup_action_hint("actions/checkout@v4"), None);
468484
}
485+
486+
#[test]
487+
fn checkout_notice_is_reassuring_not_a_skip_warning() {
488+
// The SIMPLE workflow uses `actions/checkout@v4`; its notice must say
489+
// the repo is checked out automatically, NOT "not supported; skipped".
490+
let plans = build_plans(SIMPLE, "ubuntu:24.04").ok().unwrap();
491+
let notices = plans[0].plan["notices"].as_array().unwrap();
492+
let checkout = notices
493+
.iter()
494+
.filter_map(|n| n.as_str())
495+
.find(|n| n.contains("actions/checkout@v4"))
496+
.expect("a checkout notice");
497+
assert!(checkout.contains("checked out automatically"), "{checkout}");
498+
assert!(!checkout.contains("not supported"), "{checkout}");
499+
500+
assert!(is_checkout_action("actions/checkout@v4"));
501+
assert!(!is_checkout_action("actions/setup-node@v4"));
502+
}
469503
}

‎runner/Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎runner/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ anyhow = "1"
88
# `ssl` (rustls + ring) enables remote TLS daemons: DOCKER_HOST=tcp://…:2376
99
# with DOCKER_TLS_VERIFY=1 and DOCKER_CERT_PATH pointing at ca/cert/key.pem.
1010
bollard = { version = "0.21", features = ["ssl"] }
11+
# Source/artifacts move over the Docker archive API (docker cp), so we hand
12+
# bollard a `Bytes` tar body via `bollard::body_full`.
13+
bytes = "1"
1114
chrono = { version = "0.4", features = ["serde"] }
1215
dotenvy = "0.15"
1316
flate2 = "1"

0 commit comments

Comments
 (0)