Skip to content

Commit 5df8dee

Browse files
committed
Merge remote-tracking branch public/main into Bnjoroge/lock-refactor
2 parents 9c4e21c + f710eaf commit 5df8dee

9 files changed

Lines changed: 504 additions & 187 deletions

File tree

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
name: apt-indices-refresh
22

3-
# Weekly freshness loop for golden apt indices. Pack production stamps each
3+
# Weekly freshness reminder for golden apt indices. Pack production stamps each
44
# bake (marker in the image, `apt_indices` in provenance.json); this job
5-
# compares the published pack's stamp against the versions.toml policy and
6-
# dispatches a rebuild when stale. Nothing runs on the Preloop pool — the
7-
# bake itself executes in release-golden on hosted runners.
5+
# compares the published pack's stamp against the versions.toml policy. When
6+
# stale, it opens a draft PR asking an operator to rebuild on a host with enough
7+
# disk. The schedule never dispatches the hosted golden bake.
88
on:
99
schedule:
10-
# Monday 04:00 UTC: at most one rebake per week when indices rot past policy.
10+
# Monday 04:00 UTC: at most one reminder per week when indices exceed policy.
1111
- cron: "0 4 * * 1"
1212
workflow_dispatch:
1313

1414
permissions:
15-
contents: read
16-
actions: write
15+
contents: write
16+
pull-requests: write
1717

1818
jobs:
1919
check:
20-
name: Rebuild goldens past index policy
20+
name: Open draft PR when golden apt indices are stale
2121
runs-on: ubuntu-latest
2222
timeout-minutes: 10
2323
steps:
@@ -26,35 +26,115 @@ jobs:
2626
persist-credentials: false
2727
sparse-checkout: versions.toml
2828

