ci/lava: Avoid eval when generating env script
Some checks are pending
macOS-CI / macOS-CI (dri) (push) Waiting to run
macOS-CI / macOS-CI (xlib) (push) Waiting to run

Remove use of `eval` when writing `dut-job-env-vars.sh`, as it's
unnecessary. The script only needs to declare variables, not evaluate
them.

Using `eval` introduces parsing issues when variables contain both
single and double quotes, such as in commit titles. Example:
https://gitlab.freedesktop.org/mesa/mesa/-/jobs/77995175#L3188
This job failed to parse `CI_COMMIT_TITLE` and `CI_MERGE_REQUEST_TITLE`
correctly due to mixed quoting in:

    Revert "ci: disable Collabora's farm due to maintenance"

Signed-off-by: Guilherme Gallo <guilherme.gallo@collabora.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/35421>
This commit is contained in:
Guilherme Gallo 2025-06-09 14:47:53 -03:00
parent 655cf2f553
commit e1d54be524
7 changed files with 118 additions and 12 deletions

View file

@ -30,7 +30,7 @@ variables:
ALPINE_X86_64_BUILD_TAG: "20250423-rootfs"
ALPINE_X86_64_LAVA_SSH_TAG: "20250423-rootfs"
ALPINE_X86_64_LAVA_TRIGGER_TAG: "20250609-init"
ALPINE_X86_64_LAVA_TRIGGER_TAG: "20250610-eval"
FEDORA_X86_64_BUILD_TAG: "20250423-rootfs"

View file

@ -236,12 +236,24 @@ class LAVAJobDefinition:
return jwt_steps
def encode_job_env_vars(self) -> list[str]:
steps = []
with open(self.job_submitter.env_file, "rb") as f:
encoded = base64.b64encode(f.read()).decode()
safe_encoded = shlex.quote(encoded)
steps += [
f'echo {safe_encoded} | base64 -d >> /set-job-env-vars.sh',
]
return steps
def init_stage1_steps(self) -> list[str]:
run_steps = []
# job execution script:
# - inline .gitlab-ci/common/init-stage1.sh
# - fetch and unpack per-pipeline build artifacts from build job
# - fetch and unpack per-job environment from lava-submit.sh
# - fetch, unpack and encode per-job env from lava-submit.sh
# - exec .gitlab-ci/common/init-stage2.sh
with open(self.job_submitter.first_stage_init, "r") as init_sh:
@ -264,12 +276,7 @@ class LAVAJobDefinition:
# Forward environmental variables to the DUT
# base64-encoded to avoid YAML quoting issues
with open(self.job_submitter.env_file, "rb") as f:
encoded = base64.b64encode(f.read()).decode()
safe_encoded = shlex.quote(encoded)
run_steps += [
f'echo "eval \\\"$(echo {safe_encoded} | base64 -d)\\\"" >> /set-job-env-vars.sh',
]
run_steps += self.encode_job_env_vars()
run_steps.append("export CURRENT_SECTION=dut_boot")

View file

@ -89,7 +89,7 @@ actions:
steps:
- |-
echo test FASTBOOT
echo "eval \"$(echo ZWNobyB0ZXN0IEZBU1RCT09U | base64 -d)\"" >> /set-job-env-vars.sh
echo ZWNobyB0ZXN0IEZBU1RCT09U | base64 -d >> /set-job-env-vars.sh
export CURRENT_SECTION=dut_boot
- export -p > /dut-env-vars.sh
- test:

View file

@ -85,7 +85,7 @@ actions:
run:
steps:
- echo test FASTBOOT
- echo "eval \"$(echo ZWNobyB0ZXN0IEZBU1RCT09U | base64 -d)\"" >> /set-job-env-vars.sh
- echo ZWNobyB0ZXN0IEZBU1RCT09U | base64 -d >> /set-job-env-vars.sh
- export CURRENT_SECTION=dut_boot
- set -e
- echo Could not find jwt file, disabling S3 requests...

View file

