From 67a692c1fe4194cde54722e9ce7bc417a99d36c1 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 27 May 2026 15:57:08 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20OARS=20content=20rating=20and?= =?UTF-8?q?=20initial=20release=20block=20to=20metainfo.x=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GSD context: - Milestone: M001 - Fix Flatpak Build + CI Pipeline - Slice: S03 - Task: T02 - Added OARS content rating and initial release block to metainfo.xml; all appstreamcli validation infos resolved GSD-Task: S03/T02 --- org.droposs.client.metainfo.xml | 27 ++++- scripts/verify-s01-build.sh | 115 ++++++++++++++++++- tests/validate-nightly-workflow.py | 174 +++++++++++++++++++++++++++++ tests/validate-nightly-yaml.py | 92 +++++++++++++++ 4 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 tests/validate-nightly-workflow.py create mode 100644 tests/validate-nightly-yaml.py diff --git a/org.droposs.client.metainfo.xml b/org.droposs.client.metainfo.xml index a581766..873b51b 100644 --- a/org.droposs.client.metainfo.xml +++ b/org.droposs.client.metainfo.xml @@ -18,10 +18,35 @@ org.droposs.client.desktop https://github.com/Drop-OSS/drop-app https://github.com/Drop-OSS/drop-app/issues - Drop OSS + + Drop OSS + Game + + none + none + none + none + none + none + none + none + none + none + none + none + none + none + + + + +

Initial Flatpak packaging release.

+
+
+
1280 diff --git a/scripts/verify-s01-build.sh b/scripts/verify-s01-build.sh index 6c22025..24fe0fd 100755 --- a/scripts/verify-s01-build.sh +++ b/scripts/verify-s01-build.sh @@ -12,6 +12,21 @@ APP_ID="org.droposs.client" BINARY_NAME="drop-app" FLATPAK_BUILDER="flatpak run --share=network org.flatpak.Builder" +APPSTREAMCLI="appstreamcli" +DESKTOP_FILE_VALIDATE="desktop-file-validate" + +# Tool-availability guards for non-build checks +for tool_cmd in "$APPSTREAMCLI" "$DESKTOP_FILE_VALIDATE"; do + if ! command -v "$tool_cmd" &>/dev/null; then + tool_name=$(basename "$tool_cmd") + echo "ERROR: $tool_cmd not found" + case "$tool_name" in + appstreamcli) echo " Install with: sudo dnf install appstream" ;; + desktop-file-validate) echo " Install with: sudo dnf install desktop-file-utils" ;; + esac + exit 2 + fi +done PASS_COUNT=0 FAIL_COUNT=0 @@ -64,8 +79,8 @@ else done done echo "" - echo "FAILED: Build did not succeed. Check logs above." - exit 1 + echo "FAILED: Build did not succeed. Subsequent source-file checks (5-7) will still run." + BUILD_FAILED=1 fi # Step 2: Verify binary exists @@ -128,6 +143,102 @@ else fi fi +# Step 5: AppStream metadata validation +echo "" +echo "--- Step 5: AppStream metadata validation ---" +METAINFO_FILE="$PROJECT_DIR/org.droposs.client.metainfo.xml" +if test -f "$METAINFO_FILE"; then + set +e + APPSTREAM_OUTPUT=$("$APPSTREAMCLI" validate "$METAINFO_FILE" 2>&1) + APPSTREAM_EXIT=$? + set -e + echo "$APPSTREAM_OUTPUT" + if [ "$APPSTREAM_EXIT" -eq 0 ]; then + pass "appstreamcli validate returned exit 0 (warnings/infos are non-fatal)" + else + fail "appstreamcli validate failed (exit $APPSTREAM_EXIT) — check AppStream metadata errors above" + fi +else + fail "AppStream metainfo file not found at $METAINFO_FILE" +fi + +# Step 6: Desktop file validation +echo "" +echo "--- Step 6: Desktop file validation ---" +DESKTOP_FILE="$PROJECT_DIR/org.droposs.client.desktop" +if test -f "$DESKTOP_FILE"; then + set +e + DESKTOP_OUTPUT=$("$DESKTOP_FILE_VALIDATE" "$DESKTOP_FILE" 2>&1) + DESKTOP_EXIT=$? + set -e + if [ -n "$DESKTOP_OUTPUT" ]; then + echo "$DESKTOP_OUTPUT" + else + echo "desktop-file-validate produced no output (clean)" + fi + if [ "$DESKTOP_EXIT" -eq 0 ]; then + pass "desktop-file-validate passed (exit 0)" + else + fail "desktop-file-validate failed (exit $DESKTOP_EXIT)" + fi +else + fail "Desktop file not found at $DESKTOP_FILE" +fi + +# Step 7: Sandbox permission audit +echo "" +echo "--- Step 7: Sandbox permission audit ---" +MANIFEST_YML="$PROJECT_DIR/org.droposs.client.yml" +if test -f "$MANIFEST_YML"; then + set +e + SANDBOX_AUDIT=$(python3 -c " +import yaml, sys +with open('$MANIFEST_YML') as f: + doc = yaml.safe_load(f) +finish_args = doc.get('finish-args', []) +if not finish_args: + print('No finish-args found in manifest.') + sys.exit(0) + +critical = 0 +notable = 0 +benign = 0 + +CRITICAL_FLAGS = {'--device=all', '--filesystem=host', '--filesystem=home', '--socket=system-bus'} +NOTABLE_FLAGS_STARTSWITH = ('--share=', '--talk-name=', '--own-name=') + +for arg in finish_args: + arg_s = str(arg).strip() + if arg_s in CRITICAL_FLAGS: + label = 'CRITICAL' + critical += 1 + elif arg_s.startswith(NOTABLE_FLAGS_STARTSWITH): + label = 'notable' + notable += 1 + else: + label = 'benign' + benign += 1 + print(f' [{label}] {arg_s}') + +print() +print(f' Critical: {critical}, notable: {notable}, benign: {benign}') +sys.exit(critical) +" 2>&1) + SANDBOX_EXIT=$? + set -e + echo "$SANDBOX_AUDIT" + if [ "$SANDBOX_EXIT" -eq 0 ]; then + pass "sandbox audit: no critical permissions found" + else + # Count criticals from Python exit code + CRIT_COUNT=$SANDBOX_EXIT + fail "sandbox audit: $CRIT_COUNT critical permission(s) found — remove --device=all, --filesystem=host, --filesystem=home, or --socket=system-bus" + echo " Note: --share=network is notable but required for Tauri app networking" + fi +else + fail "Manifest file not found at $MANIFEST_YML" +fi + # Summary echo "" echo "============================================" diff --git a/tests/validate-nightly-workflow.py b/tests/validate-nightly-workflow.py new file mode 100644 index 0000000..92dae39 --- /dev/null +++ b/tests/validate-nightly-workflow.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Validate the nightly workflow YAML structure and completeness. + +Asserts all required triggers, steps, and artifact settings +are present in .github/workflows/nightly.yml. +""" + +import sys +import yaml +from pathlib import Path + +WORKFLOW_PATH = Path(__file__).resolve().parent.parent / ".github" / "workflows" / "nightly.yml" + +def load_workflow(): + assert WORKFLOW_PATH.exists(), f"Workflow file not found: {WORKFLOW_PATH}" + with open(WORKFLOW_PATH) as f: + return yaml.safe_load(f) + + +def test_triggers(wf): + """Verify schedule cron and workflow_dispatch trigger.""" + # PyYAML 5.x parses 'on' as boolean True (YAML 1.1 spec). + # Access the triggers block via the True key. + on_block = wf.get(True, {}) + if not on_block and "on" in wf: + on_block = wf.get("on", {}) + assert on_block, "Missing 'on' triggers block in workflow" + + # Schedule trigger with cron + schedule = on_block.get("schedule", []) + assert isinstance(schedule, list) and len(schedule) > 0, "Missing on.schedule trigger" + cron = schedule[0].get("cron", "") + assert "0 3 *" in cron, f"Expected cron containing '0 3 *', got: {cron}" + + # Manual trigger + assert "workflow_dispatch" in on_block, "Missing on.workflow_dispatch trigger" + + +def test_job_config(wf): + """Verify job-level configuration.""" + jobs = wf.get("jobs", {}) + assert "nightly" in jobs, "Missing 'nightly' job in jobs" + job = jobs["nightly"] + + assert job.get("runs-on") == "ubuntu-latest", \ + f"Expected runs-on 'ubuntu-latest', got: {job.get('runs-on')}" + assert job.get("timeout-minutes") == 120, \ + f"Expected timeout-minutes 120, got: {job.get('timeout-minutes')}" + + +def test_steps(wf): + """Verify all required workflow steps are present.""" + job = wf["jobs"]["nightly"] + steps = job.get("steps", []) + assert isinstance(steps, list) and len(steps) > 0, "No steps defined" + + # Collect all step `run` strings and `uses` strings for inspection + run_texts = [] + uses_entries = [] + step_names = [] + + for step in steps: + name = step.get("name", "") + run = step.get("run", "") + uses = step.get("uses", "") + step_names.append(name) + if run: + run_texts.append(run) + if uses: + uses_entries.append(uses) + + # Checkout step + assert "actions/checkout@v4" in uses_entries, \ + "Missing actions/checkout@v4 step" + + # apt install flatpak / flatpak-builder + apt_found = any( + "apt-get install" in run and "flatpak" in run + for run in run_texts + ) + assert apt_found, "Missing apt-get install step for flatpak" + + # Flathub runtime install (GNOME Platform/SDK) + flathub_found = any( + "flatpak install" in run + and "org.gnome.Platform" in run + and "org.gnome.Sdk" in run + for run in run_texts + ) + assert flathub_found, "Missing flatpak install step for GNOME Platform/SDK" + + # Cache restore + assert "actions/cache/restore@v4" in uses_entries, \ + "Missing actions/cache/restore@v4 step" + + # flatpak-builder --force-clean + builder_found = any( + "flatpak-builder" in run and "--force-clean" in run + for run in run_texts + ) + assert builder_found, "Missing flatpak-builder --force-clean step" + + # Verify build step + verify_found = any( + "verify" in name.lower() for name in step_names + ) + assert verify_found, "Missing verify build step" + + # flatpak build-bundle + bundle_found = any( + "flatpak build-bundle" in run for run in run_texts + ) + assert bundle_found, "Missing flatpak build-bundle step" + + # Cache save + assert "actions/cache/save@v4" in uses_entries, \ + "Missing actions/cache/save@v4 step" + + # Upload artifact + upload_uses = any("actions/upload-artifact@v4" in uses for uses in uses_entries) + assert upload_uses, "Missing actions/upload-artifact@v4 step" + + # Upload artifact retention-days: 90 + upload_step = None + for step in steps: + if "actions/upload-artifact@" in step.get("uses", ""): + upload_step = step + break + assert upload_step is not None, "Could not locate upload-artifact step for retention check" + assert upload_step.get("with", {}).get("retention-days") == 90, \ + f"Expected retention-days 90, got: {upload_step.get('with', {}).get('retention-days')}" + + return len(steps) + + +def main(): + print(f"Validating workflow: {WORKFLOW_PATH}") + + wf = load_workflow() + failures = [] + step_count = 0 + + tests = { + "triggers (cron + workflow_dispatch)": test_triggers, + "job config (runs-on, timeout)": test_job_config, + "steps (flatpak-builder, build-bundle, upload, etc.)": test_steps, + } + + for label, test_fn in tests.items(): + try: + if test_fn is test_steps: + step_count = test_fn(wf) + else: + test_fn(wf) + print(f" PASS: {label}") + except AssertionError as e: + print(f" FAIL: {label} — {e}") + failures.append((label, str(e))) + + print(f"\nStep count: {step_count}") + print(f"Tests passed: {len(tests) - len(failures)} / {len(tests)}") + + if failures: + print(f"\n{len(failures)} failure(s):") + for label, msg in failures: + print(f" - {label}: {msg}") + sys.exit(1) + + print("\nAll assertions passed!") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/tests/validate-nightly-yaml.py b/tests/validate-nightly-yaml.py new file mode 100644 index 0000000..16aa54a --- /dev/null +++ b/tests/validate-nightly-yaml.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Validate .github/workflows/nightly.yml is a valid GitHub Actions workflow document. + +Checks YAML correctness and top-level schema: the document must be a dict +with required keys (name, on, jobs) in the expected shapes. +""" + +import sys +import yaml +from pathlib import Path + +WORKFLOW_PATH = Path(__file__).resolve().parent.parent / ".github" / "workflows" / "nightly.yml" + + +def load_workflow(): + assert WORKFLOW_PATH.exists(), f"Workflow file not found: {WORKFLOW_PATH}" + with open(WORKFLOW_PATH) as f: + return yaml.safe_load(f) + + +def main(): + print(f"Validating YAML correctness: {WORKFLOW_PATH}") + + # 1. Load YAML + wf = load_workflow() + + # 2. Top-level must be a dict (not None, not a list) + assert isinstance(wf, dict), ( + f"Top-level document must be a dict, got: {type(wf).__name__}" + ) + + # 3. name: non-empty string + name = wf.get("name") + assert isinstance(name, str) and len(name) > 0, ( + f"'name' must be a non-empty string, got: {name!r}" + ) + + # 4. on: dict with at least one trigger key + # PyYAML 5.x parses 'on' as boolean True (YAML 1.1), so try both. + on_block = wf.get(True) or wf.get("on") or {} + assert isinstance(on_block, dict), ( + f"'on' must be a dict, got: {type(on_block).__name__}" + ) + assert len(on_block) >= 1, ( + f"'on' must define at least one trigger, got {len(on_block)} key(s)" + ) + + # 5. jobs: dict with at least one job + jobs = wf.get("jobs", {}) + assert isinstance(jobs, dict), ( + f"'jobs' must be a dict, got: {type(jobs).__name__}" + ) + assert len(jobs) >= 1, ( + f"'jobs' must contain at least one job, got {len(jobs)} job(s)" + ) + + # 6. Each job: runs-on (string) + steps (non-empty list) + timeout-minutes + for job_id, job in jobs.items(): + runs_on = job.get("runs-on") + assert isinstance(runs_on, str) and len(runs_on) > 0, ( + f"job '{job_id}': 'runs-on' must be a non-empty string, got: {runs_on!r}" + ) + + timeout = job.get("timeout-minutes") + assert timeout is not None, ( + f"job '{job_id}': 'timeout-minutes' is required but missing" + ) + + steps = job.get("steps", []) + assert isinstance(steps, list) and len(steps) > 0, ( + f"job '{job_id}': 'steps' must be a non-empty list, " + f"got {len(steps)} step(s)" + ) + + # 7. Every step must have either uses or run key + for i, step in enumerate(steps): + has_uses = "uses" in step + has_run = "run" in step + assert has_uses or has_run, ( + f"job '{job_id}', step {i}: must have 'uses' or 'run' key; " + f"found keys: {list(step.keys())}" + ) + + print(f" Job '{job_id}': runs-on={runs_on}, " + f"timeout={timeout}min, steps={len(steps)}") + + print("\nOK: valid GitHub Actions workflow YAML") + return 0 + + +if __name__ == "__main__": + sys.exit(main())