Skip to content

Commit 7e14ca0

Browse files
authored
Added github action to update feature catalog MD file (#1714)
1 parent 92e4b59 commit 7e14ca0

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Update feature catalog page
2+
on:
3+
schedule:
4+
- cron: 0 10 * * TUE
5+
workflow_dispatch:
6+
jobs:
7+
generate-feature-catalog-file:
8+
name: Generate feature catalog page
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout docs repository
12+
uses: actions/checkout@v4
13+
14+
- name: Latest run-id from community repository
15+
run: |
16+
latest_workflow_id=$(curl -s https://api.github.com/repos/localstack/localstack/actions/workflows \
17+
| jq '.workflows[] | select(.name=="AWS / Archive feature files").id')
18+
latest_run_id=$(curl -s \
19+
https://api.github.com/repos/localstack/localstack/actions/workflows/$latest_workflow_id/runs | jq '.workflow_runs[0].id')
20+
echo "Latest run-id: ${latest_run_id}"
21+
echo "FEATURES_ARTIFACTS_COMMUNITY_RUN_ID=${latest_run_id}" >> $GITHUB_ENV
22+
23+
- name: Download features files from Collect feature files (GitHub)
24+
uses: actions/download-artifact@v4
25+
with:
26+
path: features-files-community
27+
name: features-files
28+
github-token: ${{ secrets.GH_PAT_FEATURE_CATALOG_PAGE }} # PAT with access to artifacts from GH Actions
29+
repository: localstack/localstack
30+
run-id: ${{ env.FEATURES_ARTIFACTS_COMMUNITY_RUN_ID }}
31+
32+
- name: Latest run-id from ext repository
33+
run: |
34+
latest_workflow_id=$(curl -s https://api.github.com/repos/localstack/localstack-ext/actions/workflows \
35+
| jq '.workflows[] | select(.name=="AWS / Archive feature files").id')
36+
latest_run_id=$(curl -s \
37+
https://api.github.com/repos/localstack/localstack-ext/actions/workflows/$latest_workflow_id/runs | jq '.workflow_runs[0].id')
38+
echo "Latest run-id: ${latest_run_id}"
39+
echo "FEATURES_ARTIFACTS_EXT_RUN_ID=${latest_run_id}" >> $GITHUB_ENV
40+
41+
- name: Download features files from Collect feature files from PRO (GitHub)
42+
uses: actions/download-artifact@v4
43+
with:
44+
path: features-files-ext
45+
name: features-files-ext
46+
repository: localstack/localstack
47+
github-token: ${{ secrets.GH_PAT_FEATURE_CATALOG_PAGE_PRO }} # PAT with access to artifacts from GH Actions
48+
run-id: ${{ env.FEATURES_ARTIFACTS_EXT_RUN_ID }}
49+
50+
- name: Generate feature catalog page
51+
run: python3 scripts/generate_feature_catalog_page.py
52+
env:
53+
PATH_FEATURE_FILES_COMMUNITY: 'features-files-community'
54+
PATH_FEATURE_FILES_EXT: 'features-files-ext'
55+
PATH_FEATURE_CATALOG_MD: 'content/en/user-guide/aws/feature-coverage.md'
56+
57+
- name: Create PR
58+
uses: peter-evans/create-pull-request@v7
59+
with:
60+
title: "Update Feature catalog page"
61+
body: "This PR updates Feature catalog page based on feature catalog YAML files"
62+
branch: "update-feature-catalog"
63+
add-paths: "content/en/user-guide/aws/feature-coverage.md"
64+
author: "LocalStack Bot <[email protected]>"
65+
committer: "LocalStack Bot <[email protected]>"
66+
commit-message: "Upgrade feature catalog"
67+
labels: "documentation"
+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import os
2+
import sys
3+
from pathlib import Path
4+
5+
import yaml
6+
7+
DEFAULT_STATUS = 'unsupported'
8+
DEFAULT_EMULATION_LEVEL = 'CRUD'
9+
FEATURES_FILE_NAME='features.yml'
10+
11+
MD_FILE_HEADER = """---
12+
title: "AWS Service Feature Coverage"
13+
linkTitle: "⭐ Feature Coverage"
14+
weight: 1
15+
description: >
16+
Overview of the implemented AWS APIs and their level of parity with the AWS cloud
17+
aliases:
18+
- /localstack/coverage/
19+
- /aws/feature-coverage/
20+
hide_readingtime: true
21+
---
22+
23+
24+
## Emulation Levels
25+
26+
* CRUD: The service accepts requests and returns proper (potentially static) responses.
27+
No additional business logic besides storing entities.
28+
* Emulated: The service imitates the functionality, including synchronous and asynchronous business logic operating on service entities.
29+
30+
| Service / Feature | Implementation status | Emulation Level | Limitations |
31+
|-------------------|----------------|-----------------|--------------------------|"""
32+
33+
class FeatureCatalogMarkdownGenerator:
34+
md_content = [MD_FILE_HEADER]
35+
36+
def __init__(self, file_path: str):
37+
self.file_path = file_path
38+
pass
39+
40+
def add_service_section(self, feature_file_content: str):
41+
service_name = feature_file_content.get('name')
42+
emulation_level = feature_file_content.get('emulation_level', DEFAULT_EMULATION_LEVEL)
43+
self.md_content.append(f"| **{service_name}** | [Details 🔍] | {emulation_level} | |")
44+
45+
def add_features_rows(self, feature_file_content: str):
46+
for feature in feature_file_content.get('features', []):
47+
feature_name = feature.get('name', '')
48+
documentation_page = feature.get('documentation_page')
49+
if documentation_page:
50+
feature_name = f'[{feature_name}]({documentation_page})'
51+
status = feature.get('status', DEFAULT_STATUS)
52+
53+
limitations = feature.get('limitations', [])
54+
limitations_md = '\n '.join(limitations) if limitations else ''
55+
56+
self.md_content.append(f"| {feature_name} | {status} | | {limitations_md} |")
57+
58+
def generate_file(self):
59+
try:
60+
with open(self.file_path, "w") as feature_coverage_md_file:
61+
feature_coverage_md_file.writelines(s + '\n' for s in self.md_content)
62+
except Exception as e:
63+
print(f"Error writing to file: {e}")
64+
sys.exit(1)
65+
66+
def load_yaml_file(file_path: str):
67+
try:
68+
with open(file_path, 'r') as file:
69+
return yaml.safe_load(file)
70+
except yaml.YAMLError as e:
71+
sys.stdout.write(f"::error title=Failed to parse features file::An error occurred while parsing {file_path}: {e}")
72+
sys.exit(1)
73+
except FileNotFoundError:
74+
sys.stdout.write(f"::error title=Missing features file::No features file found at {file_path}")
75+
sys.exit(1)
76+
77+
def get_service_path_to_abs_community_ext_paths(community_files_path: str, ext_files_path: str) -> dict[str, (str, str)]:
78+
relative_to_abs_paths = {}
79+
for community_abs_path in Path(community_files_path).rglob(FEATURES_FILE_NAME):
80+
rel_path = str(community_abs_path.relative_to(community_files_path))
81+
relative_to_abs_paths[rel_path] = (community_abs_path, None)
82+
83+
for abs_path_ext in Path(ext_files_path).rglob(FEATURES_FILE_NAME):
84+
rel_path = str(abs_path_ext.relative_to(ext_files_path))
85+
if rel_path in relative_to_abs_paths:
86+
community_abs_path, _ = relative_to_abs_paths[rel_path]
87+
relative_to_abs_paths[rel_path] = (community_abs_path, abs_path_ext)
88+
else:
89+
relative_to_abs_paths[rel_path] = (None, abs_path_ext)
90+
return relative_to_abs_paths
91+
92+
def main():
93+
community_feature_files_path = os.getenv('PATH_FEATURE_FILES_COMMUNITY')
94+
ext_feature_files_path = os.getenv('PATH_FEATURE_FILES_EXT')
95+
feature_catalog_md_file_path = os.getenv('PATH_FEATURE_CATALOG_MD')
96+
97+
service_path_to_abs_paths = get_service_path_to_abs_community_ext_paths(community_feature_files_path, ext_feature_files_path)
98+
md_generator = FeatureCatalogMarkdownGenerator(feature_catalog_md_file_path)
99+
100+
for service_name in sorted(service_path_to_abs_paths):
101+
abs_path_community, abs_path_ext = service_path_to_abs_paths.get(service_name)
102+
service_definition_created = False
103+
if abs_path_community:
104+
feature_file_community = load_yaml_file(abs_path_community)
105+
md_generator.add_service_section(feature_file_community)
106+
service_definition_created = True
107+
md_generator.add_features_rows(feature_file_community)
108+
if abs_path_ext:
109+
feature_file_ext = load_yaml_file(abs_path_ext)
110+
if not service_definition_created:
111+
md_generator.add_service_section(feature_file_community)
112+
md_generator.add_features_rows(feature_file_ext)
113+
md_generator.generate_file()
114+
115+
if __name__ == "__main__":
116+
main()

0 commit comments

Comments
 (0)