@ -60,7 +60,7 @@ actions:
steps:
- |-
echo test UBOOT
echo "eval \"$(echo ZWNobyB0ZXN0IFVCT09U | base64 -d)\"" >> /set-job-env-vars.sh
echo ZWNobyB0ZXN0IFVCT09U | base64 -d >> /set-job-env-vars.sh
export CURRENT_SECTION=dut_boot
- export -p > /dut-env-vars.sh
- test:

View file

@ -58,7 +58,7 @@ actions:
run:
steps:
- echo test UBOOT
- echo "eval \"$(echo ZWNobyB0ZXN0IFVCT09U | base64 -d)\"" >> /set-job-env-vars.sh
- echo ZWNobyB0ZXN0IFVCT09U | base64 -d >> /set-job-env-vars.sh
- export CURRENT_SECTION=dut_boot
- set -e
- echo Could not find jwt file, disabling S3 requests...

View file

@ -1,6 +1,7 @@
import importlib
import os
import re
import subprocess
from itertools import chain
from pathlib import Path
from typing import Any, Iterable, Literal
@ -217,3 +218,101 @@ def test_lava_job_definition(
# Check that the generated job definition matches the expected one
assert job_dict == expected_job_dict
@pytest.mark.parametrize(
"directive",
["declare -x", "export"],
)
@pytest.mark.parametrize(
"original_env_output",
[
# Test basic environment variables
"FOO=bar\nBAZ=qux",
# Test export statements
"{directive} FOO=bar",
# Test multiple exports
"{directive} FOO=bar\n{directive} BAZ=qux\nNORM=val",
# Test mixed content with export
"{directive} FOO=bar\nBAZ=qux\n{directive} HELLO=world",
# Test empty file
"",
# Test special characters that need shell quoting
"FOO='bar baz'\nQUOTE=\"hello world\"",
# Test variables with spaces and quotes
"{directive} VAR='val spaces'\nQUOTES=\"test\"",
# Test inline scripts with export
"{directive} FOO=bar\nBAZ=qux\n{directive} HELLO=world",
# Test single quote inside double quotes in variable
"{directive} FOO='Revert \"commit's error\"'",
# Test backticks in variable
"{directive} FOO=`echo 'test'`",
],
ids=[
"basic_vars",
"single_export",
"multiple_exports",
"mixed_exports",
"empty_file",
"special_chars",
"spaces_and_quotes",
"inline_scripts_with_export",
"single_quote_in_var",
"backticks",
]
)
def test_encode_job_env_vars(directive, original_env_output, shell_file, clear_env_vars):
"""Test the encode_job_env_vars function with various environment file contents."""
import base64
import shlex
# Create environment file with test content
original_env_output = original_env_output.format(directive=directive)
env_file = shell_file(original_env_output)
# Create job submitter with the environment file
job_submitter = mock.MagicMock(spec=LAVAJobSubmitter, env_file=env_file)
job_definition = LAVAJobDefinition(job_submitter)
# Call the function under test
result = job_definition.encode_job_env_vars()
# Verify the result is a list with exactly one element
assert isinstance(result, list)
assert len(result) == 1
# Extract the command from the result
command = result[0]
assert isinstance(command, str)
# Extract the base64 encoded part
start_marker = 'echo '
end_marker = ' | base64 -d'
start_idx = command.find(start_marker) + len(start_marker)
end_idx = command.find(end_marker)
redirect_idx = command.find(">")
encoded_part = command[start_idx:end_idx]
# Verify if the script is executed correctly
env_script_process = subprocess.run(
["bash", "-c", command[:redirect_idx]], capture_output=True, text=True
)
if env_script_process.returncode != 0:
pytest.fail(f"Failed to execute script: {env_script_process.stderr}")
generated_env_output = env_script_process.stdout.strip()
# The encoded part should be shell-quoted, so we need to parse it
# Use shlex to unquote the encoded content
unquoted_encoded = shlex.split(encoded_part)[0]
# Decode the base64 content
try:
decoded_content = base64.b64decode(unquoted_encoded).decode()
except Exception as e:
pytest.fail(f"Failed to decode base64 content: {e}. Encoded part: {encoded_part}")
# Verify the decoded content matches the original file content
assert decoded_content == original_env_output == generated_env_output