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
75 changes: 75 additions & 0 deletions openapi/upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!-- Generated by https://github.com/reakaleek/gh-action-readme -->
# <!--name-->OpenAPI upload<!--/name-->
<!--description-->
Uploads an already-linted, already-bundled OpenAPI spec to S3 via OIDC. Does not lint or bundle — the consumer workflow must do both first.
<!--/description-->

## Inputs
<!--inputs-->
| Name | Description | Required | Default |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|------------------------------|
| `spec-path` | Path to the bundled OpenAPI spec, relative to the repo root (.yaml, .yml, or .json) | `true` | ` ` |
| `spec-name` | Filename stem for the uploaded object. Repos publishing more than one spec set this per upload (e.g. "kibana", "kibana-serverless").<br> | `false` | `openapi` |
| `s3-bucket` | Target S3 bucket. Only override if a different bucket has been provisioned. | `false` | `elastic-docs-openapi-specs` |
| `s3-prefix` | Key prefix under the bucket. Defaults to "<org>/<repo>", which is also the prefix the OIDC role is scoped to write — only override within that same repo prefix, or the upload will be denied.<br> | `false` | `${{ github.repository }}` |
| `version` | Version segment of the key. Defaults to the triggering branch name. Override for repos whose branch names do not match the RFC convention (e.g. elastic/cloud, whose default branch is "master" and whose release branches are named "ECE-4.1").<br> | `false` | `${{ github.ref_name }}` |
| `aws-region` | The AWS region to use | `false` | `us-east-1` |
| `aws-account-id` | The AWS account ID. Only override if OIDC trust and IAM roles have been provisioned for the target account. | `false` | `197730964718` |
<!--/inputs-->

## Outputs
<!--outputs-->
| Name | Description |
|------|-------------|
<!--/outputs-->

## Usage
<!--usage action="your/action" version="v1"-->
<!--/usage-->

A consumer wires this action into two workflows: a PR check (`openapi/lint`, no secrets, works for forks) and a publish job that runs on merge to a publishable branch (`main` or a `<major>.<minor>` release branch). Only the publish job needs `id-token: write` and calls this action, after bundling and linting the spec itself.

**Publish on merge** (`.github/workflows/openapi-publish.yml`):

```yaml
on:
push:
branches:
- main
- '[0-9]+.[0-9]+' # <major>.<minor> only
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # required for OIDC role assumption
contents: read
steps:
- uses: actions/checkout@v6
- run: npx @redocly/cli bundle openapi/openapi.yaml -o openapi.yaml
- uses: elastic/docs-actions/openapi/lint@v1
with:
spec-path: openapi.yaml
- uses: elastic/docs-actions/openapi/upload@v1
with:
spec-path: openapi.yaml
# uploads to elastic/<repo>/<branch>/openapi.yaml
```

