Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/actions/test-api/admin/environment/point/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: API - admin - environment point tests
description: Run environment point tests as admin user

inputs:
api-host:
description: API test host URL
required: true
api-key:
description: Admin API key
required: true

runs:
using: composite
steps:
- name: (admin) Point environment prod to snapshot 1
shell: bash
run: |
RESULT=$(curl --fail-with-body -L -s -X PATCH -H "Authorization: Bearer ${{ inputs.api-key }}" -H "Content-Type: application/json" -d '{"snapshot":1}' ${{ inputs.api-host }}/api/v2/environment/prod/point)
if [ $? -ne 0 ]; then
echo "Failed to point environment"
exit 1
fi
echo "$RESULT" | jq
23 changes: 23 additions & 0 deletions .github/actions/test-api/admin/snapshot/diff/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: API - admin - snapshot diff tests
description: Run snapshot diff tests as admin user

inputs:
api-host:
description: API test host URL
required: true
api-key:
description: Admin API key
required: true

runs:
using: composite
steps:
- name: (admin) Snapshot diff
shell: bash
run: |
RESULT=$(curl --fail-with-body -L -s -X GET -H "Authorization: Bearer ${{ inputs.api-key }}" ${{ inputs.api-host }}/api/v2/snapshot/1/diff/3)
if [ $? -ne 0 ]; then
echo "Failed to get snapshot diff"
exit 1
fi
echo "$RESULT" | jq
63 changes: 63 additions & 0 deletions .github/actions/test-api/admin/tasks/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: API - admin - tasks listing tests
description: Run tasks listing tests as admin user

inputs:
api-host:
description: API test host URL
required: true
api-key:
description: Admin API key
required: true

runs:
using: composite
steps:
- name: (admin) List all tasks
shell: bash
run: |
RESULT=$(curl --fail-with-body -L -s -X GET -H "Authorization: Bearer ${{ inputs.api-key }}" ${{ inputs.api-host }}/api/v2/tasks/)
if [ $? -ne 0 ]; then
echo "Failed to list all tasks"
exit 1
fi
echo "$RESULT" | jq

- name: (admin) List running tasks
shell: bash
run: |
RESULT=$(curl --fail-with-body -L -s -X GET -H "Authorization: Bearer ${{ inputs.api-key }}" ${{ inputs.api-host }}/api/v2/tasks/running)
if [ $? -ne 0 ]; then
echo "Failed to list running tasks"
exit 1
fi
echo "$RESULT" | jq

- name: (admin) List queued tasks
shell: bash
run: |
RESULT=$(curl --fail-with-body -L -s -X GET -H "Authorization: Bearer ${{ inputs.api-key }}" ${{ inputs.api-host }}/api/v2/tasks/queued)
if [ $? -ne 0 ]; then
echo "Failed to list queued tasks"
exit 1
fi
echo "$RESULT" | jq

- name: (admin) List scheduled tasks
shell: bash
run: |
RESULT=$(curl --fail-with-body -L -s -X GET -H "Authorization: Bearer ${{ inputs.api-key }}" ${{ inputs.api-host }}/api/v2/tasks/scheduled)
if [ $? -ne 0 ]; then
echo "Failed to list scheduled tasks"
exit 1
fi
echo "$RESULT" | jq

- name: (admin) List done tasks
shell: bash
run: |
RESULT=$(curl --fail-with-body -L -s -X GET -H "Authorization: Bearer ${{ inputs.api-key }}" ${{ inputs.api-host }}/api/v2/tasks/done)
if [ $? -ne 0 ]; then
echo "Failed to list done tasks"
exit 1
fi
echo "$RESULT" | jq
174 changes: 174 additions & 0 deletions .github/scripts/validate-source-repository-templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
from pathlib import Path
import sys

import yaml


ROOT = Path(__file__).resolve().parents[2]
TEMPLATES_DIR = ROOT / "www" / "templates" / "source-repositories"
VALID_TYPES = {"deb", "rpm"}


