Skip to content
Closed
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
72 changes: 72 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: test

on:
push:
branches: [main]
pull_request:

jobs:
shell-tests:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Install yq v4 (linux)
if: runner.os == 'Linux'
run: |
if command -v yq && yq --version | grep -qE 'version v?4\.'; then exit 0; fi
sudo curl -fsSL --retry 3 --retry-delay 2 \
-o /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/download/v4.47.1/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
yq --version

- name: Install yq v4 (macos)
if: runner.os == 'macOS'
run: brew install yq && yq --version

- run: bash tests/metadata_test.sh
- run: bash tests/parse_test.sh

smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run action against fixture (buildContext present)
id: cfg
uses: ./
with:
working_directory: tests/fixtures
service_name: smoke-svc

- name: Verify outputs
run: |
set -e
test "${{ steps.cfg.outputs.config_found }}" = "true"
test "${{ steps.cfg.outputs.service_found }}" = "true"
test "${{ steps.cfg.outputs.name }}" = "smoke-svc"
test "${{ steps.cfg.outputs.path }}" = "services/smoke"
test "${{ steps.cfg.outputs.deployment_repo }}" = "KoalaOps/deployment"
test "${{ steps.cfg.outputs.deployment_repo_path }}" = "smoke"
test "${{ steps.cfg.outputs.build_context }}" = "services/smoke"
test "${{ steps.cfg.outputs.dockerfile_path }}" = "services/smoke/Dockerfile"
echo "smoke-svc outputs OK"

- name: Run action against fixture (buildContext absent -> default '.')
id: cfg2
uses: ./
with:
working_directory: tests/fixtures
service_name: smoke-default-context

- name: Verify default-context outputs
run: |
set -e
test "${{ steps.cfg2.outputs.build_context }}" = "."
test "${{ steps.cfg2.outputs.dockerfile_path }}" = "services/default/Dockerfile"
echo "default-context outputs OK"
47 changes: 31 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
# read-config

