Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/install-tmt/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ description: 'Install a pinned version of tmt (Test Management Tool)'
runs:
using: 'composite'
steps:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12.14'
- name: Install tmt
shell: bash
run: |
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:

# Run basic validation checks (linting, formatting, etc)
validate:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
- name: Bootc Ubuntu Setup
Expand All @@ -149,7 +149,7 @@ jobs:
run: just validate
# Check for security vulnerabilities and license compliance
cargo-deny:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
- uses: EmbarkStudios/cargo-deny-action@v2
Expand All @@ -167,7 +167,7 @@ jobs:
# Re-enable once the underlying hang is root-caused.
if: false && needs.compute-ci-level.outputs.run_heavy == 'true'
needs: compute-ci-level
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
Expand Down Expand Up @@ -236,7 +236,7 @@ jobs:
done
# Test that we can build documentation
docs:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
- name: Bootc Ubuntu Setup
Expand All @@ -254,7 +254,7 @@ jobs:
matrix:
test_os: ${{ fromJson(needs.compute-ci-level.outputs.package_os_matrix) }}

runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
# Rawhide is best-effort; don't let it block merges
continue-on-error: ${{ matrix.test_os == 'fedora-46' }}

Expand Down Expand Up @@ -344,7 +344,7 @@ jobs:
# - bootloader: grub-cc
# seal_state: sealed

runs-on: ubuntu-24.04
runs-on: ubuntu-26.04

steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -443,7 +443,7 @@ jobs:
- test_os: fedora-44
variant: composefs

runs-on: ubuntu-24.04
runs-on: ubuntu-26.04

steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -505,7 +505,7 @@ jobs:
# centos-9 ships an older dracut that lacks the auto-install of setup-root-conf.toml
- test_os: centos-9

runs-on: ubuntu-24.04
runs-on: ubuntu-26.04

steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -571,7 +571,7 @@ jobs:
# We need to change to use coreos-assembler.
if: false
needs: [compute-ci-level, package]
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04

steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -614,7 +614,7 @@ jobs:
test-container-export:
if: needs.compute-ci-level.outputs.run_heavy == 'true'
needs: [compute-ci-level, package]
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04

