feat: Added OARS content rating and initial release block to metainfo.x…

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
This commit is contained in:
John Smith
2026-05-27 15:57:08 -04:00
parent 36afcea16a
commit 67a692c1fe
4 changed files with 405 additions and 3 deletions
+26 -1
View File
@@ -18,10 +18,35 @@
<launchable type="desktop-id">org.droposs.client.desktop</launchable>
<url type="homepage">https://github.com/Drop-OSS/drop-app</url>
<url type="bugtracker">https://github.com/Drop-OSS/drop-app/issues</url>
<developer_name>Drop OSS</developer_name>
<developer id="org.droposs">
<name>Drop OSS</name>
</developer>
<categories>
<category>Game</category>
</categories>
<content_rating type="oars-1.1">
<content_attribute id="violence-cartoon">none</content_attribute>
<content_attribute id="violence-fantasy">none</content_attribute>
<content_attribute id="violence-realistic">none</content_attribute>
<content_attribute id="violence-bloodshed">none</content_attribute>
<content_attribute id="language-profanity">none</content_attribute>
<content_attribute id="language-humor">none</content_attribute>
<content_attribute id="language-discrimination">none</content_attribute>
<content_attribute id="social-chat">none</content_attribute>
<content_attribute id="social-info">none</content_attribute>
<content_attribute id="social-audio">none</content_attribute>
<content_attribute id="social-location">none</content_attribute>
<content_attribute id="social-contacts">none</content_attribute>
<content_attribute id="money-purchasing">none</content_attribute>
<content_attribute id="money-gambling">none</content_attribute>
</content_rating>
<releases>
<release version="0.1.0" date="2026-01-01">
<description>
<p>Initial Flatpak packaging release.</p>
</description>
</release>
</releases>
<recommends>
<display_length compare="ge">1280</display_length>
</recommends>
+113 -2
View File
@@ -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 "============================================"
+174
View File
@@ -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()
+92
View File
@@ -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())