Skip to content

chore: promote develop to main without cutting a release - #1610

Merged
Jaro-c merged 8 commits into
mainfrom
develop
Aug 30, 2026
Merged

Jaro-c merged 8 commits into
mainfrom
develop

Conversation

@Jaro-c

@Jaro-c Jaro-c commented Aug 30, 2026

Copy link
Copy Markdown
Member

Summary

Promotes develop to main with no version bump and no tag. Both branches stay at 5.4.0, and release.yml fires only on a v* tag or a manual dispatch, so nothing is published.

What it carries

Eight pull requests. Three of them landed after this was opened, which is why the checks ran more than once.

Update path

Security

Housekeeping

What this promotion measured, before it merged

The code_scanning rule does not block on pre-existing alerts. It was added to protect-main earlier today with the configuration Glyndor/apt uses. Seven rust/cleartext-logging alerts were open on main, and the first green run of this pull request came back mergeState=CLEAN. GitHub's documented behaviour — that the rule blocks on alerts introduced by the pull request rather than pre-existing ones — is now confirmed rather than read. The rule stays.

What it does not do

Code scanning still runs on the default branch only, so none of the eight was scanned on its own pull request. That is why alerts from a promotion are attributed to the promotion rather than to the change that caused them. #1603 would have fixed it and is closed: the organization's Glyndor baseline configuration pins CodeQL to default setup across fifteen repositories.

Test plan

  • All eight merged green on develop before this
  • main and develop are both at 5.4.0 in Cargo.toml and debian/changelog, so no changelog entry is owed
  • release.yml triggers re-read: push: tags: v* and workflow_dispatch only
  • Nothing publishes: chore: stop publishing podup to crates.io #1606 removed the cargo publish steps, and tests/podup_is_not_published.rs fails if any workflow gains one back

What changes for a user

Two things, both shipping with the next release rather than with this promotion: podup requires glyndor-archive-keyring, and podup update refuses on Homebrew and Scoop installs rather than overwriting them.

Signed-off-by: Jaro-c 75870284+Jaro-c@users.noreply.github.com

Jaro-c added 8 commits August 29, 2026 21:36
#1593 asked whether podup should ship a postinst writing
/etc/apt/apt.conf.d/20auto-upgrades, the file that actually switches unattended
upgrades on. It should not. That file turns on automatic upgrades for every
package on the machine rather than for Glyndor's, and podup only answers for
Glyndor's. Whether a whole server updates itself is the operator's decision, or a
fleet manager's, not the container runtime's.

Choosing to leave the gap is what made the README wrong, which is the substance
here. It said an apt-installed podup "updates through apt and nothing else",
reading as a promise that it stays current. The dependency guarantees the package
is installed, not that it runs: on a Debian where somebody registered the archive
by hand and ran `apt install podup`, the package and the Glyndor allowlist are
both present and the daemon is off. The README now names that row and gives
`systemctl status unattended-upgrades` as the way to tell which one you are on.

debian/control already recorded this in a comment and nothing enforced it.
tests/no_system_wide_apt_policy.rs does: no file podup packages writes the switch,
a first maintainer script trips a tripwire asking you to re-read the decision
rather than passing as routine, and the README keeps the sentence.

Two things came out of building it. The first matcher reported debian/control as
a breach, because the paragraph explaining that podup does not write the switch
names the switch; comment lines are now stripped, and a heredoc emitting the file
is still a line, checked by adding a postinst that does exactly that. And the
README needle spanned a line break, which cannot match on the CRLF Windows
checkout — reproduced locally by converting the file rather than inferred from
the error — so the assertion now collapses whitespace and compares words, which
also survives a reflow.

Closes #1593.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…1605)

`line-limit` reads the whole tree, not the diff, so a file that has crept to within a few lines of the 500-line hard limit fails whichever pull request next touches it. The pull request that breaks is not the one that made the file long.

  internal/engine/lifecycle/tests.rs   487 -> 212, plus teardown_tests.rs 285
  internal/quadlet/tests/fields.rs     481 -> 365, plus fields_resources.rs 119

Both are test files with no inline module, and that distinction decides the cost. cargo llvm-cov does not report on sibling test files -- measured: both are absent from the coverage data, while quadlet/warnings.rs appears at 95.0% because its 271 lines of inline tests count as covered lines of the file. So these two cannot move the number, and the run confirms it: 81.88% to 81.91%.

Splitting the other eleven by moving their inline tests out would, against a threshold of 79 with about three points of headroom. That is a decision about the coverage gate rather than a refactor, so it is not made here.

