Compare commits

..

2 Commits

Author SHA1 Message Date
github-actions[bot] d1a04d6c81 Create schema v2 with new tags: internet, owner 2026-02-12 20:34:27 +00:00
github-actions[bot] 954c29422d Add community submission from @ggman12 (closes #12) 2026-02-12 20:34:26 +00:00
8 changed files with 270 additions and 156 deletions
+36 -20
View File
@@ -5,7 +5,7 @@ on:
branches: [main]
paths:
- 'community/**'
- 'schemas/community_submission.v1.schema.json'
- 'schemas/community_submission.*.schema.json'
permissions:
contents: write
@@ -29,9 +29,18 @@ jobs:
- name: Install dependencies
run: pip install jsonschema
- name: Get current schema version
id: schema
run: |
# Find the latest schema version on main
latest=$(ls schemas/community_submission.v*.schema.json 2>/dev/null | sed 's/.*\.v\([0-9]*\)\.schema\.json/\1/' | sort -n | tail -1)
echo "latest_version=${latest:-1}" >> "$GITHUB_OUTPUT"
echo "Latest schema version: ${latest:-1}"
- name: Find and update open community PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LATEST_SCHEMA_VERSION: ${{ steps.schema.outputs.latest_version }}
run: |
# Get list of open community PRs
prs=$(gh pr list --label community --state open --json number,headRefName --jq '.[] | "\(.number) \(.headRefName)"')
@@ -48,28 +57,35 @@ jobs:
git fetch origin "$branch_name"
git checkout "$branch_name"
# Merge main into PR branch
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Check if this PR has a schema file that needs updating
pr_schema=$(ls schemas/community_submission.v*.schema.json 2>/dev/null | sed 's/.*\.v\([0-9]*\)\.schema\.json/\1/' | sort -n | tail -1)
if git merge origin/main -m "Merge main to update schema"; then
# Regenerate schema for this PR's submission (adds any new tags)
python -m src.contributions.regenerate_pr_schema || true
if [ "$pr_schema" -le "$LATEST_SCHEMA_VERSION" ] 2>/dev/null; then
echo " PR schema version ($pr_schema) <= main version ($LATEST_SCHEMA_VERSION)"
# If there are changes, commit and push
if [ -n "$(git status --porcelain schemas/)" ]; then
git add schemas/
git commit -m "Update schema with new tags"
git push origin "$branch_name"
echo " Updated PR #$pr_number with schema changes"
# Merge main into PR branch
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git merge origin/main -m "Merge main to update schema baseline"; then
# Regenerate schema for this PR's submission
python -m src.contributions.regenerate_pr_schema || true
# If there are changes, commit and push
if [ -n "$(git status --porcelain schemas/)" ]; then
new_version=$((LATEST_SCHEMA_VERSION + 1))
git add schemas/
git commit -m "Update schema to v${new_version} (rebased on main)"
git push origin "$branch_name"
echo " Updated PR #$pr_number with new schema"
else
git push origin "$branch_name"
echo " Merged main into PR #$pr_number"
fi
else
git push origin "$branch_name"
echo " Merged main into PR #$pr_number"
fi
else
echo " Merge conflict in PR #$pr_number, adding comment"
gh pr comment "$pr_number" --body $'⚠️ **Merge Conflict**\n\nAnother community submission was merged and this PR has conflicts.\n\nA maintainer may need to:\n1. Close this PR\n2. Remove the `approved` label from the original issue\n3. Re-add the `approved` label to regenerate the PR'
git merge --abort
echo " Merge conflict in PR #$pr_number, adding comment"
gh pr comment "$pr_number" --body $'⚠️ **Merge Conflict**\n\nAnother community submission was merged and this PR has conflicts.\n\nA maintainer may need to:\n1. Close this PR\n2. Remove the `approved` label from the original issue\n3. Re-add the `approved` label to regenerate the PR'
git merge --abort
fi
fi
@@ -1,14 +0,0 @@
[
{
"contributor_name": "hellohello",
"contributor_uuid": "2981c3ee-8712-5f96-84bf-732eda515a3f",
"creation_timestamp": "2026-02-12T21:02:32.325360+00:00",
"registration_number": "N12345",
"tags": {
"airshows": true,
"cat_friendly": false,
"dog_friendly": true,
"notes": "is a pet carrier"
}
}
]
@@ -0,0 +1,21 @@
[
{
"contributor_name": "memetestme",
"contributor_uuid": "2981c3ee-8712-5f96-84bf-732eda515a3f",
"creation_timestamp": "2026-02-12T20:34:25.789124+00:00",
"registration_number": "N12345",
"tags": {
"internet": "starlink"
}
},
{
"contributor_name": "memetestme",
"contributor_uuid": "2981c3ee-8712-5f96-84bf-732eda515a3f",
"creation_timestamp": "2026-02-12T20:34:25.789124+00:00",
"tags": {
"internet": "viasat",
"owner": "John Doe"
},
"transponder_code_hex": "ABC123"
}
]
+16 -49
View File
@@ -3,6 +3,7 @@
"title": "PlaneQuery Aircraft Community Submission (v1)",
"type": "object",
"additionalProperties": false,
"properties": {
"registration_number": {
"type": "string",
@@ -16,6 +17,7 @@
"type": "string",
"minLength": 1
},
"contributor_uuid": {
"type": "string",
"format": "uuid"
@@ -26,12 +28,14 @@
"maxLength": 150,
"description": "Display name (may be blank)"
},
"creation_timestamp": {
"type": "string",
"format": "date-time",
"description": "Set by the system when the submission is persisted/approved.",
"readOnly": true
},
"start_date": {
"type": "string",
"format": "date",
@@ -44,6 +48,7 @@
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
"description": "Optional end date for when this submission's tags are valid (ISO 8601, e.g., 2025-07-03)."
},
"tags": {
"type": "object",
"description": "Additional community-defined tags as key/value pairs (values may be scalar, array, or object).",
@@ -51,76 +56,38 @@
"type": "string",
"pattern": "^[a-z][a-z0-9_]{0,63}$"
},
"additionalProperties": {
"$ref": "#/$defs/tagValue"
},
"properties": {
"airshows": {
"type": "boolean"
},
"cat_friendly": {
"type": "boolean"
},
"dog_friendly": {
"type": "boolean"
},
"notes": {
"type": "string"
}
}
"additionalProperties": { "$ref": "#/$defs/tagValue" }
}
},
"allOf": [
{
"anyOf": [
{
"required": [
"registration_number"
]
},
{
"required": [
"transponder_code_hex"
]
},
{
"required": [
"planequery_airframe_id"
]
}
{ "required": ["registration_number"] },
{ "required": ["transponder_code_hex"] },
{ "required": ["planequery_airframe_id"] }
]
}
],
"$defs": {
"tagScalar": {
"type": [
"string",
"number",
"integer",
"boolean",
"null"
]
"type": ["string", "number", "integer", "boolean", "null"]
},
"tagValue": {
"anyOf": [
{
"$ref": "#/$defs/tagScalar"
},
{ "$ref": "#/$defs/tagScalar" },
{
"type": "array",
"maxItems": 50,
"items": {
"$ref": "#/$defs/tagScalar"
}
"items": { "$ref": "#/$defs/tagScalar" }
},
{
"type": "object",
"maxProperties": 50,
"additionalProperties": {
"$ref": "#/$defs/tagScalar"
}
"additionalProperties": { "$ref": "#/$defs/tagScalar" }
}
]
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "PlaneQuery Aircraft Community Submission (v2)",
"type": "object",
"additionalProperties": false,
"properties": {
"registration_number": {
"type": "string",
"minLength": 1
},
"transponder_code_hex": {
"type": "string",
"pattern": "^[0-9A-F]{6}$"
},
"planequery_airframe_id": {
"type": "string",
"minLength": 1
},
"contributor_uuid": {
"type": "string",
"format": "uuid"
},
"contributor_name": {
"type": "string",
"minLength": 0,
"maxLength": 150,
"description": "Display name (may be blank)"
},
"creation_timestamp": {
"type": "string",
"format": "date-time",
"description": "Set by the system when the submission is persisted/approved.",
"readOnly": true
},
"start_date": {
"type": "string",
"format": "date",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
"description": "Optional start date for when this submission's tags are valid (ISO 8601, e.g., 2025-05-01)."
},
"end_date": {
"type": "string",
"format": "date",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
"description": "Optional end date for when this submission's tags are valid (ISO 8601, e.g., 2025-07-03)."
},
"tags": {
"type": "object",
"description": "Community-defined tags. New tags can be added, but must use consistent types.",
"propertyNames": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{0,63}$"
},
"properties": {
"internet": {
"type": "string"
},
"owner": {
"type": "string"
}
},
"additionalProperties": {
"$ref": "#/$defs/tagValue"
}
}
},
"allOf": [
{
"anyOf": [
{
"required": [
"registration_number"
]
},
{
"required": [
"transponder_code_hex"
]
},
{
"required": [
"planequery_airframe_id"
]
}
]
}
],
"$defs": {
"tagScalar": {
"type": [
"string",
"number",
"integer",
"boolean",
"null"
]
},
"tagValue": {
"anyOf": [
{
"$ref": "#/$defs/tagScalar"
},
{
"type": "array",
"maxItems": 50,
"items": {
"$ref": "#/$defs/tagScalar"
}
},
{
"type": "object",
"maxProperties": 50,
"additionalProperties": {
"$ref": "#/$defs/tagScalar"
}
}
]
}
}
}
+15 -28
View File
@@ -21,13 +21,13 @@ import urllib.request
import urllib.error
from datetime import datetime, timezone
from .schema import extract_json_from_issue_body, extract_contributor_name_from_issue_body, parse_and_validate, load_schema, SCHEMAS_DIR
from .schema import extract_json_from_issue_body, extract_contributor_name_from_issue_body, parse_and_validate, get_latest_schema_version, load_schema
from .contributor import (
generate_contributor_uuid,
generate_submission_filename,
compute_content_hash,
)
from .update_schema import generate_updated_schema, check_for_new_tags, get_existing_tag_definitions
from .update_schema import generate_new_schema, check_for_new_tags, get_existing_tag_definitions
from .read_community_data import build_tag_type_registry
@@ -100,30 +100,14 @@ def create_branch(branch_name: str, sha: str) -> None:
raise
def get_file_sha(path: str, branch: str) -> str | None:
"""Get the SHA of an existing file, or None if it doesn't exist."""
try:
response = github_api_request("GET", f"/contents/{path}?ref={branch}")
return response.get("sha")
except Exception:
return None
def create_or_update_file(path: str, content: str, message: str, branch: str) -> None:
"""Create or update a file in the repository."""
content_b64 = base64.b64encode(content.encode()).decode()
payload = {
github_api_request("PUT", f"/contents/{path}", {
"message": message,
"content": content_b64,
"branch": branch,
}
# If file exists, we need to include its SHA to update it
sha = get_file_sha(path, branch)
if sha:
payload["sha"] = sha
github_api_request("PUT", f"/contents/{path}", payload)
})
def create_pull_request(title: str, head: str, base: str, body: str) -> dict:
@@ -206,15 +190,17 @@ def process_submission(
commit_message = f"Add community submission from @{author_username} (closes #{issue_number})"
create_or_update_file(file_path, content_json, commit_message, branch_name)
# Update schema with any new tags (modifies v1 in place)
# Update schema with any new tags (creates new version if needed)
schema_updated = False
new_version = None
new_tags = []
try:
# Build tag registry from new submissions
tag_registry = build_tag_type_registry(submissions)
# Get current schema and merge existing tags
current_schema = load_schema()
current_version = get_latest_schema_version()
current_schema = load_schema(current_version)
existing_tags = get_existing_tag_definitions(current_schema)
# Merge existing tags into registry
@@ -227,14 +213,15 @@ def process_submission(
new_tags = check_for_new_tags(tag_registry, current_schema)
if new_tags:
# Generate updated schema
updated_schema = generate_updated_schema(current_schema, tag_registry)
schema_json = json.dumps(updated_schema, indent=2) + "\n"
# Generate new schema version
new_version = current_version + 1
new_schema = generate_new_schema(current_schema, tag_registry, new_version)
schema_json = json.dumps(new_schema, indent=2) + "\n"
create_or_update_file(
"schemas/community_submission.v1.schema.json",
f"schemas/community_submission.v{new_version}.schema.json",
schema_json,
f"Update schema with new tags: {', '.join(new_tags)}",
f"Create schema v{new_version} with new tags: {', '.join(new_tags)}",
branch_name
)
schema_updated = True
@@ -244,7 +231,7 @@ def process_submission(
# Create PR
schema_note = ""
if schema_updated:
schema_note = f"\n**Schema Updated:** Added new tags: `{', '.join(new_tags)}`\n"
schema_note = f"\n**Schema Updated:** Created v{new_version} with new tags: `{', '.join(new_tags)}`\n"
pr_body = f"""## Community Submission
+16 -14
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
Regenerate schema for a PR branch after main has been merged in.
This script looks at the submission files in this branch and updates
the schema if new tags were introduced.
This script looks at the submission files in this branch and generates
an updated schema version if new tags were introduced.
Usage: python -m src.contributions.regenerate_pr_schema
"""
@@ -18,14 +18,15 @@ from src.contributions.read_community_data import read_all_submissions, build_ta
from src.contributions.update_schema import (
get_existing_tag_definitions,
check_for_new_tags,
generate_updated_schema,
generate_new_schema,
)
from src.contributions.schema import load_schema, SCHEMAS_DIR
from src.contributions.schema import get_latest_schema_version, load_schema, SCHEMAS_DIR
def main():
"""Main entry point."""
# Load current schema
# Get current schema version and load it
current_version = get_latest_schema_version()
current_schema = load_schema()
# Get existing tag definitions from schema
@@ -45,19 +46,20 @@ def main():
new_tags = check_for_new_tags(tag_registry, current_schema)
if new_tags:
# Generate new schema version
new_version = current_version + 1
print(f"Found new tags: {new_tags}")
print("Updating schema...")
print(f"Generating schema v{new_version}")
# Generate updated schema
updated_schema = generate_updated_schema(current_schema, tag_registry)
# Generate new schema with updated tag definitions
new_schema = generate_new_schema(current_schema, tag_registry, new_version)
# Write updated schema (in place)
schema_path = SCHEMAS_DIR / "community_submission.v1.schema.json"
with open(schema_path, 'w') as f:
json.dump(updated_schema, f, indent=2)
f.write("\n")
# Write new schema version
new_schema_path = SCHEMAS_DIR / f"community_submission.v{new_version}.schema.json"
with open(new_schema_path, 'w') as f:
json.dump(new_schema, f, indent=2)
print(f"Updated {schema_path}")
print(f"Created {new_schema_path}")
else:
print("No new tags found, schema is up to date")
+46 -31
View File
@@ -40,27 +40,40 @@ def type_name_to_json_schema(type_name: str) -> dict:
return type_map.get(type_name, {"$ref": "#/$defs/tagValue"})
def generate_updated_schema(base_schema: dict, tag_registry: dict[str, str]) -> dict:
def generate_new_schema(base_schema: dict, tag_registry: dict[str, str], new_version: int) -> dict:
"""
Generate an updated schema with explicit tag definitions.
Generate a new schema version with explicit tag definitions.
Args:
base_schema: The current schema to update
base_schema: The current schema to base the new one on
tag_registry: Dict mapping tag name to type name
new_version: The new version number
Returns:
Updated schema dict
Complete new schema dict
"""
schema = json.loads(json.dumps(base_schema)) # Deep copy
# Update title with new version
schema["title"] = f"PlaneQuery Aircraft Community Submission (v{new_version})"
# Build tag properties with explicit types
tag_properties = {}
for tag_name, type_name in sorted(tag_registry.items()):
tag_properties[tag_name] = type_name_to_json_schema(type_name)
# Only add/update the properties key within tags, preserve everything else
if "properties" in schema and "tags" in schema["properties"]:
schema["properties"]["tags"]["properties"] = tag_properties
# Update tags definition
schema["properties"]["tags"] = {
"type": "object",
"description": "Community-defined tags. New tags can be added, but must use consistent types.",
"propertyNames": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{0,63}$"
},
"properties": tag_properties,
# Still allow additional properties for new tags
"additionalProperties": {"$ref": "#/$defs/tagValue"}
}
return schema
@@ -76,55 +89,57 @@ def check_for_new_tags(tag_registry: dict[str, str], current_schema: dict) -> li
return [tag for tag in tag_registry if tag not in existing_tags]
def update_schema_file(
def create_new_schema_version(
tag_registry: dict[str, str],
check_only: bool = False
) -> tuple[bool, list[str]]:
) -> tuple[int | None, list[str]]:
"""
Update the v1 schema file with new tag definitions.
Create a new schema version if there are new tags.
Args:
tag_registry: Dict mapping tag name to type name
check_only: If True, only check if update is needed without writing
Returns:
Tuple of (was_updated, list_of_new_tags)
Tuple of (new_version or None if no update, list_of_new_tags)
"""
current_schema = load_schema()
current_version = get_latest_schema_version()
current_schema = load_schema(current_version)
# Find new tags
new_tags = check_for_new_tags(tag_registry, current_schema)
if not new_tags:
return False, []
return None, []
if check_only:
return True, new_tags
return current_version + 1, new_tags
# Generate and write updated schema (in place)
updated_schema = generate_updated_schema(current_schema, tag_registry)
schema_path = get_schema_path()
# Generate and write new schema
new_version = current_version + 1
new_schema = generate_new_schema(current_schema, tag_registry, new_version)
new_schema_path = get_schema_path(new_version)
with open(schema_path, "w") as f:
json.dump(updated_schema, f, indent=2)
with open(new_schema_path, "w") as f:
json.dump(new_schema, f, indent=2)
f.write("\n")
return True, new_tags
return new_version, new_tags
def update_schema_from_submissions(check_only: bool = False) -> tuple[bool, list[str]]:
def update_schema_from_submissions(check_only: bool = False) -> tuple[int | None, list[str]]:
"""
Read all submissions and update the schema if needed.
Read all submissions and create a new schema version if needed.
Args:
check_only: If True, only check if update is needed without writing
Returns:
Tuple of (was_updated, list_of_new_tags)
Tuple of (new_version or None if no update, list_of_new_tags)
"""
submissions = read_all_submissions()
tag_registry = build_tag_type_registry(submissions)
return update_schema_file(tag_registry, check_only)
return create_new_schema_version(tag_registry, check_only)
def main():
@@ -133,21 +148,21 @@ def main():
args = parser.parse_args()
was_updated, new_tags = update_schema_from_submissions(check_only=args.check)
new_version, new_tags = update_schema_from_submissions(check_only=args.check)
if args.check:
if was_updated:
print(f"Schema update needed. New tags: {', '.join(new_tags)}")
if new_version:
print(f"Schema update needed -> v{new_version}. New tags: {', '.join(new_tags)}")
sys.exit(1)
else:
print("Schema is up to date")
print(f"Schema is up to date (v{get_latest_schema_version()})")
sys.exit(0)
else:
if was_updated:
print(f"Updated {get_schema_path()}")
if new_version:
print(f"Created {get_schema_path(new_version)}")
print(f"Added tags: {', '.join(new_tags)}")
else:
print("No update needed")
print(f"No update needed (v{get_latest_schema_version()})")
if __name__ == "__main__":