Skip to content

Commit 34d8db0

Browse files
authored
Validate files containing descriptions of AWS services (#15)
1 parent 627752a commit 34d8db0

File tree

5 files changed

+256
-0
lines changed

5 files changed

+256
-0
lines changed

.github/features_schema.json

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"required": ["service", "name", "features"],
5+
"properties": {
6+
"service": {
7+
"type": "string",
8+
"description": "Service's abbreviation in AWS"
9+
},
10+
"name": {
11+
"type": "string",
12+
"description": "The display name of the service from AWS"
13+
},
14+
"emulation_level": {
15+
"type": "string",
16+
"enum": ["CRUD", "emulated"],
17+
"description": "The level of emulation support on the service level"
18+
},
19+
"localstack_page": {
20+
"type": "string",
21+
"format": "uri",
22+
"description": "URL to the LocalStack user guide documentation"
23+
},
24+
"features": {
25+
"type": "array",
26+
"description": "List of features supported by the service",
27+
"items": {
28+
"type": "object",
29+
"required": ["name", "description", "documentation_page", "status"],
30+
"properties": {
31+
"name": {
32+
"type": "string",
33+
"description": "Name of the feature"
34+
},
35+
"description": {
36+
"type": "string",
37+
"description": "Description of the feature"
38+
},
39+
"documentation_page": {
40+
"type": "string",
41+
"format": "uri",
42+
"description": "URL to the AWS feature's documentation"
43+
},
44+
"status": {
45+
"type": "string",
46+
"enum": ["supported", "unsupported"],
47+
"description": "Current status of the feature"
48+
},
49+
"api_methods": {
50+
"type": "array",
51+
"items": {
52+
"type": "string"
53+
},
54+
"description": "List of API methods associated with the feature"
55+
},
56+
"limitations": {
57+
"type": "array",
58+
"items": {
59+
"type": "string"
60+
},
61+
"description": "List of known limitations for the feature"
62+
},
63+
"emulation_level": {
64+
"type": "string",
65+
"enum": ["CRUD", "emulated"],
66+
"description": "The level of emulation support"
67+
}
68+
},
69+
"additionalProperties": false
70+
},
71+
"minItems": 0
72+
}
73+
},
74+
"additionalProperties": false
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
import sys
3+
4+
FEATURES_FILE_NAME='features.yml'
5+
6+
def find_paths_to_service_providers(directory: str) -> list[str]:
7+
provider_path = os.path.join(directory, 'provider.py')
8+
if os.path.isfile(provider_path):
9+
return [provider_path]
10+
paths = []
11+
for root, _, files in os.walk(directory):
12+
if 'provider.py' in files:
13+
paths.append(os.path.join(root, 'provider.py'))
14+
return paths
15+
16+
17+
def map_features_files_status(services_path, changed_files) -> dict[str, bool]:
18+
features_files_exist_status = {}
19+
for file_path in changed_files:
20+
if file_path.startswith(services_path):
21+
service_folder_name = file_path.removeprefix(services_path).split('/')[1]
22+
service_path = os.path.join(services_path, service_folder_name)
23+
24+
provider_files = find_paths_to_service_providers(service_path)
25+
for provider_file in provider_files:
26+
features_file = os.path.join(os.path.dirname(provider_file), FEATURES_FILE_NAME)
27+
features_files_exist_status[features_file] = os.path.exists(features_file)
28+
return features_files_exist_status
29+
30+
def main():
31+
# Detect changed features files
32+
comma_separated_changed_files = os.getenv('ALL_CHANGED_FILES')
33+
changed_files = comma_separated_changed_files.split(',')
34+
35+
#Check features file exists in services folder
36+
services_path = os.getenv('SERVICES_PATH')
37+
features_file_status = map_features_files_status(services_path, changed_files)
38+
for file_path, exists in features_file_status.items():
39+
if not exists:
40+
sys.stdout.write(f"::error title=Features file is missing::Service code was changed but no corresponding feature file found at {file_path}")
41+
42+
43+
if __name__ == "__main__":
44+
main()
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import json
2+
import os
3+
4+
import yaml
5+
from jsonschema import validate, ValidationError
6+
import sys
7+
8+
FEATURES_FILE_NAME='features.yml'
9+
10+
def load_yaml_file(file_path: str):
11+
try:
12+
with open(file_path, 'r') as file:
13+
return yaml.safe_load(file)
14+
except yaml.YAMLError as e:
15+
print(f"Error parsing YAML file: {e}")
16+
sys.exit(1)
17+
except FileNotFoundError:
18+
print(f"YAML file not found: {file_path}")
19+
sys.exit(1)
20+
21+
def load_json_file(file_path: str):
22+
try:
23+
with open(file_path, 'r') as file:
24+
return json.load(file)
25+
except json.JSONDecodeError as e:
26+
print(f"Failed to parse JSON schema file: {e}")
27+
sys.exit(1)
28+
except FileNotFoundError:
29+
print(f"Json schema file not found: {file_path}")
30+
sys.exit(1)
31+
32+
def validate_yaml_against_schema(yaml_data: str, json_schema: str, file_path: str) -> bool:
33+
try:
34+
validate(instance=yaml_data, schema=json_schema)
35+
print(f"Successful validation of file: {file_path}")
36+
return True
37+
except ValidationError as e:
38+
sys.stdout.write(f"::error file={file_path},title=Validation has failed at path {' -> '.join(str(x) for x in e.path)}::{e.message}")
39+
return False
40+
41+
def main():
42+
# Detect changed features files
43+
comma_separated_changed_files = os.getenv('ALL_CHANGED_FILES')
44+
if not comma_separated_changed_files:
45+
print("Environment variable ALL_CHANGED_FILES is not set. Skipping validation.")
46+
sys.exit(1)
47+
48+
changed_files = comma_separated_changed_files.split(',')
49+
changed_features_files = [path for path in changed_files if path.lower().endswith(FEATURES_FILE_NAME)]
50+
print(f'Changed features files: {",".join(changed_features_files)}')
51+
52+
if len(changed_features_files) == 0:
53+
print('No feature file was changed. Skipping validation.')
54+
sys.exit(0)
55+
56+
features_schema_path = os.getenv('FEATURES_JSON_SCHEMA', 'features_schema.json')
57+
features_schema = load_json_file(features_schema_path)
58+
print(f'Features schema file was loaded: {features_schema_path}')
59+
60+
# validate schemas
61+
all_valid = True
62+
for file_path in changed_features_files:
63+
features_file = load_yaml_file(file_path)
64+
if not validate_yaml_against_schema(features_file, features_schema, file_path):
65+
all_valid = False
66+
sys.exit(0 if all_valid else 1)
67+
68+
if __name__ == "__main__":
69+
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Validate feature catalog files
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
aws_services_path:
7+
description: 'Directory containing AWS service implementations in the localstack repository'
8+
type: string
9+
required: true
10+
localstack_meta_ref:
11+
description: 'Branch to checkout in the localstack/meta repository without "origin/" prefix'
12+
type: string
13+
required: false
14+
15+
jobs:
16+
validate-features-files:
17+
name: Validate feature catalog files
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
# Clone repository that's calling this reusable workflow
22+
- name: Checkout current repository
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 2
26+
27+
# Clone the localstack/meta repo to access required Python scripts and JSON schema files
28+
- name: Checkout localstack/meta repository
29+
uses: actions/checkout@v4
30+
with:
31+
repository: 'localstack/meta'
32+
sparse-checkout: |
33+
.github/scripts
34+
.github/features_schema.json
35+
ref: ${{ inputs.localstack_meta_ref || 'main' }}
36+
path: 'localstack-meta'
37+
38+
- name: Fetch list of modified files in the PR
39+
id: changed-files
40+
run: |
41+
ALL_CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r \
42+
--diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | paste -sd "," -)
43+
echo "all_changed_files=$ALL_CHANGED_FILES" >> $GITHUB_OUTPUT
44+
45+
- name: Set up Python
46+
id: setup-python
47+
uses: actions/setup-python@v5
48+
with:
49+
python-version: '3.11'
50+
51+
- name: Install dependencies
52+
run: |
53+
python3 -m pip install pyyaml jsonschema
54+
55+
- name: Check features files exist
56+
env:
57+
ALL_CHANGED_FILES: "${{ steps.changed-files.outputs.all_changed_files }}"
58+
SERVICES_PATH: "${{ inputs.aws_services_path }}"
59+
run: python3 localstack-meta/.github/scripts/check_features_files_exist.py
60+
61+
- name: Validate features files
62+
env:
63+
ALL_CHANGED_FILES: "${{ steps.changed-files.outputs.all_changed_files }}"
64+
FEATURES_JSON_SCHEMA: 'localstack-meta/.github/features_schema.json'
65+
run: python3 localstack-meta/.github/scripts/validate_features_files.py

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.DS_Store
2+
3+
.idea/

0 commit comments

Comments
 (0)