Both cuts are by subject and both follow a precedent in the same directory: scale_tests.rs and drop_recheck_tests.rs were already split off the lifecycle suite, and fields_logging.rs off fields.rs for this exact reason (#1354).

1731 unit tests pass, the same count as before, so nothing was dropped in the move.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…ation (#1604)

Cargo.toml excludes fuzz and bench/timeit from the workspace, so each resolves its own dependencies into its own Cargo.lock. Dependabot's cargo entry names a directory, and the one for / reaches neither. Both have been unwatched for as long as they existed, and audit.yml runs cargo audit against the workspace lock rather than these two. The fuzz harness is the sharper of the pair: it exists to be fed hostile input.

Both new entries group every update type, majors included, unlike the entry for the crate itself, because nothing here is shipped. Their limit is 5 rather than 10: once an ecosystem's limit is full Dependabot stops proposing anything for it, security bumps included.

tests/dependabot_covers_every_lockfile.rs is the gate. It derives the directories from [workspace] exclude rather than from the tree, which matters twice over: the first version shelled out to git ls-files and the Debian build container has no git binary, and the second took the file's first exclude -- the [package] one that Cargo.toml warns about two lines above -- so the derived list came back as just / and the test passed comparing an empty set. It now carries a floor that fails on that.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
podup was published to crates.io for helmly-agent, and helmly-agent does not use it. Measured: zero `use podup::` in its source, zero entries for podup in its Cargo.lock, and its own manifest records that the dep is gone. When the dependency did exist it was a git dep, so crates.io was never what carried it. crates.io reports zero reverse dependencies.

What it cost: a semver promise on seventeen pub items with nobody to make it to, two of the thirteen required status checks, and 219 lines of workflow and test that existed only to keep the promise.

Removed crates-publish.yml, the cargo publish steps at the end of release.yml -- which is the one worth finding, since deleting only the workflow would have left the release pipeline publishing on the next tag -- and the two API-surface tests. package-check and semver-check are false in ci.yml, and both were taken out of protect-main and protect-develop FIRST: a job with a job-level if: does not emit its check, so flipping the input while the ruleset still required the name would have blocked this very pull request.

Kept: the [lib] target, because the integration tests build against it; rust / Doc warnings, because cargo doc with warnings denied catches broken intra-doc links whether or not anyone downloads the crate, so only two of the three checks were publish-only; and 5.4.0 on crates.io, which cannot be deleted, only yanked, and reserves the name.

publish = false is the part cargo enforces. tests/podup_is_not_published.rs is the part it does not.

The claim that helmly-agent consumed the library had gone false in four places, not one: README.md, CONTRIBUTING.md verbatim, Cargo.toml's documentation pointing at docs.rs which would freeze at 5.4.0 while reading as current, and docs/debian-packaging.md. Prose does not have one home, so a third test now scans README, CONTRIBUTING and every docs/*.md.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
unattended-upgrades has been a hard dependency since 5.4.0, and on its own it guarantees an engine with nothing aimed at Glyndor. glyndor-archive-keyring is what aims it: it ships /etc/apt/sources.list.d/glyndor.sources and /etc/apt/apt.conf.d/51glyndor-unattended-upgrades and nothing else. Without them the daemon runs against Debian's own security suite, the logs look healthy, and podup is never upgraded -- the exact outcome the unattended-upgrades dependency was added to prevent. Two paths reached it: dpkg -i of a release .deb, and adding the repository by hand.

No bootstrap problem, measured rather than reasoned: apt-cache policy resolves the keyring from apt.glyndor.net, so a machine that already reaches the archive installs it like any other dependency, and a standalone dpkg -i with no repository fails on the unmet dependency, which is the intended answer. The one-line installer was never affected -- it dpkg -i's the keyring before apt-get install.

The field stays on one line: a continuation is valid and dpkg reads it, but tests/podman_floor.rs finds it with starts_with, so a wrapped field would have been invisible to the checks that exist to read it.

the_package_requires_the_archive_keyring is the gate, driven red by demoting to Recommends and by removing it.

Also corrected: debian/control claimed 'Ubuntu server ships both'; measured on 26.04 the switch is present and enabled but no package owns the file, so that was right about the outcome and wrong about the mechanism. And docs/debian-packaging.md now records that a fork publishing its own archive must declare Provides: glyndor-archive-keyring.

Scope is podup alone, by the owner's decision: epistle declares neither dependency, so two products of the same archive answer this differently, knowingly.

Closes #1602.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
podup update replaces the running executable in place and refuses when a package manager owns it, because doing so desyncs that manager's record of the file. It only ever recognised one. pkg.rs said so plainly -- 'Only dpkg/apt is detected, on Linux' -- and off Linux it did not look at all. So a Homebrew or Scoop install running podup update had its binary rewritten underneath a manifest that still claimed to know the contents.

apt is still asked, because dpkg-query -S is an authoritative database lookup. The other two are read off the path, deliberately: brew costs a process spawn on every update to learn a prefix the path already shows, and Scoop is a PowerShell function rather than an executable, so there is nothing to spawn.

Homebrew installs under <prefix>/Cellar/<name>/<version>/ and links into <prefix>/bin, so the existing canonicalize resolves into the Cellar either way. The prefix moves -- /opt/homebrew, /usr/local, /home/linuxbrew/.linuxbrew, anywhere custom -- and the Cellar component does not, which is why that is what is matched. Scoop puts apps under <root>/apps with shims in <root>/shims, with SCOOP consulted first for a relocated root.

Both path checks can be wrong in one direction only, and it is the safe one: a layout that merely looks like theirs refuses an update that would have worked, which is visible and recoverable, rather than missing one and rewriting a file whose manager still believes it knows the contents.

scoop_owns delegates to scoop_owns_under, which takes the root as an argument, because setting an environment variable in a parallel test binary is a race rather than a fixture -- and that branch is the one the fallback provably cannot see.

Six tests, all driven red. The Windows fixtures use / on purpose: Path::components splits on backslash too when it runs on Windows, while a backslash fixture would not parse into components on Linux and the test would pass by asserting nothing.

The error message hardcoded 'apt upgrade podup', correct while apt was the only manager detected and a wrong instruction the moment it is not. Each manager now gets its own command, asserted. docs/self-update.md carried the same stale claim and now carries the table.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…ns (#1611)

Seven rust/cleartext-logging alerts are open on main, all the same shape: an assertion in the integration suite formatting a Result that CodeQL traced back to create_project_secrets.

The source is the function's NAME. SensitiveDataCall matches on HeuristicNames::nameIndicatesSensitiveData, so create_project_secrets is a taint source because it contains 'secret', not because of anything its Result<()> can carry -- and #1599 already made it impossible for a payload to reach one of those errors. The fix that corrects the model rather than the sinks needs a barrier model, which needs a configuration this repository owns, which the organization's Glyndor baseline pins away. That was #1603 and it is closed.

Removing the formatted value removes the sink, and that is certain rather than hoped: resources_health.rs:209 already asserts on the same tainted result with a plain message and is not flagged, and eighteen .unwrap() calls on the same values are not flagged either. Only assert! interpolating the value is a sink.

Seven changed, eleven left alone. There are eighteen occurrences and CodeQL flagged seven; the rest are on paths the query does not reach. Nothing syntactic separates them, so each of the three files gains a note saying the inconsistency is deliberate -- without it the next reader tidies it up and the alerts come back.

The matches! assertion lost the most, so it is split rather than flattened: expect_err first, which prints the Ok value -- here () -- then the variant check. That keeps the difference between 'the run succeeded' and 'it failed with the wrong variant'.

The cost, stated rather than discovered: these seven no longer say which error they got, and nothing prevents an eighth, because a syntactic gate cannot tell the seven from the eleven.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…odup (#1612)

A machine with podup installed and glyndor-archive-keyring absent is never upgraded automatically, and never learns it. The rule permitting the Glyndor origin is shipped by that package, so unattended-upgrades does not look at podup, the version carrying the fix never arrives, and the Depends added in #1607 is never seen. Circular, and silent -- nobody reports it because the affected machine does not find out.

podup update already refuses on an apt-owned binary and sends the operator to apt upgrade podup. That instruction is correct and, on such a machine, half the story. The refusal now appends the reason and the remedy when nothing will act.

Where it runs is the design. Not a cron, which would have the same silence one level down. Not a stored timestamp, which needs new state on disk and reintroduces the same problem when the file goes missing. Not every command, which is how a warning becomes noise. Only on the refusal, where the operator has just asked to update and is being told to use something else: one process spawn, on the one path that has earned it.

Three checks, because there are three ways to be stuck and the first draft knew one. Allowed-Origins is what our keyring writes; Origins-Pattern is documented by the package's own README as an alternative, so an operator using it is covered and a check that knew only the first would have raised exactly the false alarm this exists to avoid; and Package-Blacklist vetoes by name, so an allowed origin is not sufficient. No rule of either kind means Unknown and silence.

The origin match is loose on purpose -- any rule mentioning Glyndor rather than Glyndor:stable exactly. An operator who allowed a different suite has thought about this; the machine worth warning is the one whose configuration has never heard of Glyndor.

glyndor_auto_update takes the dump text rather than fetching it, so the configurations that matter can be fed to it. Five fixtures, one of them this machine's real apt-config dump verbatim -- that one alone would have been the trap, because a check only ever seen to agree with the configuration we write ourselves has not been tested against the ones it meets in the field. All three checks driven red.

Not verified end to end on a machine in this state, because there is not one here.

Refs #1602.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
@Jaro-c
Jaro-c merged commit 4c282b0 into main Aug 30, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant