Skip to content
Open
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
104 changes: 104 additions & 0 deletions .github/actions/yaml.yamlfmt/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: YAML - yamlfmt
description: Check that YAML files are formatted with `yamlfmt`, without modifying them. On failure, the diff is added to the job summary. Requires the `yamlfmt` CLI on the `PATH`.


inputs:
target_dir:
required: false
default: ./
description: "(Optional) The directory to check recursively for `.yaml` and `.yml` files. Uses standard path matching and honors the config's exclude rules. Defaults to `./`."
config_file:
required: false
description: "(Optional) The path to the `yamlfmt` config file, relative to the repository root. When omitted, `yamlfmt` discovers its config as usual."
annotations_enabled:
required: false
default: "true"
description: "(Optional) Whether to annotate unformatted files in the pull request when `yamlfmt` fails. Defaults to `true`."

outputs:
skipped:
value: ${{ steps.target.outputs.skipped }}
description: "`true` when `target_dir` is missing or contains no `.yaml` or `.yml` files and `yamlfmt` was not run. Empty otherwise."
stdout:
value: ${{ steps.fmt.outputs.stdout }}
description: "The STDOUT stream of the call to `yamlfmt`. Empty when skipped."
stderr:
value: ${{ steps.fmt.outputs.stderr }}
description: "The STDERR stream of the call to `yamlfmt`. Empty when skipped."
exitcode:
value: ${{ steps.fmt.outputs.exitcode }}
description: "The exit code of the call to `yamlfmt`. Empty when skipped. The action still fails on a non-zero exit code, so use `continue-on-error: true` to inspect it."


runs:
using: composite

steps:
- name: Resolve Target
id: target
shell: bash
env:
TARGET_DIR: ${{ inputs.target_dir }}
run: |
target_dir="${TARGET_DIR%/}"
target_dir="${target_dir:-.}"
echo "target_dir=$target_dir" >> "$GITHUB_OUTPUT"

files=""
if [ -d "$target_dir" ]; then
files=$(find "$target_dir" -type f \( -name '*.yaml' -o -name '*.yml' \) -print -quit)
fi
if [ -z "$files" ]; then
echo "::notice::Skipping yamlfmt: no YAML files in $target_dir."
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi

yamlfmt -version

- name: Check YAML Format
id: fmt
if: steps.target.outputs.skipped != 'true'
uses: tedilabs/github-actions/.github/actions/shell.run@main
env:
TARGET_DIR: ${{ steps.target.outputs.target_dir }}
CONFIG_FILE: ${{ inputs.config_file }}
with:
run: |
args=(-lint -output_format default -match_type standard -extensions "yaml,yml")
if [ -n "$CONFIG_FILE" ]; then
args+=(-conf "$CONFIG_FILE")
fi

yamlfmt "${args[@]}" -- "$TARGET_DIR"

# The line output names each unformatted file but provides no line numbers.
- name: Annotate Findings
id: fmt-annotate
if: always() && inputs.annotations_enabled == 'true' && steps.fmt.outcome == 'failure'
shell: bash
env:
TARGET_DIR: ${{ steps.target.outputs.target_dir }}
CONFIG_FILE: ${{ inputs.config_file }}
run: |
args=(-dry -output_format line -match_type standard -extensions "yaml,yml")
if [ -n "$CONFIG_FILE" ]; then
args+=(-conf "$CONFIG_FILE")
fi

yamlfmt "${args[@]}" -- "$TARGET_DIR" 2>/dev/null | awk '
/: formatting difference found$/ {
sub(/: formatting difference found$/, "")
gsub(/%/, "%25"); gsub(/\r/, "%0D"); gsub(/:/, "%3A"); gsub(/,/, "%2C")
printf "::error file=%s,title=yamlfmt · Not formatted::Run `yamlfmt` to format this file.\n", $0
}
' || true

- name: Add Failure Details to Job Summary
id: fmt-summary
if: always() && steps.fmt.outcome == 'failure'
uses: tedilabs/github-actions/.github/actions/github.step-summary@main
with:
title: "❌ yamlfmt · ${{ steps.target.outputs.target_dir }}"
file: ${{ steps.fmt.outputs.log_file }}
lang: diff
92 changes: 92 additions & 0 deletions .github/actions/yaml.yamllint/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: YAML - yamllint
description: Lint YAML files with `yamllint`. On failure, the findings are added to the job summary. Requires the `yamllint` CLI on the `PATH`.