def error(errors, path, message):
errors.append(f"{path.relative_to(ROOT)}: {message}")


def is_non_empty_string(value):
return isinstance(value, str) and value.strip() != ""


def require_non_empty_string(errors, path, value, field):
if not is_non_empty_string(value):
error(errors, path, f"missing or empty `{field}`")


def require_list(errors, path, value, field):
if not isinstance(value, list) or len(value) == 0:
error(errors, path, f"`{field}` must be a non-empty list")
return False

return True


def validate_gpgkeys(errors, path, gpgkeys, context):
if not require_list(errors, path, gpgkeys, f"{context}.gpgkeys"):
return

for index, gpgkey in enumerate(gpgkeys):
key_context = f"{context}.gpgkeys[{index}]"

if not isinstance(gpgkey, dict):
error(errors, path, f"`{key_context}` must be a mapping")
continue

fingerprint = gpgkey.get("fingerprint")
link = gpgkey.get("link")

if not is_non_empty_string(fingerprint) and not is_non_empty_string(link):
error(errors, path, f"`{key_context}` must define `fingerprint` or `link`")


def validate_deb_distribution(errors, path, distribution, context):
if not isinstance(distribution, dict):
error(errors, path, f"`{context}` must be a mapping")
return

require_non_empty_string(errors, path, distribution.get("name"), f"{context}.name")
require_non_empty_string(errors, path, distribution.get("description"), f"{context}.description")

components = distribution.get("components")
if require_list(errors, path, components, f"{context}.components"):
for index, component in enumerate(components):
component_context = f"{context}.components[{index}]"

if not isinstance(component, dict):
error(errors, path, f"`{component_context}` must be a mapping")
continue

require_non_empty_string(errors, path, component.get("name"), f"{component_context}.name")

validate_gpgkeys(errors, path, distribution.get("gpgkeys"), context)


def validate_rpm_releasever(errors, path, releasever, context):
if not isinstance(releasever, dict):
error(errors, path, f"`{context}` must be a mapping")
return

if releasever.get("name") in (None, ""):
error(errors, path, f"missing or empty `{context}.name`")

require_non_empty_string(errors, path, releasever.get("description"), f"{context}.description")
validate_gpgkeys(errors, path, releasever.get("gpgkeys"), context)


def validate_repository(errors, path, repository, index, expected_type):
context = f"repositories[{index}]"

if not isinstance(repository, dict):
error(errors, path, f"`{context}` must be a mapping")
return

require_non_empty_string(errors, path, repository.get("name"), f"{context}.name")
require_non_empty_string(errors, path, repository.get("description"), f"{context}.description")
require_non_empty_string(errors, path, repository.get("url"), f"{context}.url")

repo_type = repository.get("type")
if repo_type != expected_type:
error(errors, path, f"`{context}.type` must be `{expected_type}`")

if expected_type == "deb":
distributions = repository.get("distributions")
if require_list(errors, path, distributions, f"{context}.distributions"):
for distribution_index, distribution in enumerate(distributions):
validate_deb_distribution(errors, path, distribution, f"{context}.distributions[{distribution_index}]")

if expected_type == "rpm":
releasevers = repository.get("releasever")
if require_list(errors, path, releasevers, f"{context}.releasever"):
for releasever_index, releasever in enumerate(releasevers):
validate_rpm_releasever(errors, path, releasever, f"{context}.releasever[{releasever_index}]")


def validate_file(path):
errors = []

try:
with path.open("r", encoding="utf-8") as stream:
data = yaml.safe_load(stream)
except yaml.YAMLError as exception:
error(errors, path, f"invalid YAML: {exception}")
return errors

if not isinstance(data, dict):
error(errors, path, "root document must be a mapping")
return errors

require_non_empty_string(errors, path, data.get("description"), "description")