A GitHub Action that reads service configuration from `.skyhook/skyhook.yaml`.
[![test](https://github.com/KoalaOps/read-config/actions/workflows/test.yml/badge.svg)](https://github.com/KoalaOps/read-config/actions/workflows/test.yml)

A GitHub Action that reads service configuration from `.skyhook/skyhook.yaml` and exposes the matching service's fields as step outputs.

## Description

This action parses the Skyhook configuration file and extracts service-specific settings including:
- Service path
- Deployment repository configuration
- Docker build tool settings (context path, dockerfile path)
- Docker build tool settings (build context, dockerfile path)

## Usage

```yaml
- name: Read service config
id: config
uses: skyhook-io/read-config@v1
uses: KoalaOps/read-config@v1
with:
working_directory: code
service_name: my-service
Expand All @@ -23,7 +25,7 @@ This action parses the Skyhook configuration file and extracts service-specific
run: |
docker build \
-f ${{ steps.config.outputs.dockerfile_path || 'Dockerfile' }} \
${{ steps.config.outputs.context_path || '.' }}
${{ steps.config.outputs.build_context }}
```

## Inputs
Expand All @@ -42,7 +44,7 @@ This action parses the Skyhook configuration file and extracts service-specific
| `path` | Service path relative to repo root |
| `deployment_repo` | Separate deployment repository (if configured) |
| `deployment_repo_path` | Path within deployment repository |
| `context_path` | Docker build context path relative to repo root |
| `build_context` | Docker build context relative to repo root (defaults to `.` when absent in config) |
| `dockerfile_path` | Dockerfile path relative to repo root |
| `config_found` | Whether the config file was found (`true`/`false`) |
| `service_found` | Whether the service was found in config (`true`/`false`) |
Expand All @@ -59,15 +61,15 @@ services:
deploymentRepoPath: services/my-service
buildTool:
docker:
contextPath: services/my-service
buildContext: services/my-service
dockerfilePath: docker/Dockerfile.my-service

- name: another-service
path: services/another
buildTool:
docker:
contextPath: .
# dockerfilePath defaults to {contextPath}/Dockerfile if not specified
# buildContext omitted - defaults to "."
dockerfilePath: services/another/Dockerfile

environments:
- name: dev
Expand All @@ -88,29 +90,42 @@ jobs:

- name: Read service config
id: config
uses: skyhook-io/read-config@v1
uses: KoalaOps/read-config@v1
with:
working_directory: code
service_name: ${{ env.SERVICE_NAME }}

- name: Build and push Docker image
uses: skyhook-io/docker-build-push-action@v1
uses: KoalaOps/docker-build-push-action@v1
with:
# Use config values with fallbacks
context: code/${{ steps.config.outputs.context_path || env.SERVICE_DIR }}
dockerfile: code/${{ steps.config.outputs.dockerfile_path || format('{0}/Dockerfile', steps.config.outputs.context_path || env.SERVICE_DIR) }}
context: code/${{ steps.config.outputs.build_context }}
dockerfile: code/${{ steps.config.outputs.dockerfile_path || format('{0}/Dockerfile', steps.config.outputs.build_context) }}
image: ${{ inputs.image }}
```

## Fallback Behavior
## Behavior matrix

| Scenario | `config_found` | `service_found` | `build_context` | Other outputs |
|---|---|---|---|---|
| Config file missing | `false` | `false` | `""` | `""` |
| Config found, service missing | `true` | `false` | `""` | `""` |
| Service found, `buildContext` set | `true` | `true` | from config | from config |
| Service found, `buildContext` absent | `true` | `true` | `"."` | from config |
| Duplicate service names in config | n/a | n/a | n/a | action exits 1 |

If the config file doesn't exist or the service isn't found, all output values will be empty strings. This allows workflows to use fallback values:
`build_context` defaults to `"."` only when the service is found and the field is absent. When the service or config itself is missing, `build_context` is empty - the workflow should decide whether to fall back or fail loudly:

```yaml
context: ${{ steps.config.outputs.context_path || '.' }}
context: ${{ steps.config.outputs.build_context || '.' }}
dockerfile: ${{ steps.config.outputs.dockerfile_path || 'Dockerfile' }}
```

## Runner requirements

- Bash + `yq` v4.x. The action installs yq v4.47.1 if missing.
- Auto-install supports `Linux-x86_64`, `Linux-aarch64`, `Darwin-x86_64`, `Darwin-arm64`. On other platforms (Windows, BSD, etc.), pre-install yq before this step or the action will fail with a clear error.
- The auto-installer uses `curl` (preferred) or `wget`, and `sudo` if not running as root.

## License

MIT
117 changes: 117 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: 'Read Skyhook Config'
description: 'Read service build/deploy config from a Skyhook YAML file (path, dockerfile, build context, deployment repo) by service name'
author: 'KoalaOps'

inputs:
working_directory:
description: 'Path to the repository root containing .skyhook/skyhook.yaml'
required: false
default: '.'
service_name:
description: 'Name of the service to look up in the config (must be non-empty)'
required: true
config_path:
description: 'Path to the skyhook config file relative to working_directory'
required: false
default: '.skyhook/skyhook.yaml'

outputs:
# Service configuration
name:
description: 'Service name from config'
value: ${{ steps.parse.outputs.name }}
path:
description: 'Service path relative to repo root'
value: ${{ steps.parse.outputs.path }}
deployment_repo:
description: 'Separate deployment repository (if configured)'
value: ${{ steps.parse.outputs.deployment_repo }}
deployment_repo_path:
description: 'Path within deployment repository'
value: ${{ steps.parse.outputs.deployment_repo_path }}

# Build tool configuration
build_context:
description: 'Docker build context relative to repo root (defaults to "." when absent in config)'
value: ${{ steps.parse.outputs.build_context }}
dockerfile_path:
description: 'Dockerfile path relative to repo root'
value: ${{ steps.parse.outputs.dockerfile_path }}

# Status
config_found:
description: 'Whether the config file was found (true/false)'
value: ${{ steps.parse.outputs.config_found }}
service_found:
description: 'Whether the service was found in config (true/false)'
value: ${{ steps.parse.outputs.service_found }}

runs:
using: 'composite'
steps:
- name: Install yq (v4)
shell: bash
run: |
set -euo pipefail
# Accept preinstalled yq only if it is v4.x
if command -v yq &>/dev/null; then
if yq --version 2>&1 | grep -qE 'version v?4\.'; then
echo "Found preinstalled yq: $(yq --version)"
exit 0
fi
echo "::error::Found yq but not v4.x: $(yq --version 2>&1). Uninstall it or upgrade to v4."
exit 1
fi

case "$(uname -s)-$(uname -m)" in
Linux-x86_64) ASSET=yq_linux_amd64 ;;
Linux-aarch64) ASSET=yq_linux_arm64 ;;
Darwin-x86_64) ASSET=yq_darwin_amd64 ;;
Darwin-arm64) ASSET=yq_darwin_arm64 ;;
*) echo "::error::yq not found and no prebuilt binary for $(uname -s)-$(uname -m). Install yq v4 before using this action."; exit 1 ;;
esac

SUDO=""
[ "$(id -u)" -ne 0 ] && command -v sudo &>/dev/null && SUDO=sudo
DEST=/usr/local/bin/yq
URL="https://github.com/mikefarah/yq/releases/download/v4.47.1/${ASSET}"
echo "Installing yq v4.47.1 from $URL"

download() {
if command -v curl &>/dev/null; then
$SUDO curl -fsSL --retry 3 --retry-delay 2 -o "$DEST" "$URL"
elif command -v wget &>/dev/null; then
$SUDO wget -qO "$DEST" "$URL"
else
echo "::error::Neither curl nor wget is available; cannot install yq."
return 2
fi
}

for attempt in 1 2 3; do
if download; then
break
fi
rc=$?
if [ $rc -eq 2 ] || [ $attempt -eq 3 ]; then
echo "::error::yq download failed (attempt $attempt/3, exit $rc)"
exit 1
fi
sleep $((attempt * 2))
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry loop never captures download's real exit code

Medium Severity

After if download; then break; fi, $? is always 0 per POSIX (no else clause was executed), so rc=$? on line 95 never captures the actual exit code from download. This makes the $rc -eq 2 check on line 96 dead code — when neither curl nor wget is available, the loop will pointlessly retry three times (sleeping 6 seconds total) instead of failing immediately. The error message also always reports "exit 0" instead of the real exit code.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3281755. Configure here.


$SUDO chmod +x "$DEST"
yq --version

- name: Parse skyhook config
id: parse
shell: bash
env:
WORKING_DIR: ${{ inputs.working_directory }}
SERVICE_NAME: ${{ inputs.service_name }}
CONFIG_PATH: ${{ inputs.config_path }}
run: bash "$GITHUB_ACTION_PATH/scripts/parse.sh"

branding:
icon: 'file-text'
color: 'blue'
Loading
Loading