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
58 changes: 47 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,11 @@ on:

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

- name: Install yq v4 (linux)
if: runner.os == 'Linux'
- name: Install yq v4
run: |
if command -v yq && yq --version | grep -qE 'version v?4\.'; then exit 0; fi
sudo curl -fsSL --retry 3 --retry-delay 2 \
Expand All @@ -25,10 +20,6 @@ jobs:
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

Expand Down Expand Up @@ -93,3 +84,48 @@ jobs:
test "${{ steps.cfg3.outputs.build_context }}" = "."
test "${{ steps.cfg3.outputs.dockerfile_path }}" = "Dockerfile"
echo "missing-service fallback outputs OK"

- name: Run action with include_env=true (environments present)
id: cfg4
uses: ./
with:
working_directory: tests/fixtures
service_name: smoke-svc
default_build_context: '.'
default_dockerfile_path: Dockerfile
include_env: 'true'

- name: Verify environments output is populated
# IMPORTANT: env-passthrough (not direct ${{ }} in run:). The
# environments YAML can contain quotes / $ / backticks that the
# shell would otherwise re-interpret. This step doubles as a
# contract test for the README's recommended usage pattern.
env:
ENVS: ${{ steps.cfg4.outputs.environments }}
run: |
set -e
test -n "$ENVS"
printf '%s\n' "$ENVS" > /tmp/envs.yaml
yq '.[].name' /tmp/envs.yaml | grep -q '^dev$'
yq '.[].name' /tmp/envs.yaml | grep -q '^prod$'
echo "include_env=true outputs OK"

- name: Run action with include_env=true against fixture without environments
id: cfg5
continue-on-error: true
uses: ./
with:
working_directory: tests/fixtures-no-env
service_name: smoke-svc
default_build_context: '.'
default_dockerfile_path: Dockerfile
include_env: 'true'

- name: Verify include_env=true with no env block fails 'Not supported yet'
run: |
set -e
if [ "${{ steps.cfg5.outcome }}" != "failure" ]; then
echo "FAIL: include_env=true with no env block should have failed (outcome=${{ steps.cfg5.outcome }})"
exit 1
fi
echo "include_env=true 'Not supported yet' contract OK"
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ This action parses the Skyhook configuration file and extracts service-specific
| `config_path` | Path to the skyhook config file relative to working_directory | No | `.skyhook/skyhook.yaml` |
| `default_build_context` | Value emitted as `build_context` whenever it would otherwise be empty: YAML field absent, config file missing, or service missing. The action does not pick a value for you - callers must pass a non-empty default, otherwise the action fails loudly. | Yes | - |
| `default_dockerfile_path` | Value emitted as `dockerfile_path` whenever it would otherwise be empty (same triggers as `default_build_context`). Must be non-empty. | Yes | - |
| `include_env` | When `true`, also emit the `environments` block from the local skyhook.yaml as the `environments` output. When `true` and the local config has no `environments` block (key absent, or any YAML null spelling: `null` / `Null` / `NULL` / `~` / bare `environments:`), the action exits 1 with `Not supported yet`. When `true` and the config file itself is missing, the action also exits 1 with `Not supported yet`. Both cases are reserved for a future external-repository fetch path. | No | `false` |
| `git_token` | Reserved for a future release: token used to fetch the `environments` block from an external repository when `include_env=true` and the block is not present locally. Currently unused - the action fails `Not supported yet` in that scenario regardless of this input. | No | `''` |

## Outputs

Expand All @@ -52,6 +54,7 @@ This action parses the Skyhook configuration file and extracts service-specific
| `dockerfile_path` | Dockerfile path relative to repo root. Falls back to `default_dockerfile_path` when absent/empty in config or when config/service is not found. |
| `config_found` | Whether the config file was found (`true`/`false`) |
| `service_found` | Whether the service was found in config (`true`/`false`) |
| `environments` | YAML block of environments from the local skyhook.yaml when `include_env=true` and the block is present. Empty when `include_env=false`. When `include_env=true` and the block is absent, the action exits 1 instead of emitting this output. |