steps:
- uses: actions/checkout@v7
Expand Down
2 changes: 1 addition & 1 deletion crates/utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ rustix = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
shlex = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["process", "rt", "macros"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
Expand All @@ -26,6 +25,7 @@ tracing-journald = { workspace = true }
[dev-dependencies]
similar-asserts = { workspace = true }
static_assertions = { workspace = true }
tempfile = { workspace = true }

[lints]
workspace = true
43 changes: 38 additions & 5 deletions crates/utils/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ use std::{

use anyhow::{Context, Result};

/// Create a seekable, filesystem-independent file for command output.
fn command_output_file() -> Result<std::fs::File> {
// bootc's command helpers run from a systemd generator. Generators on
// systemd 252 and older may see a read-only /tmp (253+ provides a private
// writable /tmp), so output capture must not rely on filesystem temp files.
rustix::fs::memfd_create("bootc-command-output", rustix::fs::MemfdFlags::CLOEXEC)
.map(std::fs::File::from)
.context("create memfd for command output")
}

/// Helpers intended for [`std::process::Command`].
pub trait CommandRunExt {
/// Log (at debug level) the full child commandline.
Expand Down Expand Up @@ -139,7 +149,7 @@ impl CommandRunExt for Command {

/// Synchronously execute the child, and return an error if the child exited unsuccessfully.
fn run_capture_stderr(&mut self) -> Result<()> {
let stderr = tempfile::tempfile()?;
let stderr = command_output_file()?;
self.stderr(stderr.try_clone()?);
tracing::trace!("exec: {self:?}");
self.status()?.check_status_with_stderr(stderr)
Expand Down Expand Up @@ -168,7 +178,7 @@ impl CommandRunExt for Command {
}

fn run_get_output(&mut self) -> Result<Box<dyn std::io::BufRead>> {
let mut stdout = tempfile::tempfile()?;
let mut stdout = command_output_file()?;
self.stdout(stdout.try_clone()?);
self.run_capture_stderr()?;
stdout.seek(std::io::SeekFrom::Start(0)).context("seek")?;
Expand Down Expand Up @@ -220,7 +230,7 @@ pub trait AsyncCommandRunExt {

impl AsyncCommandRunExt for tokio::process::Command {
async fn run(&mut self) -> Result<()> {
let stderr = tempfile::tempfile()?;
let stderr = command_output_file()?;
self.stderr(stderr.try_clone()?);
self.status().await?.check_status_with_stderr(stderr)
}
Expand Down Expand Up @@ -283,6 +293,22 @@ mod tests {
);
}

#[test]
fn command_output_file_is_a_memfd() {
use std::os::fd::AsRawFd;

let file = command_output_file().unwrap();
// An unprivileged test cannot reliably make /tmp read-only, so verify
// directly that the capture backing file is a memfd instead.
let fd_path = format!("/proc/self/fd/{}", file.as_raw_fd());
let target = std::fs::read_link(fd_path).unwrap();
assert!(
target
.to_string_lossy()
.contains("memfd:bootc-command-output")
);
}

#[test]
fn exit_status_check_status() {
use std::process::Command;
Expand All @@ -307,14 +333,14 @@ mod tests {

// Test successful exit status
let mut success_status = Command::new("true").status().unwrap();
let temp_stderr = tempfile::tempfile().unwrap();
let temp_stderr = command_output_file().unwrap();
success_status
.check_status_with_stderr(temp_stderr)
.unwrap();

// Test failed exit status with stderr content
let mut fail_status = Command::new("false").status().unwrap();
let mut temp_stderr = tempfile::tempfile().unwrap();
let mut temp_stderr = command_output_file().unwrap();
write!(temp_stderr, "test error message").unwrap();
let e = fail_status
.check_status_with_stderr(temp_stderr)
Expand Down Expand Up @@ -351,6 +377,13 @@ mod tests {
let (success, fail) = tokio::join!(success.run(), fail.run(),);
success.unwrap();
assert!(fail.is_err());

let error = AsyncCommand::new("/bin/sh")
.args(["-c", "echo expected-async-error 1>&2; exit 1"])
.run()
.await
.unwrap_err();
assert!(error.to_string().contains("expected-async-error"));
}

#[test]
Expand Down
36 changes: 33 additions & 3 deletions crates/xtask/src/tmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ fn sanitize_plan_name(plan: &str) -> String {
}
}

fn boot_context(boot_type: &crate::BootType, seal_state: Option<&SealState>) -> [String; 2] {
[
format!("--context=boot_type={boot_type}"),
format!(
"--context=seal_state={}",
seal_state.map_or("unspecified".to_string(), ToString::to_string)
),
]
}

/// Check that required dependencies are available
#[context("Checking dependencies")]
fn check_dependencies(sh: &Shell) -> Result<()> {
Expand Down Expand Up @@ -378,6 +388,7 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> {
.chain(std::iter::once(format!(
"--context=VARIANT_ID={variant_id}"
)))
.chain(boot_context(&args.boot_type, args.seal_state.as_ref()))
.collect::<Vec<_>>();
let preserve_vm = args.preserve_vm;

Expand Down Expand Up @@ -425,9 +436,13 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> {

// Get the list of plans
println!("Discovering test plans...");
let plans_output = cmd!(sh, "tmt plan ls")
.read()
.context("Getting list of test plans")?;
let discovery_context = context.clone();
let plans_output = cmd!(
sh,
"tmt {discovery_context...} plan ls --filter enabled:true"
)
.read()
.context("Getting list of test plans")?;

let mut plans: Vec<&str> = plans_output
.lines()
Expand Down Expand Up @@ -1399,6 +1414,21 @@ fn generate_integration() -> Result<(String, String)> {
mod tests {
use super::*;

#[test]
fn test_boot_context_values() {
assert_eq!(
boot_context(&crate::BootType::Uki, Some(&SealState::Sealed)),
["--context=boot_type=uki", "--context=seal_state=sealed"]
);
assert_eq!(
boot_context(&crate::BootType::Bls, None),
[
"--context=boot_type=bls",
"--context=seal_state=unspecified"
]
);
}

#[test]
fn test_parse_tmt_metadata_basic() {
let content = r#"# number: 1
Expand Down
Loading