template_type = data.get("type")
if template_type not in VALID_TYPES:
error(errors, path, "`type` must be `deb` or `rpm`")

repositories = data.get("repositories")
if require_list(errors, path, repositories, "repositories") and template_type in VALID_TYPES:
names = set()

for index, repository in enumerate(repositories):
if isinstance(repository, dict) and repository.get("name") in names:
error(errors, path, f"duplicate repository name `{repository.get('name')}`")
elif isinstance(repository, dict) and is_non_empty_string(repository.get("name")):
names.add(repository.get("name"))

validate_repository(errors, path, repository, index, template_type)

return errors


def main():
paths = sorted(TEMPLATES_DIR.glob("*/*.yml"))

if not paths:
print("No source repository templates found.", file=sys.stderr)
return 1

errors = []

for path in paths:
errors.extend(validate_file(path))

if errors:
print("Source repository template validation failed:", file=sys.stderr)

for validation_error in errors:
print(f"- {validation_error}", file=sys.stderr)

return 1

print(f"Validated {len(paths)} source repository templates.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
2 changes: 1 addition & 1 deletion .github/workflows/phpcs.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: PHP_CodeSniffer
name: PHP CodeSniffer

on:
push:
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: PHPStan

on:
push:
branches: [ main, devel ]
pull_request:
branches: [ main ]

jobs:
phpstan:
name: Run PHPStan
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'

- name: Run PHPStan
run: |
php $GITHUB_WORKSPACE/www/libs/vendor/bin/phpstan analyse \
-c $GITHUB_WORKSPACE/phpstan.neon \
$GITHUB_WORKSPACE/www
27 changes: 27 additions & 0 deletions .github/workflows/source-repository-templates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Validate source repository templates

on:
push:
branches: [ devel ]
pull_request:
branches: [ main, devel ]

jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.x'

- name: Install dependencies
run: python -m pip install PyYAML

- name: Validate source repository templates
run: python .github/scripts/validate-source-repository-templates.py
21 changes: 20 additions & 1 deletion .github/workflows/test-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ jobs:
#
# Run tests as admin user
#
- name: Run admin tasks tests
uses: ./.github/actions/test-api/admin/tasks
with:
api-host: ${{ secrets.API_TEST_HOST }}
api-key: ${{ secrets.API_TEST_ADMIN_API_KEY }}

- name: Run admin environment point test
uses: ./.github/actions/test-api/admin/environment/point
with:
api-host: ${{ secrets.API_TEST_HOST }}
api-key: ${{ secrets.API_TEST_ADMIN_API_KEY }}

- name: Run admin repositories tests
uses: ./.github/actions/test-api/admin/repositories
with:
Expand All @@ -72,6 +84,12 @@ jobs:
api-host: ${{ secrets.API_TEST_HOST }}
api-key: ${{ secrets.API_TEST_ADMIN_API_KEY }}

- name: Run admin snapshot diff tests
uses: ./.github/actions/test-api/admin/snapshot/diff
with:
api-host: ${{ secrets.API_TEST_HOST }}
api-key: ${{ secrets.API_TEST_ADMIN_API_KEY }}

- name: Run admin package list tests
uses: ./.github/actions/test-api/admin/snapshot/package/list
with:
Expand All @@ -90,7 +108,7 @@ jobs:
api-host: ${{ secrets.API_TEST_HOST }}
api-key: ${{ secrets.API_TEST_ADMIN_API_KEY }}

- name: Run admin host tests
- name: Run admin single host tests
uses: ./.github/actions/test-api/admin/host
with:
api-host: ${{ secrets.API_TEST_HOST }}
Expand Down Expand Up @@ -124,6 +142,7 @@ jobs:
api-host: ${{ secrets.API_TEST_HOST }}
api-key: ${{ secrets.API_TEST_TEST_USER1_API_KEY }}


#
# Run tests as a regular user: test-user2
# This user has permissions to upload packages and rebuild snapshots metadata
Expand Down
Loading
Loading