Skip to content
Merged
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
1 change: 1 addition & 0 deletions rust/Cargo.lock

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

1 change: 1 addition & 0 deletions rust/agama-files/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition.workspace = true
agama-software = { version = "0.1.0", path = "../agama-software" }
agama-utils = { path = "../agama-utils" }
async-trait = "0.1.89"
gettext-rs = { version = "0.7.7", features = ["gettext-system"] }
strum = "0.27.2"
tempfile = "3.23.0"
thiserror = "2.0.17"
Expand Down
81 changes: 69 additions & 12 deletions rust/agama-files/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ use std::{
use agama_software::{self as software, Resolvable, ResolvableType};
use agama_utils::{
actor::{self, Actor, Handler, MessageHandler},
api::files::{
scripts::{self, ScriptsGroup, ScriptsRepository},
user_file, ScriptsConfig, UserFile,
api::{
files::{
scripts::{self, ScriptsGroup, ScriptsRepository},
user_file, Script, ScriptsConfig, UserFile,
},
question::QuestionSpec,
},
progress, question,
progress,
question::{self, ask_question, AskError},
};
use async_trait::async_trait;
use gettextrs::gettext;
use strum::IntoEnumIterator;
use tokio::sync::Mutex;

Expand All @@ -48,6 +53,8 @@ pub enum Error {
Software(#[from] software::service::Error),
#[error(transparent)]
Actor(#[from] actor::Error),
#[error(transparent)]
Questions(#[from] AskError),
}

const DEFAULT_SCRIPTS_DIR: &str = "run/agama/scripts";
Expand Down Expand Up @@ -142,29 +149,28 @@ impl Service {
}

pub async fn add_scripts(&mut self, config: ScriptsConfig) -> Result<(), Error> {
let mut repo = self.scripts.lock().await;
if let Some(scripts) = config.pre {
for pre in scripts {
repo.add(pre.into())?;
self.add_script(pre.into()).await?;
}
}

if let Some(scripts) = config.post_partitioning {
for post in scripts {
repo.add(post.into())?;
self.add_script(post.into()).await?;
}
}

if let Some(scripts) = config.post {
for post in scripts {
repo.add(post.into())?;
self.add_script(post.into()).await?;
}
}

let mut packages = vec![];
if let Some(scripts) = config.init {
for init in scripts {
repo.add(init.into())?;
self.add_script(init.into()).await?;
}
packages.push(Resolvable::new("agama-scripts", ResolvableType::Package));
}
Expand All @@ -176,6 +182,59 @@ impl Service {
.await?;
Ok(())
}

async fn add_script(&self, script: Script) -> Result<(), Error> {
let result = {
let mut repo = self.scripts.lock().await;
repo.add(script.clone())
};

let mut attempt = 1;
while let Err(error) = &result {
tracing::error!("Failed to write the script {}: {error}.", script.name());

// TRANSLATORS: %s is replaced by the script name.
let text = &gettext("Failed to retrieve the script %s. Do you want to try again?")
.replace("%s", script.name());
let question = QuestionSpec::new(text, "write_script_failed")
.with_yes_no_actions()
.with_data(&[
("attempt", &attempt.to_string()),
("details", &error.to_string()),
]);
let answer = ask_question(&self.questions, question).await?;
if answer.action == "No" {
return Ok(());
}
attempt += 1;
}

Ok(())
}

async fn write_file(&self, file: &UserFile) -> Result<(), Error> {
let mut attempt = 1;
while let Err(error) = file.write(&self.install_dir).await {
tracing::error!("Failed to write the file {}: {error}.", file.destination);

// TRANSLATORS: %s is replaced by the script name.
let text = &gettext("Failed to write the file %s. Do you want to try again?")
.replace("%s", &file.destination);
let question = QuestionSpec::new(text, "write_file_failed")
.with_yes_no_actions()
.with_data(&[
("attempt", &attempt.to_string()),
("details", &error.to_string()),
]);
let answer = ask_question(&self.questions, question).await?;
if answer.action == "No" {
return Ok(());
}
attempt += 1;
}

Ok(())
}
}

impl Actor for Service {
Expand Down Expand Up @@ -237,9 +296,7 @@ impl MessageHandler<message::RunScripts> for Service {
impl MessageHandler<message::WriteFiles> for Service {
async fn handle(&mut self, _message: message::WriteFiles) -> Result<(), Error> {
for file in &self.files {
if let Err(error) = file.write(&self.install_dir).await {
tracing::error!("Failed to write file {}: {error}", file.destination);
}
self.write_file(file).await?;
}
Ok(())
}
Expand Down
5 changes: 5 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
-------------------------------------------------------------------
Wed Mar 18 22:29:10 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

- Report problems when writing files and scripts (gh#agama-project/agama#3301).

-------------------------------------------------------------------
Tue Mar 17 14:49:14 UTC 2026 - Michal Filka <mfilka@suse.com>

Expand Down
5 changes: 5 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
-------------------------------------------------------------------
Wed Mar 18 22:29:48 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

- Initial support for questions with details (gh#agama-project/agama#3301).

-------------------------------------------------------------------
Tue Mar 17 17:07:58 UTC 2026 - David Diaz <dgonzalez@suse.com>

Expand Down
24 changes: 24 additions & 0 deletions web/src/components/questions/GenericQuestion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ const question: Question = {
defaultAction: "sometimes",
};

const questionWithDetails: Question = {
id: 1,
text: "Failed to fetch the script. Do you want to retry?",
class: "failed-script",
field: { type: FieldType.None },
actions: [
{ id: "yes", label: "Yes" },
{ id: "no", label: "No" },
],
data: {
details: "Some details",
},
defaultAction: "yes",
};

const answerFn = jest.fn();

const renderQuestion = () =>
Expand All @@ -51,6 +66,15 @@ describe("GenericQuestion", () => {
await screen.findByText(question.text);
});

describe("if there are details", () => {
it("renders the question details", async () => {
plainRender(<GenericQuestion question={questionWithDetails} answerCallback={answerFn} />);

await screen.findByText(questionWithDetails.text);
await screen.findByText(questionWithDetails.data.details);
});
});

it("sets chosen option and calls the callback after user clicking an action", async () => {
const { user } = renderQuestion();

Expand Down
5 changes: 4 additions & 1 deletion web/src/components/questions/GenericQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ export default function GenericQuestion({
answerCallback(question);
};

const details = question.data?.details;

return (
<Popup isOpen aria-label={_("Question")}>
<Content>{question.text}</Content>
<Content component="p">{question.text}</Content>
{details && <Content>{details}</Content>}
<Popup.Actions>
<QuestionActions
actions={question.actions}
Expand Down
Loading