From 84d9077a06bb5b67386c82482e48a84302b5795d Mon Sep 17 00:00:00 2001 From: Usman Ahmed Saeed Date: Wed, 29 Jul 2026 16:03:50 +0200 Subject: [PATCH 1/7] move api-deploy, remove redundant inline schemas, align release with code artificat --- .github/workflows/docker.yml | 51 ---------- README.md | 135 ++++++++++++++++++++++++- api_deploy/__init__.py | 2 +- api_deploy/config.py | 5 + api_deploy/converters.py | 103 ++++++++++++++++++- scripts/release.sh | 156 +++++++++++++++++++++++++++++ setup.py | 10 +- tests/unit/test_flatten_dedup.py | 139 +++++++++++++++++++++++++ tests/unit/test_remove_examples.py | 96 ++++++++++++++++++ 9 files changed, 637 insertions(+), 60 deletions(-) delete mode 100644 .github/workflows/docker.yml create mode 100755 scripts/release.sh create mode 100644 tests/unit/test_flatten_dedup.py create mode 100644 tests/unit/test_remove_examples.py diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 85d9d15..0000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Docker Hub - -concurrency: - cancel-in-progress: true - group: ${{ github.workflow }}-${{ github.ref }} - -on: - push: - branches: - - 'develop' - - 'main' - tags: - - '*.*.*' - -jobs: - build: - runs-on: ubuntu-latest - - - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Login to Docker Hub - uses: docker/login-action@v1 - with: - username: fabfuel - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Build and push - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: fabfuel/api-deploy:${{ github.ref_name }} - - - name: "Build and push (tag: latest)" - if: github.ref == 'refs/heads/develop' - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: fabfuel/api-deploy:latest diff --git a/README.md b/README.md index da726d2..b72ceb2 100644 --- a/README.md +++ b/README.md @@ -1 +1,134 @@ -# API Deploy \ No newline at end of file +# API Deploy + +Compile an OpenAPI spec into an Amazon API Gateway definition and deploy it. + +Packmatic fork of [fabfuel/api-deploy](https://github.com/fabfuel/api-deploy). Published +privately as `packmatic-api-deploy` to the `packmatic` AWS CodeArtifact domain. + +## Install + +```bash +aws codeartifact login --tool pip \ + --domain packmatic --domain-owner 038513119918 \ + --repository packmatic --region eu-central-1 + +pip install packmatic-api-deploy +``` + +For local development, pipx keeps the `api` CLI isolated: + +```bash +pipx uninstall api-deploy # remove the upstream build first, the `api` binary collides +pipx install packmatic-api-deploy --pip-args="--index-url $(aws codeartifact get-repository-endpoint \ + --domain packmatic --domain-owner 038513119918 --repository packmatic \ + --format pypi --region eu-central-1 --output text)simple/" +``` + +## Usage + +```bash +api compile # compile only +api deploy +``` + +## Configuration + +```yaml +gateway: + integrationHost: 'https://${stageVariables.host}' + connectionId: '${stageVariables.connectionId}' + removeScopes: true # strip OAuth scopes; API Gateway rejects them + removeDescriptions: true # strip `description` from components/schemas + removeExamples: true # strip `example` / `examples` +flatten: + dedupExternalRefs: true # share resolved external $refs instead of copying them +headers: + request: [Authorization, Content-Type] +cors: + origin: '*' +static: + files: [api.v1.yml] +strict: + enabled: true + overwriteRequired: true + blocklist: [_links, _meta] +generator: + languages: [typescript] + output: src/openapi/types +``` + +`removeExamples` and `dedupExternalRefs` both default to `false`, so existing configs +compile byte-for-byte as before. + +### `flatten.dedupExternalRefs` + +API Gateway's `put-rest-api` / `import-rest-api` body limit is +[6 MiB](https://docs.aws.amazon.com/apigateway/latest/api/API_PutRestApi.html#API_PutRestApi_RequestBody). +By default every external `$ref` is resolved into a **full inline copy** at each use site, and +YAML anchors are disabled, so a spec that shares a handful of error responses across a few +hundred endpoints inflates enormously — Packmatic's spec inlined ~1,100 copies of the same +six error schemas. + +With `dedupExternalRefs: true`, each resolved external schema is registered once under +`components/schemas` and every use site becomes an internal `$ref`. API Gateway resolves +internal `#/components/schemas/*` refs, so the result imports unchanged. + +Scope: refs in a `schema:` position, and the payload schema of any resolved object carrying +`content` (responses, request bodies). The response *wrapper* stays inline — only the payload +schema is shared. Refs nested inside `properties` are still inlined, as are parameter refs, +which API Gateway requires inline. + +Component names derive from the ref filename, forced to be alphanumeric and to start with a +letter (`responses/500-server-error.yml` → `Response500ServerError`), and are uniquified +against existing components. Schemas with identical bodies collapse into one component even +when reached through differently spelled refs. + +Effect on Packmatic's packaging spec: **6,364,377 B → 4,214,757 B**, or 3,915,048 B with +`removeExamples` as well. API Gateway model count drops from ~1,214 to ~128, since identical +models are shared rather than duplicated per use site; per-method validation is unchanged. + +### `gateway.removeExamples` + +Strips `example` and `examples`. A schema *property* literally named `example` is preserved — +only keyword positions are stripped — and `x-amazon-apigateway-integration` blocks are left +untouched. Note this runs before the CORS processor, so CORS response-header examples it +generates afterwards remain. + +Do not enable it in a config that also has a `generator` section: the TypeScript generator +emits `Example:` JSDoc from these values, so the generated types would lose those comments. +Keep it in the API-Gateway-only config. + +## Releasing + +Releases are published manually by a developer, not by CI. The flow is: + +1. open a PR with your change +2. get it approved and merged into `develop` +3. from `develop`, publish the package: + +```bash +./scripts/release.sh # interactive +./scripts/release.sh --bump patch # or non-interactive +./scripts/release.sh --bump none # publish the current version as-is +``` + +The script runs the unit tests, builds sdist + wheel in a throwaway venv, authenticates +to CodeArtifact with your own AWS credentials, and uploads. It bumps +`api_deploy/__init__.py` only — no git tag or commit — so commit that bump yourself +afterwards. + +Then pin the new version where it is consumed, e.g. `packaging`'s +`.github/workflows/deployment.yml`. + +## Development + +```bash +pip install . -r requirements-test.txt +pytest +flake8 api_deploy +``` + +`tests/functional/test_compile.py::test_compile_external_ref` and `::test_compile_one_of` +fetch live schemas from `api.packmatic.io` and currently fail against the committed +fixtures, which predate a change to the published `urn.yml` pattern. Pre-existing on +`develop`; unrelated to the flatten/examples options. \ No newline at end of file diff --git a/api_deploy/__init__.py b/api_deploy/__init__.py index c4d9146..518855e 100644 --- a/api_deploy/__init__.py +++ b/api_deploy/__init__.py @@ -1 +1 @@ -VERSION = '0.17.0' +VERSION = '0.18.0' diff --git a/api_deploy/config.py b/api_deploy/config.py index af5a874..47ea63d 100644 --- a/api_deploy/config.py +++ b/api_deploy/config.py @@ -14,6 +14,7 @@ def __init__(self, config_file: ConfigFile, file_path) -> None: 'response': [], }, 'strict': {}, + 'flatten': {}, 'gateway': {}, 'cors': {}, 'static': { @@ -32,6 +33,10 @@ def __init__(self, config_file: ConfigFile, file_path) -> None: default_config['gateway'].setdefault('connection_id', config_file.get('gateway', {}).get('connectionId', '')) default_config['gateway'].setdefault('remove_scopes', config_file.get('gateway', {}).get('removeScopes', False)) default_config['gateway'].setdefault('remove_descriptions', config_file.get('gateway', {}).get('removeDescriptions', False)) + default_config['gateway'].setdefault('remove_examples', config_file.get('gateway', {}).get('removeExamples', False)) + + default_config['flatten'].setdefault('dedup_external_refs', + config_file.get('flatten', {}).get('dedupExternalRefs', False)) default_config['cors'].setdefault('allow_origin', config_file.get('cors', {}).get('origin', '*')) diff --git a/api_deploy/converters.py b/api_deploy/converters.py index 141c909..eb04fab 100644 --- a/api_deploy/converters.py +++ b/api_deploy/converters.py @@ -1,4 +1,7 @@ +import re from copy import deepcopy + +import yaml from mergedeep import merge from requests import get, HTTPError @@ -18,7 +21,7 @@ def __init__(self) -> None: def default(cls, config: Config): default_manager = cls() default_manager.register(StaticFileProcessor(config, **config['static'])) - default_manager.register(FlattenProcessor(config)) + default_manager.register(FlattenProcessor(config, **config['flatten'])) default_manager.register(PassthroughProcessor(config, **config['headers'])) default_manager.register(StrictProcessor(config, **config['strict'])) default_manager.register(ApiGatewayProcessor(config, **config['gateway'])) @@ -40,11 +43,14 @@ def process(self, original_schema: Schema) -> Schema: class FlattenProcessor(AbstractProcessor): - def __init__(self, config: Config, **kwargs) -> None: + def __init__(self, config: Config, dedup_external_refs=False, **kwargs) -> None: super().__init__(config) self.base_url = None self.used_refs = set() self.external_schemas = {} + self.dedup_external_refs = dedup_external_refs + self.hoisted_names_by_fingerprint = {} + self.schemas_to_register = {} def process(self, source: Schema) -> Schema: target = deepcopy(source) @@ -63,6 +69,12 @@ def process(self, source: Schema) -> Schema: self.replace_refs_dict(target['paths'], target) self.replace_refs_dict(target['paths'], target) + # Register hoisted schemas, resolving refs inside them until nothing new is hoisted + while self.schemas_to_register: + newly_hoisted, self.schemas_to_register = self.schemas_to_register, {} + target['components']['schemas'].update(newly_hoisted) + self.replace_refs_dict(target['components']['schemas'], target) + try: del target['components']['parameters'] except KeyError: @@ -78,6 +90,8 @@ def process(self, source: Schema) -> Schema: def replace_refs_dict(self, node, schema, replace_ref=True, enforce_replace=False): if self.is_ref(node) and (replace_ref or enforce_replace or self.is_external_ref(node)): + if self.dedup_external_refs and self.is_external_ref(node) and not enforce_replace: + return self.hoist_external_ref(node, schema, is_schema_position=not replace_ref) return self.lookup_ref(node, schema) elif self.is_ref(node): self.used_refs.add(self.get_ref_model_name(node['$ref'])) @@ -106,6 +120,65 @@ def get_to_used_schemas(schemas: dict, used_refs: set): used_schemas[model] = schemas[model] return used_schemas + def hoist_external_ref(self, ref: dict, schema: Schema, is_schema_position: bool): + """Resolve an external $ref once into components/schemas and reference it from every use site. + + API Gateway resolves internal $refs, so this collapses thousands of duplicated inline copies. + """ + ref_url = ref['$ref'] + resolved = deepcopy(self.lookup_ref(ref, schema)) + + if not isinstance(resolved, dict): + return resolved + + if is_schema_position: + return self.register_hoisted_schema(ref_url, resolved, schema) + + # Response and request body objects: hoist only the payload schema, keep the small wrapper inline + if isinstance(resolved.get('content'), dict): + for media_type, media in resolved['content'].items(): + if isinstance(media, dict) and isinstance(media.get('schema'), dict): + media['schema'] = self.register_hoisted_schema( + f'{ref_url}#{media_type}', media['schema'], schema + ) + + return resolved + + def register_hoisted_schema(self, ref_url: str, body: dict, schema: Schema): + # Identical schemas reached through differently spelled refs must collapse into one component + fingerprint = yaml.dump(body, sort_keys=True) + component_name = self.hoisted_names_by_fingerprint.get(fingerprint) + + if not component_name: + component_name = self.build_component_name(ref_url, schema) + self.hoisted_names_by_fingerprint[fingerprint] = component_name + self.schemas_to_register[component_name] = body + + self.used_refs.add(component_name) + + return {'$ref': f'#/components/schemas/{component_name}'} + + def build_component_name(self, ref_url: str, schema: Schema): + directory, _, file_name = ref_url.split('#')[0].rpartition('/') + base_name = self.to_pascal_case(re.sub(r'\.(ya?ml|json)$', '', file_name)) + + # API Gateway model names must be alphanumeric and cannot start with a digit + if not base_name[:1].isalpha(): + base_name = self.to_pascal_case(directory.rpartition('/')[2].rstrip('s')) + base_name + + taken = set(schema.get('components', {}).get('schemas', {})) | set(self.schemas_to_register) + component_name = base_name + suffix = 2 + while component_name in taken: + component_name = f'{base_name}{suffix}' + suffix += 1 + + return component_name + + @staticmethod + def to_pascal_case(value: str): + return ''.join(part[:1].upper() + part[1:] for part in re.split(r'[^A-Za-z0-9]+', value) if part) + @staticmethod def get_ref_model_name(ref): return ref.split('/')[-1] @@ -203,12 +276,14 @@ def merge_all_of(self, node, schema: Schema): class ApiGatewayProcessor(AbstractProcessor): - def __init__(self, config: Config, integration_host, connection_id, remove_scopes, remove_descriptions, **kwargs) -> None: + def __init__(self, config: Config, integration_host, connection_id, remove_scopes, remove_descriptions, + remove_examples=False, **kwargs) -> None: super().__init__(config) self.integration_host = integration_host self.connection_id = connection_id self.remove_scopes = remove_scopes self.remove_descriptions = remove_descriptions + self.remove_examples = remove_examples def process(self, schema: Schema) -> Schema: for path in schema['paths']: @@ -237,6 +312,11 @@ def process(self, schema: Schema) -> Schema: for model_name in schema['components'].get('schemas', {}): self._remove_descriptions(schema['components']['schemas'][model_name]) + # Examples are documentation only, API Gateway ignores them + if self.remove_examples: + self._remove_examples(schema['paths']) + self._remove_examples(schema['components']) + # Replace all authorizers with API key type for authorizer in schema['components'].get('securitySchemes', {}): scheme = schema['components']['securitySchemes'][authorizer] @@ -264,6 +344,23 @@ def _remove_descriptions(self, schema: object): self._remove_descriptions(schema['items']['properties'][property_name]) + def _remove_examples(self, node: object): + if isinstance(node, dict): + node.pop('example', None) + node.pop('examples', None) + + for key, value in node.items(): + # Never treat a schema property literally named "example" as a keyword + if key == 'properties' and isinstance(value, dict): + for property_schema in value.values(): + self._remove_examples(property_schema) + elif key != 'x-amazon-apigateway-integration': + self._remove_examples(value) + + elif isinstance(node, list): + for item in node: + self._remove_examples(item) + def _get_response_codes(self, schema, path, method): responses = { 'default': { diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..a4d78ce --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +VERSION_FILE="$REPO_ROOT/api_deploy/__init__.py" +BUMP="" + +# --- Help --- +usage() { + cat <<'HELP' +Usage: ./scripts/release.sh [OPTIONS] + +Publish packmatic-api-deploy to AWS CodeArtifact. Without options the script +runs interactively. + +Options: + --bump Version bump type: patch, minor, major, or none + (none publishes the version currently in api_deploy/__init__.py) + --help Show this help message + +Examples: + ./scripts/release.sh # interactive prompts + ./scripts/release.sh --bump patch # bump patch, then publish + ./scripts/release.sh --bump none # publish the current version as-is + +Notes: + - Run this from develop after your PR is merged. + - The version is bumped in api_deploy/__init__.py only (no git tags or commits). + Commit that change yourself afterwards. + - Requires valid AWS credentials with access to the packmatic CodeArtifact repository. +HELP + exit 0 +} + +# --- Parse arguments --- +while [[ $# -gt 0 ]]; do + case "$1" in + --bump) BUMP="$2"; shift 2 ;; + --help|-h) usage ;; + *) echo "Unknown option: $1. Use --help for usage."; exit 1 ;; + esac +done + +read_version() { + python3 -c "import re,sys;print(re.search(r\"VERSION = '([^']+)'\", open(sys.argv[1]).read()).group(1))" "$VERSION_FILE" +} + +CURRENT_VERSION="$(read_version)" + +# --- Interactive: select bump type --- +if [[ -z "$BUMP" ]]; then + echo "" + echo "Current version: $CURRENT_VERSION" + echo "" + echo "Version bump type?" + echo " 1) patch" + echo " 2) minor" + echo " 3) major" + echo " 4) none (publish $CURRENT_VERSION as-is)" + echo "" + read -rp "Select [1/2/3/4]: " choice + case "$choice" in + 1) BUMP="patch" ;; + 2) BUMP="minor" ;; + 3) BUMP="major" ;; + 4) BUMP="none" ;; + *) echo "Invalid choice"; exit 1 ;; + esac +fi + +if [[ "$BUMP" != "patch" && "$BUMP" != "minor" && "$BUMP" != "major" && "$BUMP" != "none" ]]; then + echo "Error: Invalid bump type '$BUMP'. Must be 'patch', 'minor', 'major', or 'none'." + exit 1 +fi + +# --- Bump version --- +cd "$REPO_ROOT" + +if [[ "$BUMP" != "none" ]]; then + NEW_VERSION=$(BUMP="$BUMP" python3 - "$VERSION_FILE" <<'PY' +import os, re, sys + +path = sys.argv[1] +source = open(path).read() +current = re.search(r"VERSION = '([^']+)'", source).group(1) + +major, minor, patch = (int(part) for part in current.split('.')[:3]) +bump = os.environ['BUMP'] +if bump == 'major': + major, minor, patch = major + 1, 0, 0 +elif bump == 'minor': + minor, patch = minor + 1, 0 +else: + patch += 1 + +new = f'{major}.{minor}.{patch}' +open(path, 'w').write(re.sub(r"VERSION = '[^']+'", f"VERSION = '{new}'", source)) +print(new) +PY +) + echo "Version bumped to $NEW_VERSION (was $CURRENT_VERSION)" +else + NEW_VERSION="$CURRENT_VERSION" + echo "Publishing current version $NEW_VERSION" +fi + +# --- Build tooling in a throwaway venv, so nothing global is touched --- +BUILD_VENV="$(mktemp -d)/venv" +trap 'rm -rf "$(dirname "$BUILD_VENV")"' EXIT + +echo "" +echo "Preparing build environment..." +python3 -m venv "$BUILD_VENV" +"$BUILD_VENV/bin/pip" install --quiet --upgrade pip build twine + +# --- Test --- +echo "" +echo "Running tests..." +"$BUILD_VENV/bin/pip" install --quiet . -r requirements-test.txt +"$BUILD_VENV/bin/python" -m pytest tests/unit -q + +# --- Build --- +echo "" +echo "Building distributions..." +rm -rf "$REPO_ROOT/dist" +"$BUILD_VENV/bin/python" -m build --outdir "$REPO_ROOT/dist" +ls -1 "$REPO_ROOT/dist" + +# --- Authenticate with CodeArtifact --- +echo "" +echo "Authenticating with AWS CodeArtifact..." +aws codeartifact login \ + --tool twine \ + --domain packmatic \ + --domain-owner 038513119918 \ + --repository packmatic \ + --region eu-central-1 + +# --- Publish --- +echo "" +echo "Publishing..." +"$BUILD_VENV/bin/twine" upload --repository codeartifact "$REPO_ROOT/dist"/* + +# --- Summary --- +echo "" +echo "==========================================" +echo "Release complete!" +echo "==========================================" +echo " packmatic-api-deploy $NEW_VERSION" +echo "" +echo " pip install packmatic-api-deploy==$NEW_VERSION" +echo "" +if [[ "$BUMP" != "none" ]]; then + echo " Remember to commit the version bump in api_deploy/__init__.py" + echo "" +fi diff --git a/setup.py b/setup.py index bfd3c25..5656d52 100644 --- a/setup.py +++ b/setup.py @@ -22,15 +22,17 @@ def readme(): ] setup( - name='api-deploy', + name='packmatic-api-deploy', version=VERSION, - url='https://github.com/fabfuel/api-deploy', - download_url='https://github.com/fabfuel/api-deploy/archive/%s.tar.gz' % VERSION, + url='https://github.com/Packmatic/api-deploy', license='BSD-3-Clause', author='Fabian Fuelling', author_email='pypi@fabfuel.de', - description='Manage Amazon REST API Gateway deployments', + maintainer='Packmatic Tech', + maintainer_email='jonas.cwojdzinski@packmatic.io', + description='Manage Amazon REST API Gateway deployments (Packmatic fork of api-deploy)', long_description=readme(), + long_description_content_type='text/markdown', packages=find_packages(exclude=['tests']), include_package_data=True, zip_safe=False, diff --git a/tests/unit/test_flatten_dedup.py b/tests/unit/test_flatten_dedup.py new file mode 100644 index 0000000..07d7fbd --- /dev/null +++ b/tests/unit/test_flatten_dedup.py @@ -0,0 +1,139 @@ +from api_deploy.config import Config, ConfigFile +from api_deploy.converters import FlattenProcessor +from api_deploy.schema import Schema, YamlDict + +ERROR_RESPONSE = ''' +description: Internal Server Error +content: + application/json: + schema: + type: object + required: + - message + properties: + message: + type: string + example: Internal Server Error +''' + +URN_SCHEMA = ''' +type: string +description: Uniform Resource Name +example: urn:pm:service::foobar/1 +''' + +EXTERNAL_SCHEMAS = { + 'https://api.example.com/types/responses/500-server-error.yml': ERROR_RESPONSE, + # Same body, different URL: must collapse into a single component + 'https://api.example.com/types/mirror/500-server-error.yml': ERROR_RESPONSE, + 'https://api.example.com/types/schemas/urn.yml': URN_SCHEMA, +} + + +class OfflineFlattenProcessor(FlattenProcessor): + """The real processor fetches external refs over HTTP; serve them from a dict instead.""" + + def get_external_schema(self, url): + return YamlDict(EXTERNAL_SCHEMAS[url]) + + +def build_source(mirror_ref=False): + second_ref = ('https://api.example.com/types/mirror/500-server-error.yml' if mirror_ref + else 'https://api.example.com/types/responses/500-server-error.yml') + return Schema(f''' +openapi: 3.0.1 +info: + title: Test + version: '1' +servers: + - url: http://localhost +tags: [] +paths: + /foo: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + urn: + $ref: 'https://api.example.com/types/schemas/urn.yml' + '500': + $ref: 'https://api.example.com/types/responses/500-server-error.yml' + /bar: + get: + responses: + '500': + $ref: '{second_ref}' +components: + schemas: {{}} +''') + + +def build_processor(dedup_external_refs): + config = Config(ConfigFile(f'flatten:\n dedupExternalRefs: {str(dedup_external_refs).lower()}\n'), 'test') + assert config['flatten']['dedup_external_refs'] is dedup_external_refs + return OfflineFlattenProcessor(config, **config['flatten']) + + +def response_schema(schema, path): + return schema['paths'][path]['get']['responses']['500']['content']['application/json']['schema'] + + +def test_external_refs_are_inlined_by_default(): + processed = build_processor(dedup_external_refs=False).process(build_source()) + + for path in ('/foo', '/bar'): + inlined = response_schema(processed, path) + assert '$ref' not in inlined + assert inlined['properties']['message']['type'] == 'string' + + assert processed['components']['schemas'] == {} + + +def test_dedup_hoists_response_payload_into_components(): + processed = build_processor(dedup_external_refs=True).process(build_source()) + + ref = {'$ref': '#/components/schemas/Response500ServerError'} + assert response_schema(processed, '/foo') == ref + assert response_schema(processed, '/bar') == ref + + hoisted = processed['components']['schemas']['Response500ServerError'] + assert hoisted['properties']['message']['type'] == 'string' + assert hoisted['required'] == ['message'] + + # The response wrapper itself must stay inline, only the payload schema is shared + assert processed['paths']['/foo']['get']['responses']['500']['description'] == 'Internal Server Error' + + +def test_hoisted_component_names_are_api_gateway_safe(): + processed = build_processor(dedup_external_refs=True).process(build_source()) + + for name in processed['components']['schemas']: + assert name.isalnum(), f'{name} is not alphanumeric' + assert name[0].isalpha(), f'{name} must not start with a digit' + + +def test_identical_bodies_reached_via_different_urls_collapse(): + processed = build_processor(dedup_external_refs=True).process(build_source(mirror_ref=True)) + + assert len(processed['components']['schemas']) == 1 + assert response_schema(processed, '/foo') == response_schema(processed, '/bar') + + +def test_refs_nested_in_properties_stay_inline(): + processed = build_processor(dedup_external_refs=True).process(build_source()) + + urn = processed['paths']['/foo']['get']['responses']['200']['content']['application/json']['schema'] + assert urn['properties']['urn']['type'] == 'string' + assert '$ref' not in urn['properties']['urn'] + + +def test_dedup_leaves_no_external_refs(): + processed = build_processor(dedup_external_refs=True).process(build_source()) + + dumped = processed.dump() + assert 'https://' not in dumped.replace('http://localhost', '') \ No newline at end of file diff --git a/tests/unit/test_remove_examples.py b/tests/unit/test_remove_examples.py new file mode 100644 index 0000000..999f3cb --- /dev/null +++ b/tests/unit/test_remove_examples.py @@ -0,0 +1,96 @@ +from api_deploy.config import Config, ConfigFile +from api_deploy.converters import ApiGatewayProcessor +from api_deploy.schema import Schema + + +def build_source(): + return Schema(''' +openapi: 3.0.1 +info: + title: Test + version: '1' +servers: + - url: http://localhost +tags: [] +paths: + /foo: + get: + parameters: + - name: filter + in: query + schema: + type: string + example: abc + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: Foo + example: + type: string + example: this property is literally named "example" + examples: + sample: + value: {name: Foo} +components: + schemas: + Thing: + type: object + example: {a: 1} + properties: + a: + type: integer + example: 1 +''') + + +def build_processor(remove_examples): + config = Config(ConfigFile(f'gateway:\n removeExamples: {str(remove_examples).lower()}\n'), 'test') + assert config['gateway']['remove_examples'] is remove_examples + return ApiGatewayProcessor(config, **config['gateway']) + + +def response_schema(schema): + return schema['paths']['/foo']['get']['responses']['200']['content']['application/json'] + + +def test_examples_are_kept_by_default(): + processed = build_processor(remove_examples=False).process(build_source()) + + assert response_schema(processed)['schema']['properties']['name']['example'] == 'Foo' + assert processed['components']['schemas']['Thing']['example'] == {'a': 1} + + +def test_examples_are_removed_when_enabled(): + processed = build_processor(remove_examples=True).process(build_source()) + + media = response_schema(processed) + assert 'example' not in media['schema']['properties']['name'] + assert 'examples' not in media + assert 'example' not in processed['components']['schemas']['Thing'] + assert 'example' not in processed['components']['schemas']['Thing']['properties']['a'] + assert 'example' not in processed['paths']['/foo']['get']['parameters'][0]['schema'] + + +def test_a_property_named_example_is_never_treated_as_a_keyword(): + processed = build_processor(remove_examples=True).process(build_source()) + + properties = response_schema(processed)['schema']['properties'] + assert 'example' in properties, 'a schema property named "example" must survive' + assert properties['example']['type'] == 'string' + # ...but its own annotation is still stripped + assert 'example' not in properties['example'] + + +def test_integration_blocks_are_left_alone(): + processed = build_processor(remove_examples=True).process(build_source()) + + integration = processed['paths']['/foo']['get']['x-amazon-apigateway-integration'] + assert integration['type'] == 'http' + assert integration['responses']['200']['statusCode'] == '200' \ No newline at end of file From 7e1fa9c683da75dc3ce67a5edb5677bc5aa46fd2 Mon Sep 17 00:00:00 2001 From: Usman Ahmed Saeed Date: Wed, 29 Jul 2026 16:52:29 +0200 Subject: [PATCH 2/7] re order generated yml and update read me --- README.md | 10 +- tests/openapi/external_ref_target.yml | 148 ++++++++++++++- tests/openapi/one_of_target.yml | 264 +++++++++++++++++--------- 3 files changed, 322 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index b72ceb2..eae457e 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,9 @@ pytest flake8 api_deploy ``` -`tests/functional/test_compile.py::test_compile_external_ref` and `::test_compile_one_of` -fetch live schemas from `api.packmatic.io` and currently fail against the committed -fixtures, which predate a change to the published `urn.yml` pattern. Pre-existing on -`develop`; unrelated to the flatten/examples options. \ No newline at end of file +The functional tests resolve external `$ref`s against the live schemas at +`api.packmatic.io` / `api-staging.packmatic.io`, which are served from the +[api-types](https://github.com/Packmatic/api-types) repo. When api-types changes a shared +schema, `tests/openapi/*_target.yml` goes stale and must be regenerated — api-types 1.11.0 +widened the `urn.yml` pattern to support two ids and added fields to the error responses, +which is why those fixtures were refreshed. \ No newline at end of file diff --git a/tests/openapi/external_ref_target.yml b/tests/openapi/external_ref_target.yml index adc47c5..3d8a078 100644 --- a/tests/openapi/external_ref_target.yml +++ b/tests/openapi/external_ref_target.yml @@ -83,16 +83,48 @@ paths: statusCode: type: integer example: 401 + description: Deprecated HTTP status code message: type: string example: Unauthorized + description: Deprecated human readable error message error: type: string example: Unauthorized + description: Deprecated machine readable error message incident: description: Unique incident identifier type: string example: 5beb965c-7ffc-468b-a063-eb47c5b366c2 + status: + type: integer + example: 400 + description: HTTP status code + name: + type: string + example: Bad Request + description: HTTP status name + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Invalid value + description: Human readable error message + errorCode: + type: string + example: SomeCustomError + description: Machine readable error code of what happened + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred headers: Access-Control-Allow-Origin: schema: @@ -112,16 +144,48 @@ paths: statusCode: type: integer example: 403 + description: Deprecated HTTP status code message: type: string example: Forbidden resource + description: Deprecated human readable error message error: type: string example: Forbidden + description: Deprecated machine readable error message incident: description: Unique incident identifier type: string example: 5beb965c-7ffc-468b-a063-eb47c5b366c2 + status: + type: integer + example: 400 + description: HTTP status code + name: + type: string + example: Bad Request + description: HTTP status name + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Invalid value + description: Human readable error message + errorCode: + type: string + example: SomeCustomError + description: Machine readable error code of what happened + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred headers: Access-Control-Allow-Origin: schema: @@ -141,16 +205,48 @@ paths: statusCode: type: integer example: 500 + description: Deprecated HTTP status code message: type: string example: Internal Server Error + description: Deprecated human readable error message error: type: string example: Internal Server Error + description: Deprecated machine readable error message incident: description: Unique incident identifier type: string example: 5beb965c-7ffc-468b-a063-eb47c5b366c2 + status: + type: integer + example: 400 + description: HTTP status code + name: + type: string + example: Bad Request + description: HTTP status name + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Invalid value + description: Human readable error message + errorCode: + type: string + example: SomeCustomError + description: Machine readable error code of what happened + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred headers: Access-Control-Allow-Origin: schema: @@ -283,16 +379,48 @@ paths: statusCode: type: integer example: 500 + description: Deprecated HTTP status code message: type: string example: Internal Server Error + description: Deprecated human readable error message error: type: string example: Internal Server Error + description: Deprecated machine readable error message incident: description: Unique incident identifier type: string example: 5beb965c-7ffc-468b-a063-eb47c5b366c2 + status: + type: integer + example: 400 + description: HTTP status code + name: + type: string + example: Bad Request + description: HTTP status name + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Invalid value + description: Human readable error message + errorCode: + type: string + example: SomeCustomError + description: Machine readable error code of what happened + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred headers: Access-Control-Allow-Origin: schema: @@ -731,15 +859,13 @@ components: - modifiedBy additionalProperties: false properties: - additionalField: - type: string id: type: string - example: 5d4470a6-121b-40d2-aff9-00f24aa4a110 readOnly: true + example: 5d4470a6-121b-40d2-aff9-00f24aa4a110 urn: type: string - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 tenant: type: string @@ -756,7 +882,7 @@ components: format: date-time createdBy: type: string - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 modifiedAt: type: string @@ -764,9 +890,11 @@ components: nullable: true modifiedBy: type: string - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 nullable: true + additionalField: + type: string TestTwo: type: object readOnly: true @@ -782,11 +910,11 @@ components: properties: id: type: string - example: 5d4470a6-121b-40d2-aff9-00f24aa4a110 readOnly: true + example: 5d4470a6-121b-40d2-aff9-00f24aa4a110 urn: type: string - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 tenant: type: string @@ -803,7 +931,7 @@ components: format: date-time createdBy: type: string - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 modifiedAt: type: string @@ -811,7 +939,7 @@ components: nullable: true modifiedBy: type: string - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 nullable: true securitySchemes: diff --git a/tests/openapi/one_of_target.yml b/tests/openapi/one_of_target.yml index 7289b3f..0f9a9b2 100644 --- a/tests/openapi/one_of_target.yml +++ b/tests/openapi/one_of_target.yml @@ -11,8 +11,8 @@ servers: tags: - name: Message description: Message sending -- name: Static Files - description: Static file content +- description: Static file content + name: Static Files - name: CORS description: CORS configuration endpoints paths: @@ -28,8 +28,8 @@ paths: application/json: schema: oneOf: - - $ref: '#/components/schemas/UserCreated' - - $ref: '#/components/schemas/ProjectCreated' + - $ref: '#/components/schemas/UserCreated' + - $ref: '#/components/schemas/ProjectCreated' discriminator: propertyName: message responses: @@ -39,102 +39,197 @@ paths: application/json: schema: oneOf: - - $ref: '#/components/schemas/UserCreated' - - $ref: '#/components/schemas/ProjectCreated' - - $ref: '#/components/schemas/MessageList' + - $ref: '#/components/schemas/UserCreated' + - $ref: '#/components/schemas/ProjectCreated' + - $ref: '#/components/schemas/MessageList' headers: Access-Control-Allow-Origin: schema: type: string - example: 'foo.com' + example: foo.com '400': description: Bad Request content: application/json: schema: + type: object + required: + - statusCode + - message additionalProperties: false properties: - error: + statusCode: + type: integer + example: 400 + description: Deprecated HTTP status code + message: + type: string example: Bad Request + description: Deprecated human readable error message + error: type: string + example: Bad Request + description: Deprecated machine readable error message incident: description: Unique incident identifier + type: string example: 5beb965c-7ffc-468b-a063-eb47c5b366c2 + status: + type: integer + example: 400 + description: HTTP status code + name: type: string - message: example: Bad Request + description: HTTP status name + path: type: string - statusCode: - example: 400 - type: integer - required: - - statusCode - - message - type: object + example: /pm/v1/foobar + description: Path of where the error occurred + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Invalid value + description: Human readable error message + errorCode: + type: string + example: SomeCustomError + description: Machine readable error code of what happened + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred headers: Access-Control-Allow-Origin: schema: type: string - example: 'foo.com' + example: foo.com '404': description: Not Found content: application/json: schema: + type: object + required: + - statusCode + - message additionalProperties: false properties: + statusCode: + type: integer + example: 404 + description: Deprecated HTTP status code + message: + type: string + example: Can not get entity /tasks/123 + description: Deprecated human readable error message error: - example: Not Found type: string + example: Not Found + description: Deprecated machine readable error message incident: description: Unique incident identifier + type: string example: 5beb965c-7ffc-468b-a063-eb47c5b366c2 + status: + type: integer + example: 400 + description: HTTP status code + name: type: string - message: - example: Can not get entity /tasks/123 + example: Bad Request + description: HTTP status name + path: type: string - statusCode: - example: 404 - type: integer - required: - - statusCode - - message - type: object + example: /pm/v1/foobar + description: Path of where the error occurred + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Invalid value + description: Human readable error message + errorCode: + type: string + example: SomeCustomError + description: Machine readable error code of what happened + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred headers: Access-Control-Allow-Origin: schema: type: string - example: 'foo.com' + example: foo.com '500': description: Internal Server Error content: application/json: schema: + type: object + required: + - statusCode + - message additionalProperties: false properties: - error: + statusCode: + type: integer + example: 500 + description: Deprecated HTTP status code + message: + type: string example: Internal Server Error + description: Deprecated human readable error message + error: type: string + example: Internal Server Error + description: Deprecated machine readable error message incident: description: Unique incident identifier + type: string example: 5beb965c-7ffc-468b-a063-eb47c5b366c2 + status: + type: integer + example: 400 + description: HTTP status code + name: type: string - message: - example: Internal Server Error + example: Bad Request + description: HTTP status name + path: type: string - statusCode: - example: 500 - type: integer - required: - - statusCode - - message - type: object - + example: /pm/v1/foobar + description: Path of where the error occurred + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Invalid value + description: Human readable error message + errorCode: + type: string + example: SomeCustomError + description: Machine readable error code of what happened + path: + type: string + example: /pm/v1/foobar + description: Path of where the error occurred headers: Access-Control-Allow-Origin: schema: type: string - example: 'foo.com' + example: foo.com x-amazon-apigateway-request-validator: all parameters: - name: host @@ -162,8 +257,8 @@ paths: required: false schema: type: string - - in: header - name: x-datadog-sampling-priority + - name: x-datadog-sampling-priority + in: header required: false schema: type: string @@ -213,7 +308,7 @@ paths: Access-Control-Allow-Origin: schema: type: string - example: 'foo.com' + example: foo.com Access-Control-Allow-Methods: schema: type: string @@ -242,18 +337,18 @@ paths: '200': description: Static file api.v1.yml headers: - Access-Control-Allow-Origin: + Content-Type: schema: + example: text/yaml type: string - example: 'foo.com' Access-Control-Expose-Headers: schema: - example: Content-Type type: string - Content-Type: + example: Content-Type + Access-Control-Allow-Origin: schema: - example: text/yaml type: string + example: foo.com parameters: - name: host in: header @@ -300,9 +395,9 @@ paths: '200': statusCode: '200' responseParameters: + method.response.header.Content-Type: integration.response.header.Content-Type method.response.header.Access-Control-Allow-Origin: '''foo.com''' method.response.header.Access-Control-Expose-Headers: '''Content-Type''' - method.response.header.Content-Type: integration.response.header.Content-Type requestParameters: integration.request.header.User-Agent: context.identity.userAgent integration.request.header.X-Forwarded-For: context.identity.sourceIp @@ -325,7 +420,7 @@ paths: Access-Control-Allow-Origin: schema: type: string - example: 'foo.com' + example: foo.com Access-Control-Allow-Methods: schema: type: string @@ -339,8 +434,8 @@ paths: default: statusCode: '200' responseParameters: - method.response.header.Access-Control-Allow-Headers: '''authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id''' method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' + method.response.header.Access-Control-Allow-Headers: '''authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id''' method.response.header.Access-Control-Allow-Origin: '''foo.com''' requestTemplates: application/json: '{"statusCode": 200}' @@ -348,89 +443,86 @@ paths: type: mock components: schemas: + MessageList: + type: array + items: + type: object + additionalProperties: true UserCreated: type: object - additionalProperties: false properties: recipients: type: array minItems: 1 example: - - urn:pm:users:pm.demo:user/1234-123456-123456-1234 - - urn:pm:users:pm.demo:role/customer + - urn:pm:users:pm.demo:user/1234-123456-123456-1234 + - urn:pm:users:pm.demo:role/customer items: - example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ type: string - + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 subject: type: string message: type: string enum: - - UserCreated - - ProjectCreated + - UserCreated + - ProjectCreated parameters: type: object - additionalProperties: false properties: name: type: string email: type: string + additionalProperties: false required: - - name - - email + - name + - email + additionalProperties: false required: - - recipients - - subject - - message - - parameters - - MessageList: - items: - additionalProperties: true - type: object - type: array + - recipients + - subject + - message + - parameters ProjectCreated: type: object - additionalProperties: false properties: recipients: type: array minItems: 1 example: - - urn:pm:users:pm.demo:user/1234-123456-123456-1234 - - urn:pm:users:pm.demo:role/customer + - urn:pm:users:pm.demo:user/1234-123456-123456-1234 + - urn:pm:users:pm.demo:role/customer items: - example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 - pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+$ type: string - + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + example: urn:pm:service::foobar/d9a6fc2e-a4b3-4fba-9f20-c5bd2fdb5071 subject: type: string message: type: string enum: - - UserCreated - - ProjectCreated + - UserCreated + - ProjectCreated parameters: type: object - additionalProperties: false properties: label: type: string start: type: string format: datetime + additionalProperties: false required: - label - start + additionalProperties: false required: - - recipients - - subject - - message - - parameters + - recipients + - subject + - message + - parameters x-amazon-apigateway-request-validators: all: validateRequestParameters: true From fe998bab2f98e75c6b121ec652e8a355f699339d Mon Sep 17 00:00:00 2001 From: Usman Ahmed Saeed Date: Wed, 29 Jul 2026 17:16:25 +0200 Subject: [PATCH 3/7] use docker image deploy --- .github/workflows/docker.yml | 55 ++++++++++++ README.md | 43 ++-------- scripts/release.sh | 156 ----------------------------------- 3 files changed, 63 insertions(+), 191 deletions(-) create mode 100644 .github/workflows/docker.yml delete mode 100755 scripts/release.sh diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..12be951 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,55 @@ +name: Docker + +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + +on: + push: + branches: + - 'develop' + - 'main' + tags: + - '*.*.*' + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ghcr.io/packmatic/api-deploy:${{ github.ref_name }} + - + name: "Build and push (tag: latest)" + if: github.ref == 'refs/heads/develop' + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ghcr.io/packmatic/api-deploy:latest diff --git a/README.md b/README.md index eae457e..ee271b7 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,16 @@ Compile an OpenAPI spec into an Amazon API Gateway definition and deploy it. -Packmatic fork of [fabfuel/api-deploy](https://github.com/fabfuel/api-deploy). Published -privately as `packmatic-api-deploy` to the `packmatic` AWS CodeArtifact domain. +Packmatic fork of [fabfuel/api-deploy](https://github.com/fabfuel/api-deploy). Pushing to +`develop` publishes `ghcr.io/packmatic/api-deploy:develop`, which `packaging`'s API Gateway +deploy uses. ## Install ```bash -aws codeartifact login --tool pip \ - --domain packmatic --domain-owner 038513119918 \ - --repository packmatic --region eu-central-1 - -pip install packmatic-api-deploy -``` - -For local development, pipx keeps the `api` CLI isolated: - -```bash -pipx uninstall api-deploy # remove the upstream build first, the `api` binary collides -pipx install packmatic-api-deploy --pip-args="--index-url $(aws codeartifact get-repository-endpoint \ - --domain packmatic --domain-owner 038513119918 --repository packmatic \ - --format pypi --region eu-central-1 --output text)simple/" +docker run --rm -v "$PWD":/workspace -w /workspace \ + ghcr.io/packmatic/api-deploy:develop \ + api compile ``` ## Usage @@ -100,25 +90,8 @@ Keep it in the API-Gateway-only config. ## Releasing -Releases are published manually by a developer, not by CI. The flow is: - -1. open a PR with your change -2. get it approved and merged into `develop` -3. from `develop`, publish the package: - -```bash -./scripts/release.sh # interactive -./scripts/release.sh --bump patch # or non-interactive -./scripts/release.sh --bump none # publish the current version as-is -``` - -The script runs the unit tests, builds sdist + wheel in a throwaway venv, authenticates -to CodeArtifact with your own AWS credentials, and uploads. It bumps -`api_deploy/__init__.py` only — no git tag or commit — so commit that bump yourself -afterwards. - -Then pin the new version where it is consumed, e.g. `packaging`'s -`.github/workflows/deployment.yml`. +Merging to `develop` builds and pushes `ghcr.io/packmatic/api-deploy:develop` (and `:latest`). +Pushing a `x.y.z` tag publishes that tag too. Nothing to run by hand. ## Development diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index a4d78ce..0000000 --- a/scripts/release.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -VERSION_FILE="$REPO_ROOT/api_deploy/__init__.py" -BUMP="" - -# --- Help --- -usage() { - cat <<'HELP' -Usage: ./scripts/release.sh [OPTIONS] - -Publish packmatic-api-deploy to AWS CodeArtifact. Without options the script -runs interactively. - -Options: - --bump Version bump type: patch, minor, major, or none - (none publishes the version currently in api_deploy/__init__.py) - --help Show this help message - -Examples: - ./scripts/release.sh # interactive prompts - ./scripts/release.sh --bump patch # bump patch, then publish - ./scripts/release.sh --bump none # publish the current version as-is - -Notes: - - Run this from develop after your PR is merged. - - The version is bumped in api_deploy/__init__.py only (no git tags or commits). - Commit that change yourself afterwards. - - Requires valid AWS credentials with access to the packmatic CodeArtifact repository. -HELP - exit 0 -} - -# --- Parse arguments --- -while [[ $# -gt 0 ]]; do - case "$1" in - --bump) BUMP="$2"; shift 2 ;; - --help|-h) usage ;; - *) echo "Unknown option: $1. Use --help for usage."; exit 1 ;; - esac -done - -read_version() { - python3 -c "import re,sys;print(re.search(r\"VERSION = '([^']+)'\", open(sys.argv[1]).read()).group(1))" "$VERSION_FILE" -} - -CURRENT_VERSION="$(read_version)" - -# --- Interactive: select bump type --- -if [[ -z "$BUMP" ]]; then - echo "" - echo "Current version: $CURRENT_VERSION" - echo "" - echo "Version bump type?" - echo " 1) patch" - echo " 2) minor" - echo " 3) major" - echo " 4) none (publish $CURRENT_VERSION as-is)" - echo "" - read -rp "Select [1/2/3/4]: " choice - case "$choice" in - 1) BUMP="patch" ;; - 2) BUMP="minor" ;; - 3) BUMP="major" ;; - 4) BUMP="none" ;; - *) echo "Invalid choice"; exit 1 ;; - esac -fi - -if [[ "$BUMP" != "patch" && "$BUMP" != "minor" && "$BUMP" != "major" && "$BUMP" != "none" ]]; then - echo "Error: Invalid bump type '$BUMP'. Must be 'patch', 'minor', 'major', or 'none'." - exit 1 -fi - -# --- Bump version --- -cd "$REPO_ROOT" - -if [[ "$BUMP" != "none" ]]; then - NEW_VERSION=$(BUMP="$BUMP" python3 - "$VERSION_FILE" <<'PY' -import os, re, sys - -path = sys.argv[1] -source = open(path).read() -current = re.search(r"VERSION = '([^']+)'", source).group(1) - -major, minor, patch = (int(part) for part in current.split('.')[:3]) -bump = os.environ['BUMP'] -if bump == 'major': - major, minor, patch = major + 1, 0, 0 -elif bump == 'minor': - minor, patch = minor + 1, 0 -else: - patch += 1 - -new = f'{major}.{minor}.{patch}' -open(path, 'w').write(re.sub(r"VERSION = '[^']+'", f"VERSION = '{new}'", source)) -print(new) -PY -) - echo "Version bumped to $NEW_VERSION (was $CURRENT_VERSION)" -else - NEW_VERSION="$CURRENT_VERSION" - echo "Publishing current version $NEW_VERSION" -fi - -# --- Build tooling in a throwaway venv, so nothing global is touched --- -BUILD_VENV="$(mktemp -d)/venv" -trap 'rm -rf "$(dirname "$BUILD_VENV")"' EXIT - -echo "" -echo "Preparing build environment..." -python3 -m venv "$BUILD_VENV" -"$BUILD_VENV/bin/pip" install --quiet --upgrade pip build twine - -# --- Test --- -echo "" -echo "Running tests..." -"$BUILD_VENV/bin/pip" install --quiet . -r requirements-test.txt -"$BUILD_VENV/bin/python" -m pytest tests/unit -q - -# --- Build --- -echo "" -echo "Building distributions..." -rm -rf "$REPO_ROOT/dist" -"$BUILD_VENV/bin/python" -m build --outdir "$REPO_ROOT/dist" -ls -1 "$REPO_ROOT/dist" - -# --- Authenticate with CodeArtifact --- -echo "" -echo "Authenticating with AWS CodeArtifact..." -aws codeartifact login \ - --tool twine \ - --domain packmatic \ - --domain-owner 038513119918 \ - --repository packmatic \ - --region eu-central-1 - -# --- Publish --- -echo "" -echo "Publishing..." -"$BUILD_VENV/bin/twine" upload --repository codeartifact "$REPO_ROOT/dist"/* - -# --- Summary --- -echo "" -echo "==========================================" -echo "Release complete!" -echo "==========================================" -echo " packmatic-api-deploy $NEW_VERSION" -echo "" -echo " pip install packmatic-api-deploy==$NEW_VERSION" -echo "" -if [[ "$BUMP" != "none" ]]; then - echo " Remember to commit the version bump in api_deploy/__init__.py" - echo "" -fi From f1d68016650d0becd4e97a8abec3f86d5da5bb97 Mon Sep 17 00:00:00 2001 From: Usman Ahmed Saeed Date: Wed, 29 Jul 2026 17:49:06 +0200 Subject: [PATCH 4/7] update versions --- .github/workflows/build.yml | 4 ++-- .github/workflows/docker.yml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d637f64..e79e3bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,9 @@ jobs: python-version: ["3.9", "3.10"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 12be951..d8b5e59 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -23,20 +23,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile @@ -46,7 +46,7 @@ jobs: - name: "Build and push (tag: latest)" if: github.ref == 'refs/heads/develop' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile From 7cebc5822025ffa323862ebf0613379d6c07af85 Mon Sep 17 00:00:00 2001 From: Usman Ahmed Saeed Date: Thu, 30 Jul 2026 10:34:28 +0200 Subject: [PATCH 5/7] use tech packmatic email --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5656d52..9884f8d 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def readme(): author='Fabian Fuelling', author_email='pypi@fabfuel.de', maintainer='Packmatic Tech', - maintainer_email='jonas.cwojdzinski@packmatic.io', + maintainer_email='tech@packmatic.io', description='Manage Amazon REST API Gateway deployments (Packmatic fork of api-deploy)', long_description=readme(), long_description_content_type='text/markdown', From c411f37f3677db3d7303420cdbffd2d3e7972ad5 Mon Sep 17 00:00:00 2001 From: Usman Ahmed Saeed Date: Thu, 30 Jul 2026 10:55:30 +0200 Subject: [PATCH 6/7] add functional test for dedup --- tests/functional/api.dedup.yml | 28 + tests/functional/test_compile.py | 14 + tests/openapi/dedup_target.yml | 856 +++++++++++++++++++++++++++++++ 3 files changed, 898 insertions(+) create mode 100644 tests/functional/api.dedup.yml create mode 100644 tests/openapi/dedup_target.yml diff --git a/tests/functional/api.dedup.yml b/tests/functional/api.dedup.yml new file mode 100644 index 0000000..1fa7e51 --- /dev/null +++ b/tests/functional/api.dedup.yml @@ -0,0 +1,28 @@ +gateway: + integrationHost: https://integration.com + connectionId: 1234567890 + removeScopes: true + removeDescriptions: true + removeExamples: true +flatten: + dedupExternalRefs: true +headers: + request: + - host + - authorization + - x-datadog-trace-id + - x-datadog-parent-id + - x-datadog-origin + - x-datadog-sampling-priority + response: + - Location +cors: + origin: 'foo.com' +static: + files: + - api.v1.yml +strict: + enabled: true + overwriteRequired: false + blocklist: + - _links diff --git a/tests/functional/test_compile.py b/tests/functional/test_compile.py index cf1b527..b071555 100644 --- a/tests/functional/test_compile.py +++ b/tests/functional/test_compile.py @@ -9,6 +9,8 @@ filename = os.path.join(dirname, 'api.yml') config = Config.from_file(filename) +dedup_config = Config.from_file(os.path.join(dirname, 'api.dedup.yml')) + @fixture def simple_source_file(): @@ -52,6 +54,12 @@ def external_ref_target_file(): return Schema.from_file(filename) +@fixture +def dedup_target_file(): + filename = os.path.join(dirname, '../openapi/dedup_target.yml') + return Schema.from_file(filename) + + @fixture def one_of_source_file(): filename = os.path.join(dirname, '../openapi/one_of_source.yml') @@ -87,6 +95,12 @@ def test_compile_external_ref(external_ref_source_file, external_ref_target_file assert processed.dump(True) == external_ref_target_file.dump(True) +def test_compile_dedup_external_refs(external_ref_source_file, dedup_target_file): + manager = ProcessManager.default(dedup_config) + processed = manager.process(external_ref_source_file) + assert processed.dump(True) == dedup_target_file.dump(True) + + def test_compile_one_of(one_of_source_file, one_of_target_file): manager = ProcessManager.default(config) processed = manager.process(one_of_source_file) diff --git a/tests/openapi/dedup_target.yml b/tests/openapi/dedup_target.yml new file mode 100644 index 0000000..98acd54 --- /dev/null +++ b/tests/openapi/dedup_target.yml @@ -0,0 +1,856 @@ +openapi: 3.0.1 +info: + description: Test API + title: Test + version: '1' + contact: + name: Fabian Fuelling + email: api@fabfuel.de +servers: +- url: https://api.fabfuel.de/test +tags: +- name: Health + description: Ping and health check endpoints +- description: Static file content + name: Static Files +- name: CORS + description: CORS configuration endpoints +paths: + /-/health: + get: + operationId: health + description: HealthJSON Health check endpoint + tags: + - Health + security: + - auth0: [] + responses: + '200': + description: Health JSON response + content: + application/json: + schema: + $ref: '#/components/schemas/Response200Health' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Response401Unauthorized' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Response403Forbidden' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Response500ServerError' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + parameters: + - name: host + in: header + required: false + schema: + type: string + - name: authorization + in: header + required: false + schema: + type: string + - name: x-datadog-trace-id + in: header + required: false + schema: + type: string + - name: x-datadog-parent-id + in: header + required: false + schema: + type: string + - name: x-datadog-origin + in: header + required: false + schema: + type: string + - name: x-datadog-sampling-priority + in: header + required: false + schema: + type: string + x-amazon-apigateway-integration: + type: http + connectionId: 1234567890 + httpMethod: GET + uri: https://integration.com/-/health + passthroughBehavior: when_no_match + connectionType: VPC_LINK + responses: + default: + statusCode: '500' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + '200': + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + '401': + statusCode: '401' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + '403': + statusCode: '403' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestParameters: + integration.request.header.User-Agent: context.identity.userAgent + integration.request.header.X-Forwarded-For: context.identity.sourceIp + integration.request.header.X-Stage: context.stage + integration.request.header.host: method.request.header.host + integration.request.header.authorization: method.request.header.authorization + integration.request.header.x-datadog-trace-id: method.request.header.x-datadog-trace-id + integration.request.header.x-datadog-parent-id: method.request.header.x-datadog-parent-id + integration.request.header.x-datadog-origin: method.request.header.x-datadog-origin + integration.request.header.x-datadog-sampling-priority: method.request.header.x-datadog-sampling-priority + options: + operationId: /-/healthCORS + description: /-/health CORS + tags: + - CORS + responses: + '200': + description: '200' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + Access-Control-Allow-Methods: + schema: + type: string + example: GET,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id + x-amazon-apigateway-integration: + responses: + default: + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' + method.response.header.Access-Control-Allow-Headers: '''authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id''' + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: when_no_match + type: mock + /-/ping: + get: + operationId: ping + description: HealthJSON Ping endpoint + tags: + - Health + responses: + '200': + description: Ping response + content: {} + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Response500ServerError' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + parameters: + - name: host + in: header + required: false + schema: + type: string + - name: authorization + in: header + required: false + schema: + type: string + - name: x-datadog-trace-id + in: header + required: false + schema: + type: string + - name: x-datadog-parent-id + in: header + required: false + schema: + type: string + - name: x-datadog-origin + in: header + required: false + schema: + type: string + - name: x-datadog-sampling-priority + in: header + required: false + schema: + type: string + x-amazon-apigateway-integration: + type: http + connectionId: 1234567890 + httpMethod: GET + uri: https://integration.com/-/ping + passthroughBehavior: when_no_match + connectionType: VPC_LINK + responses: + default: + statusCode: '500' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + '200': + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestParameters: + integration.request.header.User-Agent: context.identity.userAgent + integration.request.header.X-Forwarded-For: context.identity.sourceIp + integration.request.header.X-Stage: context.stage + integration.request.header.host: method.request.header.host + integration.request.header.authorization: method.request.header.authorization + integration.request.header.x-datadog-trace-id: method.request.header.x-datadog-trace-id + integration.request.header.x-datadog-parent-id: method.request.header.x-datadog-parent-id + integration.request.header.x-datadog-origin: method.request.header.x-datadog-origin + integration.request.header.x-datadog-sampling-priority: method.request.header.x-datadog-sampling-priority + options: + operationId: /-/pingCORS + description: /-/ping CORS + tags: + - CORS + responses: + '200': + description: '200' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + Access-Control-Allow-Methods: + schema: + type: string + example: GET,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id + x-amazon-apigateway-integration: + responses: + default: + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' + method.response.header.Access-Control-Allow-Headers: '''authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id''' + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: when_no_match + type: mock + /test: + get: + tags: + - Test + summary: Test + operationId: test + responses: + '200': + description: Test + content: + application/json: + schema: + $ref: '#/components/schemas/Test' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + parameters: + - name: host + in: header + required: false + schema: + type: string + - name: authorization + in: header + required: false + schema: + type: string + - name: x-datadog-trace-id + in: header + required: false + schema: + type: string + - name: x-datadog-parent-id + in: header + required: false + schema: + type: string + - name: x-datadog-origin + in: header + required: false + schema: + type: string + - name: x-datadog-sampling-priority + in: header + required: false + schema: + type: string + x-amazon-apigateway-integration: + type: http + connectionId: 1234567890 + httpMethod: GET + uri: https://integration.com/test + passthroughBehavior: when_no_match + connectionType: VPC_LINK + responses: + default: + statusCode: '500' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + '200': + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestParameters: + integration.request.header.User-Agent: context.identity.userAgent + integration.request.header.X-Forwarded-For: context.identity.sourceIp + integration.request.header.X-Stage: context.stage + integration.request.header.host: method.request.header.host + integration.request.header.authorization: method.request.header.authorization + integration.request.header.x-datadog-trace-id: method.request.header.x-datadog-trace-id + integration.request.header.x-datadog-parent-id: method.request.header.x-datadog-parent-id + integration.request.header.x-datadog-origin: method.request.header.x-datadog-origin + integration.request.header.x-datadog-sampling-priority: method.request.header.x-datadog-sampling-priority + options: + operationId: /testCORS + description: /test CORS + tags: + - CORS + responses: + '200': + description: '200' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + Access-Control-Allow-Methods: + schema: + type: string + example: GET,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id + x-amazon-apigateway-integration: + responses: + default: + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' + method.response.header.Access-Control-Allow-Headers: '''authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id''' + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: when_no_match + type: mock + /test-two: + get: + tags: + - Test + summary: Test 2 + operationId: test2 + responses: + '200': + description: Test 2 + content: + application/json: + schema: + $ref: '#/components/schemas/TestTwo' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + parameters: + - name: host + in: header + required: false + schema: + type: string + - name: authorization + in: header + required: false + schema: + type: string + - name: x-datadog-trace-id + in: header + required: false + schema: + type: string + - name: x-datadog-parent-id + in: header + required: false + schema: + type: string + - name: x-datadog-origin + in: header + required: false + schema: + type: string + - name: x-datadog-sampling-priority + in: header + required: false + schema: + type: string + x-amazon-apigateway-integration: + type: http + connectionId: 1234567890 + httpMethod: GET + uri: https://integration.com/test-two + passthroughBehavior: when_no_match + connectionType: VPC_LINK + responses: + default: + statusCode: '500' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + '200': + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestParameters: + integration.request.header.User-Agent: context.identity.userAgent + integration.request.header.X-Forwarded-For: context.identity.sourceIp + integration.request.header.X-Stage: context.stage + integration.request.header.host: method.request.header.host + integration.request.header.authorization: method.request.header.authorization + integration.request.header.x-datadog-trace-id: method.request.header.x-datadog-trace-id + integration.request.header.x-datadog-parent-id: method.request.header.x-datadog-parent-id + integration.request.header.x-datadog-origin: method.request.header.x-datadog-origin + integration.request.header.x-datadog-sampling-priority: method.request.header.x-datadog-sampling-priority + options: + operationId: /test-twoCORS + description: /test-two CORS + tags: + - CORS + responses: + '200': + description: '200' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + Access-Control-Allow-Methods: + schema: + type: string + example: GET,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id + x-amazon-apigateway-integration: + responses: + default: + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' + method.response.header.Access-Control-Allow-Headers: '''authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id''' + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: when_no_match + type: mock + /api.v1.yml: + get: + tags: + - Static Files + responses: + '200': + description: Static file api.v1.yml + headers: + Content-Type: + schema: + type: string + Access-Control-Expose-Headers: + schema: + type: string + example: Content-Type + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + parameters: + - name: host + in: header + required: false + schema: + type: string + - name: authorization + in: header + required: false + schema: + type: string + - name: x-datadog-trace-id + in: header + required: false + schema: + type: string + - name: x-datadog-parent-id + in: header + required: false + schema: + type: string + - name: x-datadog-origin + in: header + required: false + schema: + type: string + - name: x-datadog-sampling-priority + in: header + required: false + schema: + type: string + x-amazon-apigateway-integration: + type: http + connectionId: 1234567890 + httpMethod: GET + uri: https://integration.com/api.v1.yml + passthroughBehavior: when_no_match + connectionType: VPC_LINK + responses: + default: + statusCode: '500' + responseParameters: + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + '200': + statusCode: '200' + responseParameters: + method.response.header.Content-Type: integration.response.header.Content-Type + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + method.response.header.Access-Control-Expose-Headers: '''Content-Type''' + requestParameters: + integration.request.header.User-Agent: context.identity.userAgent + integration.request.header.X-Forwarded-For: context.identity.sourceIp + integration.request.header.X-Stage: context.stage + integration.request.header.host: method.request.header.host + integration.request.header.authorization: method.request.header.authorization + integration.request.header.x-datadog-trace-id: method.request.header.x-datadog-trace-id + integration.request.header.x-datadog-parent-id: method.request.header.x-datadog-parent-id + integration.request.header.x-datadog-origin: method.request.header.x-datadog-origin + integration.request.header.x-datadog-sampling-priority: method.request.header.x-datadog-sampling-priority + options: + operationId: /api.v1.ymlCORS + description: /api.v1.yml CORS + tags: + - CORS + responses: + '200': + description: '200' + headers: + Access-Control-Allow-Origin: + schema: + type: string + example: foo.com + Access-Control-Allow-Methods: + schema: + type: string + example: GET,OPTIONS + Access-Control-Allow-Headers: + schema: + type: string + example: authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id + x-amazon-apigateway-integration: + responses: + default: + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' + method.response.header.Access-Control-Allow-Headers: '''authorization,host,x-datadog-origin,x-datadog-parent-id,x-datadog-sampling-priority,x-datadog-trace-id''' + method.response.header.Access-Control-Allow-Origin: '''foo.com''' + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: when_no_match + type: mock +components: + schemas: + Test: + type: object + readOnly: true + required: + - id + - urn + - tenant + - createdAt + - createdBy + - modifiedAt + - modifiedBy + additionalProperties: false + properties: + id: + type: string + readOnly: true + urn: + type: string + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + tenant: + type: string + pattern: ^[a-z]{2,}(?:\.[a-z]{1}[a-z-]*[a-z]{1})*$ + readOnly: true + packmaticId: + type: string + pattern: ^[A-Z]{2}-[A-Z0-9]{5,7}$ + readOnly: true + createdAt: + type: string + format: date-time + createdBy: + type: string + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + modifiedAt: + type: string + format: date-time + nullable: true + modifiedBy: + type: string + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + nullable: true + additionalField: + type: string + TestTwo: + type: object + readOnly: true + required: + - id + - urn + - tenant + - createdAt + - createdBy + - modifiedAt + - modifiedBy + additionalProperties: false + properties: + id: + type: string + readOnly: true + urn: + type: string + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + tenant: + type: string + pattern: ^[a-z]{2,}(?:\.[a-z]{1}[a-z-]*[a-z]{1})*$ + readOnly: true + packmaticId: + type: string + pattern: ^[A-Z]{2}-[A-Z0-9]{5,7}$ + readOnly: true + createdAt: + type: string + format: date-time + createdBy: + type: string + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + modifiedAt: + type: string + format: date-time + nullable: true + modifiedBy: + type: string + pattern: ^urn:[\w-]+:[\w.-]*:[\w.-]*:[\w-]+\/[^\/]+(\/[^\/]+)*$ + nullable: true + Response200Health: + type: object + additionalProperties: false + properties: + application: + type: object + additionalProperties: false + properties: + name: + type: string + environment: + type: string + version: + type: string + required: + - name + - environment + - version + instance: + type: object + additionalProperties: false + properties: + datetime: + type: string + format: date-time + uptime: + type: string + startup: + type: string + format: date-time + hostname: + type: string + required: + - datetime + - uptime + - startup + - hostname + required: + - application + - instance + Response401Unauthorized: + type: object + required: + - statusCode + - message + additionalProperties: false + properties: + statusCode: + type: integer + message: + type: string + error: + type: string + incident: + type: string + status: + type: integer + name: + type: string + path: + type: string + errors: + type: array + items: + type: object + properties: + message: + type: string + errorCode: + type: string + path: + type: string + additionalProperties: false + required: + - message + - errorCode + - path + Response403Forbidden: + type: object + required: + - statusCode + - message + additionalProperties: false + properties: + statusCode: + type: integer + message: + type: string + error: + type: string + incident: + type: string + status: + type: integer + name: + type: string + path: + type: string + errors: + type: array + items: + type: object + properties: + message: + type: string + errorCode: + type: string + path: + type: string + additionalProperties: false + required: + - message + - errorCode + - path + Response500ServerError: + type: object + required: + - statusCode + - message + additionalProperties: false + properties: + statusCode: + type: integer + message: + type: string + error: + type: string + incident: + type: string + status: + type: integer + name: + type: string + path: + type: string + errors: + type: array + items: + type: object + properties: + message: + type: string + errorCode: + type: string + path: + type: string + additionalProperties: false + required: + - message + - errorCode + - path + securitySchemes: + supplierAuth: + type: apiKey + in: header + name: x-supplier-auth +x-amazon-apigateway-request-validators: {} +x-amazon-apigateway-minimum-compression-size: 0 From 26baa307ec3a898b4736d3cb72d0cfe89df4c782 Mon Sep 17 00:00:00 2001 From: Usman Ahmed Saeed Date: Thu, 30 Jul 2026 11:13:44 +0200 Subject: [PATCH 7/7] build amd64 only --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d8b5e59..1e8da15 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,7 +40,7 @@ jobs: with: context: . file: ./Dockerfile - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: ghcr.io/packmatic/api-deploy:${{ github.ref_name }} - @@ -50,6 +50,6 @@ jobs: with: context: . file: ./Dockerfile - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: ghcr.io/packmatic/api-deploy:latest