29-
- name: Rebuild when the published pack is stale
29+
- name: Check published index freshness
30+
id: freshness
3031
env:
3132
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3233
run: |
3334
set -euo pipefail
3435
MAX_AGE_DAYS="$(grep '^apt_indices_max_age_days' versions.toml | cut -d'"' -f2)"
3536
test -n "$MAX_AGE_DAYS"
36-
TAG="$(gh release view --json tagName --jq .tagName)"
37+
TAG="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)"
3738
echo "policy: ${MAX_AGE_DAYS}d; latest release: $TAG"
38-
# A pack predating the stamp feature has no provenance field and
39-
# reads as infinitely stale: the first run self-heals by rebuilding.
39+
rm -rf /tmp/apt-prov
4040
if gh release download "$TAG" \
4141
--pattern 'preloop-ubuntu-24.04-x86_64.provenance.json' \
42-
--dir /tmp/apt-prov --clobber >/dev/null 2>&1; then
42+
--dir /tmp/apt-prov \
43+
--clobber \
44+
--repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
4345
STAMP="$(python3 -c 'import json; print(json.load(open("/tmp/apt-prov/preloop-ubuntu-24.04-x86_64.provenance.json")).get("apt_indices", {}).get("baked_at", ""))')"
4446
else
4547
echo "no provenance asset; treating pack as stale"
4648
STAMP=""
4749
fi
4850
if [ -z "$STAMP" ]; then
49-
echo "stale (no stamp); dispatching golden rebuild"
50-
gh workflow run release-golden --ref main
51+
{
52+
echo "stale=true"
53+
echo "stamp=missing"
54+
echo "age_days=unknown"
55+
} >> "$GITHUB_OUTPUT"
5156
exit 0
5257
fi
5358
AGE_DAYS="$(( ($(date -u +%s) - $(date -u -d "$STAMP" +%s)) / 86400 ))"
5459
echo "indices baked $STAMP (${AGE_DAYS}d old)"
60+
{
61+
echo "stamp=$STAMP"
62+
echo "age_days=$AGE_DAYS"
63+
} >> "$GITHUB_OUTPUT"
5564
if [ "$AGE_DAYS" -gt "$MAX_AGE_DAYS" ]; then
56-
echo "stale; dispatching golden rebuild"
57-
gh workflow run release-golden --ref main
65+
echo "stale=true" >> "$GITHUB_OUTPUT"
5866
else
67+
echo "stale=false" >> "$GITHUB_OUTPUT"
5968
echo "fresh; nothing to do"
6069
fi
70+
71+
- name: Open manual rebuild reminder
72+
if: steps.freshness.outputs.stale == 'true'
73+
env:
74+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
APT_INDEX_STAMP: ${{ steps.freshness.outputs.stamp }}
76+
APT_INDEX_AGE_DAYS: ${{ steps.freshness.outputs.age_days }}
77+
run: |
78+
set -euo pipefail
79+
REQUESTED_AT="$(date -u +%F)"
80+
BRANCH="chore/golden-apt-refresh-${REQUESTED_AT}"
81+
EXISTING_URL="$(gh pr list \
82+
--repo "$GITHUB_REPOSITORY" \
83+
--state open \
84+
--head "$BRANCH" \
85+
--json url \
86+
--jq '.[0].url // empty')"
87+
if [ -n "$EXISTING_URL" ]; then
88+
echo "rebuild reminder already open: $EXISTING_URL"
89+
exit 0
90+
fi
91+
92+
git config user.name "github-actions[bot]"
93+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
94+
git switch -c "$BRANCH"
95+
awk -v marker="# Golden rebuild requested ${REQUESTED_AT}: apt indices ${APT_INDEX_STAMP} (${APT_INDEX_AGE_DAYS}d old)." '
96+
/^# Golden rebuild requested / { next }
97+
/^apt_indices_max_age_days = / { print marker }
98+
{ print }
99+
' versions.toml > versions.toml.tmp
100+
mv versions.toml.tmp versions.toml
101+
git add versions.toml
102+
git commit -m "chore(golden): request apt index refresh [skip ci]"
103+
gh auth setup-git
104+
git fetch origin "$BRANCH" || true
105+
git push --force-with-lease origin "HEAD:refs/heads/$BRANCH"
106+
107+
BODY="$RUNNER_TEMP/golden-refresh-pr.md"
108+
cat > "$BODY" <<EOF
109+
## Manual golden rebuild required
110+
111+
The published x86_64 golden reports apt indices baked at
112+
**${APT_INDEX_STAMP}** (${APT_INDEX_AGE_DAYS} days old), beyond the
113+
policy in versions.toml.
114+
115+
This draft PR is a reminder only; do not merge it.
116+
117+
1. On the x86_64 Linux host with KVM and sufficient disk, run
118+
scripts/build-golden-x86_64.sh from current main.
119+
2. Publish the payload, checksum, bundle, signatures, and provenance
120+
sidecars to the current release.
121+
3. Confirm the published provenance has a fresh apt_indices.baked_at.
122+
4. Close this draft PR.
123+
EOF
124+
125+
if ! gh pr create \
126+
--repo "$GITHUB_REPOSITORY" \
127+
--draft \
128+
--base main \
129+
--head "$BRANCH" \
130+
--title "chore(golden): rebuild stale apt indices [skip preloop]" \
131+
--body-file "$BODY"; then
132+
EXISTING_URL="$(gh pr list \
133+
--repo "$GITHUB_REPOSITORY" \
134+
--state open \
135+
--head "$BRANCH" \
136+
--json url \
137+
--jq '.[0].url // empty')"
138+
test -n "$EXISTING_URL"
139+
echo "concurrent reminder already opened: $EXISTING_URL"
140+
fi

‎.github/workflows/release-golden.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
run: |
4343
set -euo pipefail
4444
if [ -z "$TARGET_TAG" ]; then
45-
TARGET_TAG="$(gh release view --json tagName --jq .tagName)"
45+
TARGET_TAG="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)"
4646
fi
4747
mkdir -p dist
4848
gh release download "$PREVIOUS_TAG" \

