Conversation
#1820) Closes #1810. ## The defect When the three windowed queries came back empty and the final unwindowed query returned a run **inside** the limit, the step exited 1 and reported the contradiction. The gate's question is whether a scheduled run exists inside the limit, and that run answers it, so the failure was wrong. It fires with nothing broken, for instance when a run completes between two queries. ## What changed That branch now emits one `::warning::` naming both facts, the three empty windowed pages and the timestamp the unwindowed query returned, and exits 0. The other two branches are untouched: an unwindowed answer that is absent, or one that is older than the limit, keeps its `::error::` and exit 1. ## How I checked it Case G in `tests/shell/reusable-schedule-freshness.test.sh` now asserts exit 0, a `::warning::` naming both facts, and no `::error::` in the output; the call count it already asserted is unchanged, and every other case is untouched. I sabotaged the branch four ways and each one dropped exactly one assertion, no more: | Sabotage | Assertion that fell | |---|---| | `exit 0` back to `exit 1` | G passes with a warning (exit 0) | | `::warning::` prefix removed | G emits a warning naming both facts | | `${latest}` dropped from the message | G emits a warning naming both facts | | an extra `::error::` added to the branch | G does not emit an error | Gates, all rc=0: the test file (53 passed, 0 failed); the full `test-command` line from `lint-shell.yml` (15 files, 0 failed); `shellcheck` on the test; `actionlint` on the reusable. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…1821) Closes #1812. ## The defect Three tests pointed at `https://127.0.0.1:1` and asserted the connection was refused. They inherited `ureq`'s default proxy handling, so with `HTTPS_PROXY` set and no matching `NO_PROXY` the request went to the proxy and the error was a different one. Port 1 being free was a second assumption about the host. ## What changed A test-only constructor beside `with_bases` builds the agent with proxy handling disabled, and the base points at a port the test allocated from the OS and released, so the refusal comes from a socket the test chose. No new dependency, no change to `Cargo.lock`, and the public API is unchanged. The assertions still anchor on the existing `names_a_refused_connection` helper. ## How I checked it **The obvious gate is worthless and I want that on the record.** Running the suite with `HTTPS_PROXY=http://127.0.0.1:9` passes whether or not proxy handling is disabled, because a refused proxy produces the same `Connection refused` the tests assert. It proves nothing. So I ran a local listener that *accepts* the connection and closes it, and pointed `HTTPS_PROXY` at that: - with proxy handling disabled: 13 passed, 0 failed; - with that one line reverted: `FAILED. 10 passed; 3 failed`, and the three are exactly `latest_version_maps_transport_error`, `fetch_maps_transport_error` and `https_base_passes_the_scheme_check`, each reporting `Connection reset by peer (os error 104)` instead of the refusal. I also checked the test-only agent does not blind the shipping one: dropping `https_only(true)` from `new()` alone still fails `plaintext_base_is_refused_for_being_plaintext`, which builds through `new()`. Gates, all rc=0: `cargo test --locked --lib --all-features update::` (81 passed, 0 failed), `cargo fmt --all --check`, `cargo clippy --locked --all-targets --all-features -- -D warnings`. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…1822) Closes #1795. ## The defect `podup cp payload/. svc:/tmp` should copy the CONTENTS of `payload` into `/tmp`, the way `docker cp` and `podman cp` treat a source whose last component is `.`. It copied the directory itself, so the files landed in `/tmp/payload/`. `pack_path` named the archive's top entry with `src.file_name()`, and Rust's `Path` drops a trailing `.`, so `Path::new("payload/.").file_name()` is `Some("payload")` and nothing between the CLI and the packer ever saw the cue. ## What changed The cue is detected on the source as written, before it becomes a `Path`, and threaded to the packer, which puts the directory's children at the top of the archive with no wrapper entry. A source that is not a directory is refused with the "not a directory" shape podman returns. The contents archive has no wrapper, so the confirmation routes through `tree_landed`, which asks about each entry, rather than the single-entry path. ## How I measured the rule Against podman on Linux, into a destination directory that **already exists** (a non-existent destination flattens on its own and would have confounded the reading): | Source | podman | podup before | podup now | |---|---|---|---| | `payload` | `dest/payload/a.txt` | same | same | | `payload/` | `dest/payload/a.txt` | same | same | | `payload/.` | `dest/a.txt` | `dest/payload/a.txt` | matches | | `payload/./` | `dest/a.txt` | `dest/payload/a.txt` | matches | | `payload/.//` | `dest/a.txt` | `dest/payload/a.txt` | matches | | `.` from inside | `dest/a.txt` | `dest/payload/a.txt` | matches | | `./` from inside | `dest/a.txt` | `dest/payload/a.txt` | matches | A single file with the cue: podman answers `not a directory` and exits 125; podup returns `cp: not a directory: <path>`. The cue is read with `std::path::is_separator`, the same idiom `has_trailing_separator` already uses, so the backslash shapes fire on Windows and not on Unix, where a backslash is an ordinary filename character. **The `#[cfg(windows)]` shapes did not run on this host** and what `podman cp` does with `payload\.` on Windows is not measured; the rule is written as podup's own, derived from the platform separator, and claims no parity there. ## Controls Both sabotages drop exactly the shapes they name: | Sabotage | Assertions that fell | |---|---| | back to the literal `ends_with("/.")` | the four new shapes: `.`, `./`, `payload/./`, `payload/.//` | | accept `..` as the cue | the two `..` shapes | Gates, all rc=0: `cargo test --locked --lib --all-features engine::copy` (123 passed), the live `cp_flags::` with `PODUP_REQUIRE_PODMAN=1` (8 passed, so no test skipped itself), `cargo fmt --all --check`, `cargo clippy --locked --all-targets --all-features -- -D warnings`. ## What this does not close The container-to-host direction has the same divergence and is untouched: `cp svc:/path/. host_dir` still lands the contents under the source's basename when the destination exists. Closing it needs the archive post-processed or the cue detected in `cp_from_container`, because libpod normalises the trailing `/.` away server side. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
… run (#1824) Closes #1809. ## Two defects **It could answer for a commit nobody tested.** The verdict was the newest completed run on the branch, with cancelled runs passed over. Let A pass, push B, cancel B's run by hand with nothing replacing it, and the script reported A's success as the branch's health. **The two callers fed each other.** A real failure on `main` made the next otherwise healthy `develop` run fail `health-main`; a repaired `main` then failed `health-develop` against that `develop` result, and they held each other red. Clearing it on 2026-09-20 took three reruns in a chain: the push run on `develop`, then the release pull request's run, then the push run on `main`. ## What changed The verdict is the run whose `head_sha` is the branch's current head: `GITHUB_SHA` on a push, and one extra API read to learn the head on `schedule` and `pull_request`. A run that has not finished is waited for, 15 seconds between attempts and at most 32 attempts, inside the job's ten-minute bound. A `cancelled` run for that head is red, because nothing newer can answer for the commit, and the pass-over disappears with the newest-run logic it belonged to. Same shape the three distribution channels shipped for their own suite-on-main check. A failed `gh api` counts as no answer for that attempt and the loop keeps polling, with a `::warning::` naming the failure. Under `set -euo pipefail` an unguarded call would have aborted the script on the first rate limit, which is a retry loop that runs exactly once. ## Controls Every sabotage drops exactly the assertions named for the behaviour: | Sabotage | Assertions that fell | |---|---| | `::error::` moved to stderr | `split streams: ::error:: on stdout` | | `::warning::` moved to stderr | `split streams: ::warning:: on stdout`, `::warning:: NOT on stderr` | | a failed `gh api` aborts instead of polling on | the four `transient gh failure` assertions | The stream assertions capture stdout and stderr into separate files; the rest of the suite runs the script with `2>&1`, which merges them and cannot tell the two apart. ## What I fixed on review before this landed The first pass had the design right and four defects the gates did not catch: three of four files lost their final newline (`.editorconfig` sets `insert_final_newline = true` and CI checks it); all six `::error::` lines moved to stderr, against 55 sites in `develop` that all use stdout; an untouched comment about an unrelated 2026-09-06 incident was reflowed into `reason anybody knew was it that somebody went looking`; and the poll loop aborted on a failed `gh api`. All four are fixed here. Gates, all rc=0: `bash tests/shell/branch-health-conclusion.test.sh` (51 passed), the full `test-command` line from `lint-shell.yml` (15 files, 0 failures), `cargo test --locked --test 'workflow_*'` (31 passed), `shellcheck` on the script and its test, `actionlint` on the reusable. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Closes #1826. Introduced by #1824, which I merged an hour earlier. `develop` is red on it. ## The defect The reusable set `HEAD_SHA` to the pushed commit on **any** push, and the script then looked for a run of the watched workflow on the watched branch whose `head_sha` was that commit. The two callers in `ci.yml` are cross-branch by design: `health-main` runs on every push that is not to `main`, and `health-develop` on every push that is not to `develop`. So a push to `develop` made `health-main` ask about `main` using `develop`'s commit. No run on `main` ever carries it. Measured on run 35524950565, the push run for `b75f507` on `develop`: branch health (main) failed after 8m18s no ci.yml run for commit b75f507 on main finished within 32 attempts (each 15s) Symmetric, so a push to `main` fails `health-develop` the same way. And not cosmetic: it makes `ci.yml` conclude `failure` on the branch, and the next pull request reads that conclusion and fails in eight seconds. That is how #1823 got blocked. ## The fix `HEAD_SHA` carries the pushed commit only when the push was to the branch being checked; otherwise it stays empty and the script resolves the branch head through the API, the path `pull_request` and `schedule` already use. ## Why the existing tests could not catch it The 51 shell assertions exercise the script with `HEAD_SHA` set and with it empty. That is the script's own contract and it was never wrong. The defect was in the **workflow expression that decides `HEAD_SHA`**, and nothing tested that. While reviewing #1824 I sabotaged the script six ways and every sabotage bit; none of them could touch a line of YAML. The new test reads that one line, parses the expression out of it and compares it whole. Anchored to the `HEAD_SHA:` line, because a `contains "inputs.branch"` check would be satisfied by the comment above it or by the `BRANCH:` env line below. Compared by equality, because a shape that merely names the right identifiers is not the right shape. It also carries a test of the parser itself: a planted buggy step must not parse to the expected expression, and a planted correct one must. Without that, a broken parser would make the real assertion pass on anything. Reverting the expression to the buggy one drops exactly `head_sha_expression_compares_pushed_branch_with_branch_under_test` and nothing else. Gates, all rc=0: `cargo test --locked --test 'workflow_*'` (33 passed), `bash tests/shell/branch-health-conclusion.test.sh` (51 passed), `cargo fmt --all --check`, `actionlint` on both the reusable and `ci.yml`. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Bumps the three seals: `Cargo.toml`, `Cargo.lock` and `debian/changelog`. Verified they agree, that `cargo metadata --locked` does not move the lock, and that `dpkg-parsechangelog` reads `Version: 5.9.5`. ## What 5.9.5 carries Five commits since 5.9.4, and only one of them is visible to a user, so the changelog has one entry: | Commit | User-facing | |---|---| | `ea33e38` #1795 `cp` of a source ending in `/.` copies the contents | **yes** | | `53e9c51` #1812 updater transport tests no longer depend on the machine | no, a `#[cfg(test)]` constructor | | `8c0ce68` #1810 freshness passes when it has proof of freshness | no, CI | | `b75f507` #1824 branch health reads the run for the branch's head | no, CI | | `5eddf57` #1827 branch health gates `HEAD_SHA` to the branch under test | no, CI | The last two are worth naming even though they ship nothing: together they took `branch health (main)` on a push to `develop` from exhausting 32 attempts and failing after 8m18s down to **7 seconds**, and they closed a loop where a red on one branch held the other red. ## What is deliberately NOT in it - **#1808**, the symlink target confirmation. It is right on Podman 5.7.0 and fails on Podman 6.1.2 by reporting a landed copy as failed, which is the #1777 shape. PR #1823 is closed unmerged and the issue carries the full state and the next step. - **#1825**, the live pod userns test. Its control is proven (sabotaging `pod_userns` makes it fail with the exact values) but it has an open defect of its own: it asserts on the first line of the ID map, and `auto:size=2048` can arrive split across two ranges. It stays in its pull request. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
|
Correction to this pull request's description, and to the commit message of #1822 which is now in Both say the container-to-host direction still lands the contents under the source's basename, or "has the same divergence". That is false, and I did not measure it before writing it. I took it from the working notes of the change rather than from the daemon. Measured on the published 5.9.5 linux-x86_64 binary against Podman 5.7.0, four cases, comparing podup with
They agree in all four. The container-to-host direction handles the trailing The 5.9.5 changelog entry is not affected: it says "The container to host direction is unchanged", which is accurate, since the change did not touch it. What this costs: #1822 shipped with a "what this does not close" paragraph describing a defect that does not exist, and that paragraph is in |
…t carries it (#1853) Closes #1831. P0: this makes the 5.9.5 release run on `main` red and fails both checks on every pull request opened since. ## The defect The script asked for `workflows/${workflow}/runs?branch=${branch}`, filtered by `head_sha`, sorted by `created_at` and took the newest. It did not filter by event. A release pull request goes from `develop` into `main`, so its head branch is `develop` and its run appears under `?branch=develop` beside the push run for the same commit. Being newer, it wins. For `develop`'s head `20937cc`: | Run | Event | Conclusion | |---|---|---| | #2539 | `pull_request` | failure | | #2538 | `push` | **success** | The script read #2539. That run is #1830, the release pull request, and its `ci.yml` concluded failure **because this very job failed inside it**. The gate read its own run, failed, made that run red, and every later reader saw a red the gate created. #1809 was opened to close exactly this loop in its other form. ## Why it is not cosmetic On run 35538230895, the push run for `83315af` on `main` and the 5.9.5 release commit, `branch health (develop)` is the **only** failing job. `main`'s history records the release as red, about the state of a branch that was green, as reported by a run that branch did not trigger. ## The fix Both the runs call and the jq filter select `event=push`. A branch's health is the verdict of the run that branch triggered; a `pull_request` run that merely carries the branch as its head is a different question. Nothing else moved: the polling bound, the cancelled-is-red rule and the `HEAD_SHA` gating from #1827 are unchanged. Three defects in a row have come out of this file and widening the change is how a fourth arrives. ## Sabotage Removing the filter from **both** places, because removing one leaves the other enforcing it, drops four assertions: ``` FAIL newer pull_request failure cannot shadow older push success for head (gate must not read a run the branch did not trigger): exit 0 FAIL verdict line names run #60 (the push run, not the pull_request run #62) FAIL verdict line does NOT name run #62 (the pull_request run) FAIL pull_request run URL #62 must NOT appear ``` The suite also carries the assertion that stops the lazy fix: a **newer push failure** over an older push success still fails, so this cannot become "prefer whatever succeeded". Gates, all rc=0: `bash tests/shell/branch-health-conclusion.test.sh` (59 passed), `cargo test --locked --test 'workflow_*'` (33 passed), `cargo fmt --all --check`, `shellcheck` on the script and its test, `actionlint` on the reusable. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Five commits since 5.9.4, one of them visible to a user.
What ships
cpof a source whose last component is.copies the directory's contents (#1795).podup cp payload/. SERVICE:/tmpput the files in/tmp/payload/before; they land in/tmpnow, the waydocker cpandpodman cptreat it.Measured against podman into a destination directory that already exists, because a non-existent destination flattens on its own and would have confounded the reading:
payloaddest/payload/a.txtpayload/.dest/a.txtdest/payload/a.txtpayload/./dest/a.txtdest/payload/a.txt.from insidedest/a.txtdest/payload/a.txtThe cue is read with
std::path::is_separator, so the backslash shapes fire on Windows and not on Unix, where a backslash is an ordinary filename character. A source that is not a directory is refused the way podman refuses it. The container to host direction is unchanged and still lands the contents under the source's basename.What does not ship but mattered
Four CI fixes with nothing user-visible in them. Two are worth naming:
branch healthread the newest completed run on a branch and could answer for a commit nobody tested; it now reads the run for the branch's head (#1824). That change then broke both callers, because it handed the pushed commit to whichever branch was being asked about, and the two callers are cross-branch by design (#1827). Together they tookbranch health (main)on a push todevelopfrom exhausting 32 attempts and failing after 8m18s down to 7 seconds, and closed a loop where a red on one branch held the other red.Deliberately left out