inputs:
target_dir:
required: false
default: ./
description: "(Optional) The directory to lint recursively. Defaults to `./`."
config_file:
required: false
description: "(Optional) The path to the `yamllint` config file, relative to the repository root. When omitted, `yamllint` discovers its config as usual."
annotations_enabled:
required: false
default: "true"
description: "(Optional) Whether to annotate findings in the pull request using the GitHub output format. Defaults to `true`."

outputs:
skipped:
value: ${{ steps.target.outputs.skipped }}
description: "`true` when `target_dir` is missing or contains no files selected by `yamllint`, including its ignore rules. Empty otherwise."
stdout:
value: ${{ steps.lint.outputs.stdout }}
description: "The STDOUT stream of the call to `yamllint`. Empty when skipped."
stderr:
value: ${{ steps.lint.outputs.stderr }}
description: "The STDERR stream of the call to `yamllint`. Empty when skipped."
exitcode:
value: ${{ steps.lint.outputs.exitcode }}
description: "The exit code of the call to `yamllint`. Empty when skipped. The action still fails on a non-zero exit code, so use `continue-on-error: true` to inspect it."


runs:
using: composite

steps:
- name: Resolve Target
id: target
shell: bash
env:
TARGET_DIR: ${{ inputs.target_dir }}
CONFIG_FILE: ${{ inputs.config_file }}
run: |
target_dir="${TARGET_DIR%/}"
target_dir="${target_dir:-.}"
echo "target_dir=$target_dir" >> "$GITHUB_OUTPUT"

args=()
if [ -n "$CONFIG_FILE" ]; then
args+=(--config-file "$CONFIG_FILE")
fi

files=""
if [ -d "$target_dir" ]; then
files=$(yamllint "${args[@]}" --list-files -- "$target_dir")
fi
if [ -z "$files" ]; then
echo "::notice::Skipping yamllint: no YAML files in $target_dir."
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi

yamllint --version

- name: Lint YAML Files
id: lint
if: steps.target.outputs.skipped != 'true'
uses: tedilabs/github-actions/.github/actions/shell.run@main
env:
TARGET_DIR: ${{ steps.target.outputs.target_dir }}
CONFIG_FILE: ${{ inputs.config_file }}
ANNOTATIONS_ENABLED: ${{ inputs.annotations_enabled }}
with:
run: |
args=(--format standard)
if [ "$ANNOTATIONS_ENABLED" = "true" ]; then
args=(--format github)
fi
if [ -n "$CONFIG_FILE" ]; then
args+=(--config-file "$CONFIG_FILE")
fi

yamllint "${args[@]}" -- "$TARGET_DIR"

- name: Add Failure Details to Job Summary
id: lint-summary
if: always() && steps.lint.outcome == 'failure'
uses: tedilabs/github-actions/.github/actions/github.step-summary@main
with:
title: "❌ yamllint · ${{ steps.target.outputs.target_dir }}"
file: ${{ steps.lint.outputs.log_file }}
lang: text
155 changes: 130 additions & 25 deletions .github/workflows/yaml.integration.yaml
Original file line number Diff line number Diff line change
@@ -1,35 +1,140 @@
name: Integration - YAML
name: YAML - Integration


on:
push:
branches:
- main
paths:
- "**.yaml"
- "**.yml"
workflow_call:
inputs:
runs_on:
description: >
JSON-encoded runs-on value.
Examples:
- '"ubuntu-latest"'
- '["self-hosted","linux","x64"]'
required: false
type: string
default: '"ubuntu-latest"'

pull_request:
paths:
- "**.yaml"
- "**.yml"
yamllint_version:
type: string
required: false
default: latest
description: "(Optional) The version of `yamllint` to install with mise. Defaults to `latest`."
yamlfmt_version:
type: string
required: false
default: latest
description: "(Optional) The version of `yamlfmt` to install with mise. Defaults to `latest`."