‎CHANGELOG.md‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,89 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
Releases before v0.27.0 predate the changelog.
99
## [Unreleased]
1010

11+
12+
### Fixed
13+
14+
- **Scheduled golden refreshes no longer launch an impossible hosted bake**:
15+
the apt-index freshness workflow now opens one idempotent draft PR asking for
16+
a manual host-side rebuild. Webhook `workflow_dispatch` boolean inputs are
17+
coerced before job conditions, so GitHub's string `"false"` no longer runs a
18+
truthy-gated job; the reuse path also resolves the target release without
19+
requiring a checkout.
20+
21+
- **Skipped checks now settle instead of remaining queued**: GitHub may return
22+
`404 Not Found` when an immediate completion PATCH races replication of a
23+
newly created check run. Preloop retries that narrow case, allowing
24+
submit-time `if:` skips to report `completed/skipped`. Automated golden
25+
reminder PRs now carry `[skip ci]` and `[skip preloop]` markers so the draft
26+
notification does not consume GitHub or Preloop runners.
27+
28+
- **Packed goldens on Apple Silicon bake again**: the Rosetta multiarch step
29+
spliced an apt-scoping fragment that ended in a newline, so the composed
30+
guest script had a line starting with `;` and every fresh packed-golden
31+
bake failed with `sh: 15: Syntax error: ";" unexpected`. The pool then fell
32+
back to direct per-runner creation from the plain Ubuntu base, so jobs ran
33+
without the official runner image's tools (`cmake`,
34+
`/opt/hostedtoolcache`). Regressed in 0.33.8; a test now parses the full
35+
composed script.
36+
37+
- **Runners register on images that run as a non-root user**: the official
38+
runner image declares `USER runner`, so the pool runs `preloop-runner
39+
configure` through `sudo`, whose `env_reset` dropped the injected
40+
`PRELOOP_RUNNER_TOKEN`. Every pooled runner failed provisioning with
41+
`--token <TOKEN> not provided`. The command now runs under
42+
`sudo --preserve-env`.
43+
44+
- **A workflow-set `PATH` is used verbatim**: when a job or step set
45+
`env: PATH`, the runner still prepended `/home/runner/.cargo/bin` and
46+
`/home/runner/go/bin` whenever those directories existed (as they do on
47+
the official runner image). GitHub-hosted runners leave an explicit `PATH`
48+
alone; the toolchain shims now only extend the default.
49+
50+
- **Runner no longer drops the job handed out at job completion**: when a
51+
job finished, `preloop-runner` aborted its in-flight `status=Busy` broker
52+
poll, the same pattern as actions/runner#4728. On github.com the service
53+
hands the next job out on that poll, so the job was lost and later cancelled
54+
as "not acquired by Runner". The poll now stays open across job completion
55+
and the new status goes out on the next poll. To match, the server ends a
56+
Busy poll within one second once the session's job has finished, rather than
57+
holding it for the full 50 seconds, so the following Online poll claims the
58+
next job without delay. The server still never dispatches a job on a Busy
59+
poll.
60+
- **Broker poll client timeout outlasts the server's long poll**: the runner
61+
gave up on `GET /message` after 50 s, the same length as the broker's
62+
long-poll window. A job claimed in the final moments of that window was
63+
written to a request the runner had already abandoned. The poll timeout is
64+
now 100 s, matching the official runner's default `SendTimeout`.
65+
- **Empty broker polls are no longer read as messages**: a `200` with a
66+
`null` body was treated as a message with id 0 and type `unknown`. It is
67+
now an empty poll, as in the official listener.
68+
69+
## [0.33.9] - 2026-10-02
70+
71+
### Changed
72+
73+
- **Standalone runner release builds target Linux and macOS.** Windows users
74+
can run the Linux runner under WSL2; the native Windows matrix leg and its
75+
release assets are no longer built. On a co-hosted Preloop engine, set
76+
`PRELOOP_GITHUB_SKIP_WORKFLOWS=release-runner.yml` to let GitHub Actions
77+
alone own that release workflow and avoid duplicate checks.
78+
- **Golden smolvm updated to 1.19.0** (`a6072aae`): the verified-release
79+
golden pin tracks upstream; `smolvm_min_version` remains the runtime floor.
80+
81+
### Fixed
82+
83+
- **Ubuntu 22.04 environment goldens on Apple Silicon.** Rosetta multiarch
84+
setup assumed Ubuntu 24.04's `ubuntu.sources`; the pinned 22.04 rootfs uses
85+
`/etc/apt/sources.list`. Both layouts now scope native apt sources to arm64
86+
before adding amd64 repositories, so 22.04 jobs can provision runners
87+
instead of looping on a missing-file error. The bake additionally uses a
88+
status-preserving sudo wrapper, so a refused passwordless sudo or a failed
89+
apt step fails the bake instead of producing a golden without the amd64
90+
loader.
91+
92+
## [0.33.8] - 2026-10-02
93+
1194
### Added
1295

