mirror of
https://github.com/Heretek-AI/drop-flatpak.git
synced 2026-07-01 10:04:29 -04:00
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:
@@ -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
@@ -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 "============================================"
|
||||
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user