workflow_dispatch: {}
yamllint_enabled:
type: boolean
required: false
default: true
description: "(Optional) Whether to lint YAML files with `yamllint`. Defaults to `true`."
yamllint_config_file:
type: string
required: false
description: "(Optional) The path to the `yamllint` config file, relative to the repository root. When omitted, `yamllint` discovers its config as usual."
yamllint_target_dir:
type: string
required: false
default: ./
description: "(Optional) The directory to lint recursively with `yamllint`. Defaults to `./`."
yamlfmt_enabled:
type: boolean
required: false
default: true
description: "(Optional) Whether to check YAML formatting with `yamlfmt`, without modifying files. Defaults to `true`."
yamlfmt_config_file:
type: string
required: false
description: "(Optional) The path to the `yamlfmt` config file, relative to the repository root. When omitted, `yamlfmt` discovers its config as usual."
yamlfmt_target_dir:
type: string
required: false
default: ./
description: "(Optional) The directory to check recursively with `yamlfmt`. Defaults to `./`."


concurrency:
group: integration-yaml-${{ github.ref }}
cancel-in-progress: true
pr_comment_enabled:
type: boolean
required: false
default: true
description: "(Optional) Whether to post the integration report as a single sticky comment on pull requests. Requires the `pull-requests: write` permission on the caller. The report is always written to the job summary. Defaults to `true`."


jobs:
lint:
name: Lint (yamllint)
uses: tedilabs/github-actions/.github/workflows/yaml.yamllint.yaml@main

with:
yamllint_version: latest
yamllint_config_file: .yamllint.yaml
yamllint_target_dir: ./
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
name: Lint
runs-on: ${{ fromJson(inputs.runs_on) }}

steps:
- name: Checkout
id: checkout
uses: actions/checkout@v7

- name: Set up tools
id: setup-tools
if: inputs.yamllint_enabled || inputs.yamlfmt_enabled
uses: tedilabs/github-actions/.github/actions/mise.setup-tools@main
with:
mise_toml: |
[tools]
"pipx:yamllint" = "${{ inputs.yamllint_version }}"
yamlfmt = "${{ inputs.yamlfmt_version }}"
install_args: >-
${{ inputs.yamllint_enabled && 'pipx:yamllint' || '' }}
${{ inputs.yamlfmt_enabled && 'yamlfmt' || '' }}

- name: Lint (yamllint)
id: yamllint
if: inputs.yamllint_enabled
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/yaml.yamllint@main
with:
config_file: ${{ inputs.yamllint_config_file }}
target_dir: ${{ inputs.yamllint_target_dir }}

- name: Check Format (yamlfmt)
id: yamlfmt
if: inputs.yamlfmt_enabled
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/yaml.yamlfmt@main
with:
config_file: ${{ inputs.yamlfmt_config_file }}
target_dir: ${{ inputs.yamlfmt_target_dir }}

- name: Collect Results
id: results
if: always()
uses: tedilabs/github-actions/.github/actions/github.matrix-report@main
with:
mode: collect
id: ${{ github.repository }}
id_label: Repository
artifact_prefix: yaml-integration
job_status: ${{ job.status }}
# Disabled checks are left out of the report; checks with no target files report `skipped`.
results: |
{
"yamllint": "${{ inputs.yamllint_enabled && (steps.yamllint.outputs.skipped == 'true' && 'skipped' || steps.yamllint.outcome) || '' }}",
"yamlfmt": "${{ inputs.yamlfmt_enabled && (steps.yamlfmt.outputs.skipped == 'true' && 'skipped' || steps.yamlfmt.outcome) || '' }}"
}


report:
name: Report
needs:
- lint
if: always()
runs-on: ${{ fromJson(inputs.runs_on) }}

steps:
- name: Publish Report
id: report
uses: tedilabs/github-actions/.github/actions/github.matrix-report@main
with:
mode: publish
id_label: Repository
artifact_prefix: yaml-integration
title: YAML Integration
pr_comment_enabled: ${{ inputs.pr_comment_enabled }}
pr_comment_marker: yaml-integration
32 changes: 32 additions & 0 deletions .github/workflows/yaml.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Integration - YAML


on:
push:
branches:
- main
paths:
- "**.yaml"
- "**.yml"

pull_request:
paths:
- "**.yaml"
- "**.yml"

workflow_dispatch: {}


concurrency:
group: integration-yaml-${{ github.ref }}
cancel-in-progress: true


jobs:
integration:
name: Integration
uses: tedilabs/github-actions/.github/workflows/yaml.integration.yaml@main

permissions:
contents: read
pull-requests: write
Loading
Loading