1396
- The control plane now enforces per-namespace state and quotas on both store

‎crates/preloop-orchestrator/src/lib.rs‎

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3088,12 +3088,47 @@ for source in "$APT_DIR/sources.list" "$APT_DIR"/sources.list.d/*.list; do
30883088
[ -f "$source" ] || continue
30893089
[ "$source" = "$APT_DIR/sources.list.d/preloop-amd64.list" ] && continue
30903090
sed -i -E '/^deb[[:space:]]/ { /\[arch=/b; s/^deb[[:space:]]+\[/deb [arch=arm64 /; t; s/^deb[[:space:]]+/deb [arch=arm64] /; }' "$source"
3091-
done
3092-
"#,
3091+
done"#,
30933092
);
30943093
script
30953094
}
30963095

3096+
/// The complete guest script [`prepare_rosetta_multiarch`] runs, wrapped for
3097+
/// root-or-sudo. Separate so the composed text — not just its pieces — is
3098+
/// tested to parse: a stray newline in a spliced fragment once left a line
3099+
/// starting with `;`, every fresh golden bake failed with `sh: 15: Syntax
3100+
/// error`, and the pool silently fell back to plain Ubuntu runners.
3101+
fn rosetta_multiarch_script() -> String {
3102+
// The strict wrapper is load-bearing: a refused passwordless sudo or a
3103+
// failed apt step must surface as a nonzero exit. The lenient
3104+
// `|| true` form would report success and bake a golden whose forks all
3105+
// fail `test -f /lib64/ld-linux-x86-64.so.2` consumers.
3106+
run_as_root_or_sudo_strict(&format!(
3107+
"set -e; \
3108+
case \"$(uname -m)\" in \
3109+
aarch64|arm64) ;; \
3110+
*) echo 'guest is not arm64; rosetta multiarch install is a no-op' >&2; exit 0 ;; \
3111+
esac; \
3112+
{}; \
3113+
dpkg --add-architecture amd64; \
3114+
CODENAME=$(. /etc/os-release 2>/dev/null && echo \"$VERSION_CODENAME\"); \
3115+
[ -n \"$CODENAME\" ] || CODENAME=noble; \
3116+
: > /etc/apt/sources.list.d/preloop-amd64.list; \
3117+
for s in '' '-updates' '-backports' '-security'; do \
3118+
printf 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s%s main restricted universe multiverse\\n' \"$CODENAME\" \"$s\" \
3119+
>> /etc/apt/sources.list.d/preloop-amd64.list; \
3120+
done; \
3121+
apt-get update -qq; \
3122+
DEBIAN_FRONTEND=noninteractive \
3123+
apt-get install -y -qq --no-install-recommends \
3124+
libc6:amd64 libgcc-s1:amd64 libstdc++6:amd64 zlib1g:amd64 \
3125+
libsystemd0:amd64; \
3126+
sync; \
3127+
test -f /lib64/ld-linux-x86-64.so.2",
3128+
scope_rosetta_apt_sources("/etc/apt")
3129+
))
3130+
}
3131+
30973132
/// Install the amd64 loader + libc into an arm64 golden so dynamically
30983133
/// linked x86_64 binaries can run under Rosetta translation.
30993134
///
@@ -3122,34 +3157,7 @@ async fn prepare_rosetta_multiarch<P: VmProvider>(
31223157
// image's security-update glibc, whose mutual Breaks pins block
31233158
// installation. One suite per deb line; the one-line format misparses
31243159
// extra suites as components. `sync` flushes before forking the golden.
3125-
// The strict wrapper is load-bearing: a refused passwordless sudo or a
3126-
// failed apt step must surface as a nonzero exit. The lenient
3127-
// `|| true` form would report success and bake a golden whose forks all
3128-
// fail `test -f /lib64/ld-linux-x86-64.so.2` consumers.
3129-
let script = run_as_root_or_sudo_strict(&format!(
3130-
"set -e; \
3131-
case \"$(uname -m)\" in \
3132-
aarch64|arm64) ;; \
3133-
*) echo 'guest is not arm64; rosetta multiarch install is a no-op' >&2; exit 0 ;; \
3134-
esac; \
3135-
{}; \
3136-
dpkg --add-architecture amd64; \
3137-
CODENAME=$(. /etc/os-release 2>/dev/null && echo \"$VERSION_CODENAME\"); \
3138-
[ -n \"$CODENAME\" ] || CODENAME=noble; \
3139-
: > /etc/apt/sources.list.d/preloop-amd64.list; \
3140-
for s in '' '-updates' '-backports' '-security'; do \
3141-
printf 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s%s main restricted universe multiverse\\n' \"$CODENAME\" \"$s\" \
3142-
>> /etc/apt/sources.list.d/preloop-amd64.list; \
3143-
done; \
3144-
apt-get update -qq; \
3145-
DEBIAN_FRONTEND=noninteractive \
3146-
apt-get install -y -qq --no-install-recommends \
3147-
libc6:amd64 libgcc-s1:amd64 libstdc++6:amd64 zlib1g:amd64 \
3148-
libsystemd0:amd64; \
3149-
sync; \
3150-
test -f /lib64/ld-linux-x86-64.so.2",
3151-
scope_rosetta_apt_sources("/etc/apt")
3152-
));
3160+
let script = rosetta_multiarch_script();
31533161
let output = provider
31543162
.exec(golden, &["sh".to_owned(), "-c".to_owned(), script])
31553163
.await?;
@@ -7697,6 +7705,22 @@ chmod +x "$dest/bin/node"
76977705
assert_eq!(shell_quote("a'b"), "'a'\\''b'");
76987706
}
76997707

7708+
#[test]
7709+
fn rosetta_multiarch_script_parses_as_posix_sh() {
7710+
// A spliced fragment ending in a newline once put `;` at the start of
7711+
// line 15 (`sh: 15: Syntax error: ";" unexpected`): every fresh golden
7712+
// bake failed, and the pool fell back to plain-Ubuntu runners.
7713+
let output = std::process::Command::new("sh")
7714+
.args(["-n", "-c", &rosetta_multiarch_script()])
7715+
.output()
7716+
.unwrap();
7717+
assert!(
7718+
output.status.success(),
7719+
"rosetta multiarch script does not parse: {}",
7720+
String::from_utf8_lossy(&output.stderr)
7721+
);
7722+
}
7723+
77007724
/// The rootfs selected by `ubuntu-22.04` has one-line sources while
77017725
/// 24.04 uses deb822. Test the guest shell against both real layouts:
77027726
/// missing `ubuntu.sources` used to fail every environment-golden bake

0 commit comments

Comments
 (0)