**Multiple specs** (e.g. Kibana's stateful and serverless specs): add a separate publish job per spec, setting `spec-name` per job:

```yaml
- uses: elastic/docs-actions/openapi/upload@v1
with:
spec-path: kibana.yaml
spec-name: kibana
# uploads to elastic/kibana/<branch>/kibana.yaml
```

**Non-standard branch names**: repos like `elastic/cloud` (default branch `master`, release branches named `ECE-4.1`) must set `version` explicitly:

```yaml
- uses: elastic/docs-actions/openapi/upload@v1
with:
spec-path: openapi.yaml
version: ${{ github.ref_name == 'master' && 'main' || github.ref_name }}
```
119 changes: 119 additions & 0 deletions openapi/upload/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: OpenAPI upload
description: >
Uploads an already-linted, already-bundled OpenAPI spec to S3 via OIDC.
Does not lint or bundle — the consumer workflow must do both first.

inputs:
spec-path:
description: 'Path to the bundled OpenAPI spec, relative to the repo root (.yaml, .yml, or .json)'
required: true
spec-name:
description: >
Filename stem for the uploaded object. Repos publishing more than one
spec set this per upload (e.g. "kibana", "kibana-serverless").
default: 'openapi'
s3-bucket:
description: 'Target S3 bucket. Only override if a different bucket has been provisioned.'
default: 'elastic-docs-openapi-specs'
s3-prefix:
description: >
Key prefix under the bucket. Defaults to "<org>/<repo>", which is also
the prefix the OIDC role is scoped to write — only override within that
same repo prefix, or the upload will be denied.
default: '${{ github.repository }}'
version:
description: >
Version segment of the key. Defaults to the triggering branch name.
Override for repos whose branch names do not match the RFC convention
(e.g. elastic/cloud, whose default branch is "master" and whose release
branches are named "ECE-4.1").
default: '${{ github.ref_name }}'
aws-region:
description: 'The AWS region to use'
default: 'us-east-1'
aws-account-id:
description: 'The AWS account ID. Only override if OIDC trust and IAM roles have been provisioned for the target account.'
default: '197730964718'

runs:
using: composite
steps:
- name: Validate inputs
shell: bash
env:
SPEC_PATH: ${{ inputs.spec-path }}
SPEC_NAME: ${{ inputs.spec-name }}
S3_PREFIX: ${{ inputs.s3-prefix }}
VERSION: ${{ inputs.version }}
run: |
validate_path() {
Comment thread
cotti marked this conversation as resolved.
local value="$1" name="$2"
[ -z "$value" ] && return
if [[ "$value" == /* ]]; then
echo "::error::${name} must be a relative path: ${value}"; exit 1
fi
if [[ "$value" == *..* ]]; then
echo "::error::${name} must not contain '..': ${value}"; exit 1
fi
if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
echo "::error::${name} must not contain newlines"; exit 1
fi
}

validate_segment() {
local value="$1" name="$2"
if [[ ! "$value" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "::error::${name} must match [a-zA-Z0-9._-]+: ${value}"; exit 1
fi
}

validate_path "$SPEC_PATH" "spec-path"
validate_path "$S3_PREFIX" "s3-prefix"
validate_segment "$SPEC_NAME" "spec-name"
validate_segment "$VERSION" "version"

case "$SPEC_PATH" in
*.yaml|*.yml) content_type=application/yaml ;;
*.json) content_type=application/json ;;
*)
echo "::error::spec-path must end in .yaml, .yml, or .json: ${SPEC_PATH}"
exit 1
;;
esac
ext="${SPEC_PATH##*.}"

if [[ -L "$SPEC_PATH" ]]; then
echo "::error::spec-path must not be a symlink: ${SPEC_PATH}"
exit 1
fi

if [[ ! -f "$SPEC_PATH" ]]; then
echo "::error::spec-path does not exist: ${SPEC_PATH}"
exit 1
fi

echo "ext=${ext}" >> "$GITHUB_OUTPUT"
echo "content-type=${content_type}" >> "$GITHUB_OUTPUT"
id: resolve

- name: Authenticate with AWS
uses: elastic/docs-actions/aws/auth@v1
with:
aws_account_id: ${{ inputs.aws-account-id }}
aws_region: ${{ inputs.aws-region }}
aws_role_name_prefix: elastic-docs-openapi-

- name: Upload spec to S3
shell: bash
env:
SPEC_PATH: ${{ inputs.spec-path }}
SPEC_NAME: ${{ inputs.spec-name }}
S3_BUCKET: ${{ inputs.s3-bucket }}
S3_PREFIX: ${{ inputs.s3-prefix }}
VERSION: ${{ inputs.version }}
EXT: ${{ steps.resolve.outputs.ext }}
CONTENT_TYPE: ${{ steps.resolve.outputs.content-type }}
run: |
aws s3 cp --checksum-algorithm SHA256 --content-type "$CONTENT_TYPE" \
"$SPEC_PATH" \
"s3://${S3_BUCKET}/${S3_PREFIX}/${VERSION}/${SPEC_NAME}.${EXT}"
Loading