Skip to content
Closed
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
72 changes: 72 additions & 0 deletions .github/workflows/flake-bump-auto-approve.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Flake bump auto approve
on:
pull_request:
paths:
- 'flake.lock'
branches:
- gardenlinux

jobs:
gitlint:
name: Flake bump auto approve
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Set up Python 3.11
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade gitlint
# this rule checks the prerequisits and write the exit code in its output
- name: Lint git commit messages
id: gitlint
run: |
set +e
gitlint --commits origin/$GITHUB_BASE_REF.. -C .gitlint_auto_approve
code=$?
if [ $code -eq 0 ]; then
echo "this merge request is eligible for a flake bump auto approve and merge"
else
echo "this merge request will not be automatically approved."
fi
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
exit 0
# the following steps only run if gitlint run successful
- name: Create variables
if: steps.gitlint.outputs.exit_code == '0'
id: create_variable
run: |
REPO=$(echo ${GITHUB_REPOSITORY} | cut -f 2 -d '/')
OWNER=$(echo ${GITHUB_REPOSITORY} | cut -f 1 -d '/')
echo "repo=$REPO" >> "$GITHUB_OUTPUT"
echo "owner=$OWNER" >> "$GITHUB_OUTPUT"
- name: Generate token
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a general question. Is all of this needed? In another repository, I can auto-approve all PRs automatically with much less boilerplate:

https://github.com/phip1611/spectrum-analyzer/blob/1e93a225b066b06821d99fe042a87ef304bc7c1d/.github/workflows/dependabot-auto-merge.yml#L21

WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to bikeshed here and perfectionize, just a general question.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any PR must be reviewed by 2 persons. If we want to auto approve & merge this PRs we need to define a bypass rule. This can be done with a dedicated GithubApp.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look into the README.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, thanks! :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll approve once rebased and when the PR targets the right branch again.

if: steps.gitlint.outputs.exit_code == '0' && steps.create_variable.outputs.repo != ''
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_AUTO_APPROVE_APP_ID }}
private-key: ${{ secrets.GH_AUTO_APPROVE_APP_PRIVATE_KEY }}
owner: ${{ steps.create_variable.outputs.owner }}
repositories: ${{ steps.create_variable.outputs.repo }}
- name: Merge Pull request
if: steps.gitlint.outputs.exit_code == '0' && steps.generate_token.outputs.token != ''
shell: bash
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
# GitHub CLI api
# https://cli.github.com/manual/gh_api
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.number }}/merge \
-f 'merge_method=rebase'
15 changes: 15 additions & 0 deletions .gitlint_auto_approve
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[general]
extra-path=ci/gitlint/rules_auto_approve
regex-style-search=true
ignore=body-is-missing,body-max-line-length

# default 72
[title-max-length]
line-length=72

# Empty bodies are fine
[body-min-length]
min-length=0

[UC-flake]
filepath=flake.lock
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions block/src/aligned_operation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause

use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::io;

use vm_memory::GuestAddress;

/// Owns an aligned bounce buffer used when a guest descriptor's host VA
/// does not meet the disk backend's alignment requirement.
#[derive(Debug)]
pub struct AlignedOperation {
data_addr: GuestAddress,
aligned_ptr: *mut u8,
size: usize,
layout: Layout,
}

impl AlignedOperation {
/// Allocate a zero-initialized buffer of `size` bytes aligned to
/// `alignment`. Returns `InvalidInput` if `size` is zero;
/// `alignment` must be a power of two and not exceed `isize::MAX`
/// after rounding up.
pub fn new(data_addr: GuestAddress, size: usize, alignment: usize) -> io::Result<Self> {
if size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"AlignedOperation requires a non-zero size",
));
}
let layout = Layout::from_size_align(size, alignment)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
// SAFETY: size is non-zero (checked above) and Layout::from_size_align
// rejects alignments that are not a power of two or that overflow.
let aligned_ptr = unsafe { alloc_zeroed(layout) };
if aligned_ptr.is_null() {
return Err(io::Error::last_os_error());
}
Ok(Self {
data_addr,
aligned_ptr,
size,
layout,
})
}

/// Gets the raw pointer to the aligned buffer.
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.aligned_ptr
}

/// Returns the aligned buffer as a slice.
pub fn as_bytes(&self) -> &[u8] {
// SAFETY: `new` allocates `size` bytes via alloc_zeroed (so they
// are initialized) and AlignedOperation owns the buffer
// exclusively.
unsafe { std::slice::from_raw_parts(self.aligned_ptr, self.size) }
}

/// Returns the aligned buffer as a mutable slice.
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
// SAFETY: same invariant as as_bytes; &mut self rules out other
// simultaneous borrows.
unsafe { std::slice::from_raw_parts_mut(self.aligned_ptr, self.size) }
}

/// Returns the guest address for this op.
pub fn data_addr(&self) -> GuestAddress {
self.data_addr
}
}

impl Drop for AlignedOperation {
fn drop(&mut self) {
// SAFETY: `new` is the only constructor, and it stores a pointer
// returned by `alloc_zeroed` paired with the exact `layout` used
// for that allocation. Ownership has not escaped (the type is
// neither `Clone` nor `Copy`).
unsafe {
dealloc(self.aligned_ptr, self.layout);
}
}
}

// SAFETY: AlignedOperation owns its heap allocation exclusively (no Clone/
// Copy, no shared aliases) and the allocation's lifetime is tied to the
// value's. Moving an AlignedOperation between threads transfers that
// ownership; the same rationale Box<T> uses for its Send impl.
unsafe impl Send for AlignedOperation {}
66 changes: 17 additions & 49 deletions block/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause

mod aligned_operation;
pub mod async_io;
pub mod fcntl;
pub mod fixed_vhd;
Expand All @@ -28,7 +29,7 @@ pub mod vhd;
pub mod vhdx;
pub mod vhdx_sync;

use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::alloc::{Layout, alloc_zeroed};
use std::collections::VecDeque;
use std::fmt::{self, Debug};
use std::fs::File;
Expand All @@ -40,6 +41,7 @@ use std::str::FromStr;
use std::time::Instant;
use std::{cmp, result};

pub use aligned_operation::AlignedOperation;
#[cfg(feature = "io_uring")]
use io_uring::{IoUring, Probe, opcode};
use libc::{S_IFBLK, S_IFMT, ioctl};
Expand Down Expand Up @@ -232,14 +234,6 @@ fn sector<B: Bitmap + 'static>(

const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32;

#[derive(Debug)]
pub struct AlignedOperation {
origin_ptr: u64,
aligned_ptr: u64,
size: usize,
layout: Layout,
}

pub struct BatchRequest {
pub offset: libc::off_t,
pub iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
Expand Down Expand Up @@ -473,31 +467,19 @@ impl Request {
let iov_base = if (origin_ptr.as_ptr() as u64).is_multiple_of(SECTOR_SIZE) {
origin_ptr.as_ptr() as *mut libc::c_void
} else {
let layout = Layout::from_size_align(data_len, SECTOR_SIZE as usize).unwrap();
// SAFETY: layout has non-zero size
let aligned_ptr = unsafe { alloc_zeroed(layout) };
if aligned_ptr.is_null() {
return Err(ExecuteError::TemporaryBufferAllocation(
io::Error::last_os_error(),
));
}
let mut aligned_op =
AlignedOperation::new(data_addr, data_len, SECTOR_SIZE as usize)
.map_err(ExecuteError::TemporaryBufferAllocation)?;

// We need to perform the copy beforehand in case we're writing
// data out.
if request_type == RequestType::Out {
// SAFETY: destination buffer has been allocated with
// the proper size.
unsafe { std::ptr::copy(origin_ptr.as_ptr(), aligned_ptr, data_len) };
mem.read_slice(aligned_op.as_bytes_mut(), data_addr)
.map_err(ExecuteError::Read)?;
}

// Store both origin and aligned pointers for complete_async()
// to process them.
self.aligned_operations.push(AlignedOperation {
origin_ptr: origin_ptr.as_ptr() as u64,
aligned_ptr: aligned_ptr as u64,
size: data_len,
layout,
});
let aligned_ptr = aligned_op.as_mut_ptr();
self.aligned_operations.push(aligned_op);

aligned_ptr as *mut libc::c_void
};
Expand Down Expand Up @@ -639,31 +621,17 @@ impl Request {
Ok(ret)
}

pub fn complete_async(&mut self) -> result::Result<(), Error> {
for aligned_operation in self.aligned_operations.drain(..) {
pub fn complete_async<B: Bitmap + 'static>(
&mut self,
mem: &vm_memory::GuestMemoryMmap<B>,
) -> result::Result<(), Error> {
for aligned_op in self.aligned_operations.drain(..) {
// We need to perform the copy after the data has been read inside
// the aligned buffer in case we're reading data in.
if self.request_type == RequestType::In {
// SAFETY: origin buffer has been allocated with the
// proper size.
unsafe {
std::ptr::copy(
aligned_operation.aligned_ptr as *const u8,
aligned_operation.origin_ptr as *mut u8,
aligned_operation.size,
);
};
mem.write_slice(aligned_op.as_bytes(), aligned_op.data_addr())
.map_err(Error::GuestMemory)?;
}

// Free the temporary aligned buffer.
// SAFETY: aligned_ptr was allocated by alloc_zeroed with the same
// layout
unsafe {
dealloc(
aligned_operation.aligned_ptr as *mut u8,
aligned_operation.layout,
);
};
}

Ok(())
Expand Down
8 changes: 8 additions & 0 deletions block/src/raw_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ impl AsyncIo for RawFileAsync {
let (submitter, mut sq, _) = self.io_uring.split();
let mut submitted = false;

// Refuse the whole batch if it can't fit in the SQ to avoid having to unroll a partially
// successful push.
if batch_request.len() > sq.capacity() - sq.len() {
return Err(AsyncIoError::SubmitBatchRequests(Error::other(
"io_uring submission queue is full",
)));
}

for req in batch_request {
match req.request_type {
RequestType::In => {
Expand Down
43 changes: 43 additions & 0 deletions ci/README.auto.approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Flake bump auto approve

## Description

We add a github workflow `Flake bump`.
First job of this workflow checks if a merge request contains only one commit which updates the `flake.lock` file.
If this condition is met the second job approve this merge request and automatically merge it.
The approval is done with a dedicated GitHubApp.

## Install

* Follow this guide: https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app
* Create a GitHub app `auto-approve-app` in your GH organization
* github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> New GitHub App
* Add a name and Homepage URL
* Add Repository Permissions
* Actions: RO
* Contents: RW
* Metadata: RO
* Pull Requests: RW
* Workflows: RW

* Install this app into your organization
* github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` -> Install App
* Only select repositories:
* repository-name

* Find app_id
* github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app`
* you find the app_id in the `General` section

* Create app client secret
* github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` -> Client secrets
* The private key will be downloaded using your browser
* Save it in 1Password or vault

* Create two organization secrets:
* GH_AUTO_APPROVE_APP_ID
* GH_AUTO_APPROVE_APP_PRIVATE_KEY

* Add Github App `auto-approve-app` to your branch ruleset.
* github.com/github-organization/repository -> Settings -> Rules -> Rulesets -> rule name -> Bypass list -> Add bypass
* This allows the Github App `auto-approve-app` to merge the MRs even if other conditions of the ruleset are not met.
Loading
Loading