> When a default is applied because a YAML field was absent, the action emits a `::notice::` line. When the entire config file or service is missing, the action emits a `::warning::` (more prominent in the run UI) — so the source of every emitted value is visible at a glance.

Expand Down Expand Up @@ -111,6 +114,65 @@ jobs:
image: ${{ inputs.image }}
```

## Example: include the environments block

```yaml
- name: Read service config + environments
id: config
uses: skyhook-io/read-config@v1
with:
working_directory: code
service_name: my-service
default_build_context: '.'
default_dockerfile_path: Dockerfile
include_env: 'true'

- name: Use environments
env:
# IMPORTANT: pass via env, not via direct expression interpolation. The
# environments YAML can contain double quotes, `$`, backticks, etc., which
# would be re-interpreted by the shell if inlined into the `run:` script.
ENVS: ${{ steps.config.outputs.environments }}
run: |
printf '%s\n' "$ENVS" > /tmp/envs.yaml
yq '.[].name' /tmp/envs.yaml
```

For a `skyhook.yaml` containing:

```yaml
environments:
- name: autopush
clusterName: nonprod-cluster-us-east1
cloudProvider: gcp
account: koalabackend
location: us-east1-b
namespace: autopush
- name: dev
clusterName: nonprod-cluster-us-east1
cloudProvider: gcp
account: koalabackend
location: us-east1-b
namespace: dev
- name: prod
clusterName: prod-cluster-us-east1
cloudProvider: gcp
account: koalabackend
location: us-east1-b
namespace: prod
- name: ephemeral
clusterName: ""
namespace: ephemeral
```

the `environments` output contains the YAML list above (without the top-level `environments:` key).

If `include_env=true` and the local `skyhook.yaml` has no `environments:` block (key absent, or any YAML null spelling: `null` / `Null` / `NULL` / `~` / bare `environments:`) the action exits 1 with `Not supported yet`. Same exit if the config file itself is missing. An explicit empty list (`environments: []`) is passed through as-is - the action returns whatever the file declares.

`environments` is orthogonal to service lookup: when `include_env=true` and the block is present, the output is populated even if the named service is missing. (The service-missing `::warning::` still fires for the per-service outputs.)

`git_token` is declared so callers can wire it in advance, but the external-repository code path is not yet implemented.

## Behavior matrix

Let `BC` = `default_build_context` input, `DF` = `default_dockerfile_path` input.
Expand All @@ -127,6 +189,11 @@ Let `BC` = `default_build_context` input, `DF` = `default_dockerfile_path` input
| Config file missing, both defaults non-empty | `false` | `false` | `""` | `BC` | `DF` |
| Any of the above where the relevant default is empty | n/a | n/a | n/a | n/a | **action exits 1** |
| Duplicate service names in config | n/a | n/a | n/a | n/a | action exits 1 |
| `include_env=true`, `environments` present in local config | unchanged | unchanged | unchanged | unchanged | unchanged - and `environments` output is populated |
| `include_env=true`, `environments: []` (explicit empty list) | unchanged | unchanged | unchanged | unchanged | unchanged - `environments` output is the literal `[]` (pass-through) |
| `include_env=true`, no `environments` block (key absent, `null`, `~`, `Null`, `NULL`, or bare) | n/a | n/a | n/a | n/a | **action exits 1 - `Not supported yet`** |
| `include_env=true`, local config file missing | n/a | n/a | n/a | n/a | **action exits 1 - `Not supported yet`** |
| `include_env=false` (default) | unchanged | unchanged | unchanged | unchanged | `environments` output is empty |

The action **always** emits a non-empty `build_context` and `dockerfile_path` on success, so the consuming workflow can drop `||` fallbacks:

Expand Down
28 changes: 28 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ inputs:
Value emitted as `dockerfile_path` whenever it would otherwise be empty (same triggers as
`default_build_context`). Must be non-empty.
required: true
include_env:
description: |
When `true`, also emit the `environments` block from the local skyhook.yaml as the
`environments` output. If `true` and the local config has no `environments` block (key absent,
or any YAML null spelling: `null` / `Null` / `NULL` / `~` / bare `environments:`), the action
exits 1 with "Not supported yet". Same exit if the config file itself is missing. An explicit
empty list (`environments: []`) is passed through as-is. Sourcing environments from an external
repository is reserved for a future release. Default `false`.
required: false
default: 'false'
git_token:
description: |
Reserved for a future release: token used to fetch the `environments` block from an external
repository when `include_env=true` and the block is not present locally. Currently unused -
the action fails with "Not supported yet" in that scenario regardless of this input.
required: false
default: ''

outputs:
# Service configuration
Expand Down Expand Up @@ -57,6 +74,15 @@ outputs:
service_found:
description: 'Whether the service was found in config (true/false)'
value: ${{ steps.parse.outputs.service_found }}
environments:
description: |
YAML-formatted block of environments from the local skyhook.yaml when `include_env=true` and
the block is present (multi-line, passed back verbatim - including the `[]` literal for an
explicit empty list). Empty string when `include_env=false` (the default). When
`include_env=true` and the block is absent, the action exits 1 ("Not supported yet") instead
of emitting this output. Consume via env-passthrough (see README) to preserve quotes / $ /
backticks across shell expansion.
value: ${{ steps.parse.outputs.environments }}

runs:
using: 'composite'
Expand Down Expand Up @@ -124,6 +150,8 @@ runs:
CONFIG_PATH: ${{ inputs.config_path }}
DEFAULT_BUILD_CONTEXT: ${{ inputs.default_build_context }}
DEFAULT_DOCKERFILE_PATH: ${{ inputs.default_dockerfile_path }}
INCLUDE_ENV: ${{ inputs.include_env }}
GIT_TOKEN: ${{ inputs.git_token }}
run: bash "$GITHUB_ACTION_PATH/scripts/parse.sh"

branding:
Expand Down
59 changes: 59 additions & 0 deletions scripts/parse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
# config/service is not found. The action does not pick a default
# for the caller; this is intentional so callers must opt in.
# DEFAULT_DOCKERFILE_PATH - REQUIRED. Same contract as DEFAULT_BUILD_CONTEXT, for dockerfile_path.
# INCLUDE_ENV - "true" to additionally emit the `environments` block from the local
# config as the `environments` output. Anything else (including unset)
# is treated as false. When "true" and the local config has no
# `environments` block (or the file is missing), the script exits 1
# with "Not supported yet" - external-repo support is not implemented.
# GIT_TOKEN - Reserved for the future external-repo path. Currently unused.
# GITHUB_OUTPUT - file to append outputs to (required by GitHub Actions; tests pass a tempfile)
#
# Exits non-zero with ::error:: on:
Expand All @@ -34,6 +40,7 @@ WORKING_DIR="${WORKING_DIR:-.}"
WORKING_DIR="${WORKING_DIR%/}"
CONFIG_PATH="${CONFIG_PATH:-.skyhook/skyhook.yaml}"
SERVICE_NAME="${SERVICE_NAME:-}"
INCLUDE_ENV="${INCLUDE_ENV:-false}"

if [ -z "$SERVICE_NAME" ]; then
echo "::error::service_name input is required and must be non-empty"
Expand Down Expand Up @@ -97,14 +104,62 @@ require_nonempty_defaults_for_unmatched() {
fi
}

# Emit (or refuse to emit) the `environments` output. Safe to call from any
# exit path that has confirmed CONFIG_FILE exists, OR from the empty-on-disable
# case (INCLUDE_ENV != "true").
#
# Contract:
# - INCLUDE_ENV != "true" -> emit empty string (the default; backwards-compatible).
# - INCLUDE_ENV == "true" + `environments` block present -> emit it as YAML (heredoc form
# preserves multi-line structure across $GITHUB_OUTPUT).
# - INCLUDE_ENV == "true" + `environments` block absent -> exit 1 "Not supported yet"
# (external-repo lookup via GIT_TOKEN is reserved for a future release).
emit_environments() {
if [ "$INCLUDE_ENV" != "true" ]; then
write_output environments ""
echo " environments: skipped (include_env=${INCLUDE_ENV})"
return 0
fi
# Detect "no environments" structurally via yq's tag, not by string-matching
# the value: YAML has four null spellings (null / Null / NULL / ~), plus a
# bare key with no value (also null), plus an outright-missing key. yq's
# tag is `!!null` in all of those cases; anything else means the key was
# explicitly set to a value (including `[]`, which passes through verbatim
# per the user spec - if `environments` is in the yaml, return it).
local env_tag environments
if ! env_tag=$(yq e '.environments | tag' "$CONFIG_FILE"); then
echo "::error::Failed to parse $CONFIG_FILE while reading 'environments'"
exit 1
fi
if [ "$env_tag" = "!!null" ]; then
echo "::error::include_env=true but no 'environments' block in '${CONFIG_FILE}' - Not supported yet (external-repo environments source is not implemented; git_token is reserved for that future path)"
exit 1
fi
if ! environments=$(yq e '.environments' "$CONFIG_FILE"); then
echo "::error::Failed to parse $CONFIG_FILE while reading 'environments'"
exit 1
fi
write_output environments "$environments"
echo " environments: emitted (include_env=true, tag=${env_tag})"
}

# Config file missing
if [ ! -f "$CONFIG_FILE" ]; then
# include_env=true means the caller wants the environments block from the local file.
# No local file -> there's nothing to return, and the external-repo path is not built yet.
if [ "$INCLUDE_ENV" = "true" ]; then
echo "::error::include_env=true but the local config file '${CONFIG_FILE}' is missing - Not supported yet (external-repo environments source is not implemented)"
exit 1
fi
echo "Config file not found: $CONFIG_FILE"
require_nonempty_defaults_for_unmatched "Config file not found at '${CONFIG_FILE}'"
echo "::warning::Config file not found - emitting build_context='${DEFAULT_BUILD_CONTEXT}' and dockerfile_path='${DEFAULT_DOCKERFILE_PATH}' from default_build_context / default_dockerfile_path inputs"
write_output config_found false
write_output service_found false
write_unmatched_outputs
# Safe: INCLUDE_ENV is guaranteed != "true" here (fail-fast above caught the true case),
# so emit_environments will just write an empty string.
emit_environments
exit 0
fi

Expand All @@ -127,6 +182,7 @@ if [ -z "$SERVICE_INDEX" ]; then
echo "::warning::Service '${SERVICE_NAME}' not found - emitting build_context='${DEFAULT_BUILD_CONTEXT}' and dockerfile_path='${DEFAULT_DOCKERFILE_PATH}' from default_build_context / default_dockerfile_path inputs"
write_output service_found false
write_unmatched_outputs
emit_environments
exit 0
fi

Expand Down Expand Up @@ -185,6 +241,7 @@ write_output deployment_repo "$DEPLOYMENT_REPO"
write_output deployment_repo_path "$DEPLOYMENT_REPO_PATH"
write_output build_context "$BUILD_CONTEXT"
write_output dockerfile_path "$DOCKERFILE_PATH"
emit_environments

echo "Parsed service configuration:"
echo " name: ${NAME}"
Expand All @@ -193,3 +250,5 @@ echo " deployment_repo: ${DEPLOYMENT_REPO}"
echo " deployment_repo_path: ${DEPLOYMENT_REPO_PATH}"
echo " build_context: ${BUILD_CONTEXT}"
echo " dockerfile_path: ${DOCKERFILE_PATH}"
# Note: the environments log line is emitted by emit_environments() above so it
# also fires on the early-exit paths (config-missing, service-not-found).
11 changes: 11 additions & 0 deletions tests/fixtures-no-env/.skyhook/skyhook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Fixture for the CI 'include_env=true with no environments block' smoke step.
# Intentionally has no `environments:` key so the action exits 1 with
# "Not supported yet" - the smoke job uses continue-on-error and asserts that
# outcome.
services:
- name: smoke-svc
path: services/smoke
buildTool:
docker:
buildContext: services/smoke
dockerfilePath: services/smoke/Dockerfile
8 changes: 8 additions & 0 deletions tests/fixtures/.skyhook/skyhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ services:
buildTool:
docker:
dockerfilePath: services/default/Dockerfile

environments:
- name: dev
clusterName: dev-cluster
namespace: dev
- name: prod
clusterName: prod-cluster
namespace: prod
Loading
Loading