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
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ on:
workflow_dispatch:

jobs:
# Unit tests for the skyhook config resolver. These run in seconds and don't
# need Docker, so they're the first line of defence against regressions in
# field-name handling, root-vs-service precedence, and the override semantics
# of `resolved_context` / `resolved_dockerfile`. ubuntu-latest ships yq v4.
test-skyhook-resolver-unit:
runs-on: ubuntu-latest
name: Unit tests — skyhook config resolver
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run resolver unit tests
run: bash test/unit/test_resolve_skyhook_config.sh

test-basic-build:
runs-on: ubuntu-latest
name: Test basic Docker build
Expand Down
34 changes: 31 additions & 3 deletions .skyhook/skyhook.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
# Test configuration for skyhook config resolution tests
# All paths are relative to the repository root
# Test configuration for skyhook config resolution tests.
# All paths are repo-root-relative (canonical schema, see
# koala-backend/internal/conf/skyhook.go::SkyhookDockerBuild).
#
# Exercises every branch of the resolver:
# - canonical per-service `buildContext` (web)
# - legacy per-service `contextPath` (api, worker) — back-compat path
# - root-level `buildContext` inherited by service (inherits-root)
# - service with no buildTool anywhere (simple) — defers to inputs

buildTool:
docker:
# Root-level default. Services that omit buildTool entirely should
# inherit this. Per-service overrides take precedence.
buildContext: test/services/shared

services:
- name: web
path: test/services/web
buildTool:
docker:
# Canonical field name. Should win over the root buildContext above.
buildContext: test/services/web

- name: api
path: test/services/api
deploymentRepo: my-org/deployment
deploymentRepoPath: api
buildTool:
docker:
# Legacy field name. Kept here so the resolver's deprecation /
# back-compat path stays exercised in tests.
contextPath: test/services/api

- name: worker
Expand All @@ -16,6 +39,11 @@ services:
contextPath: test/services/worker
dockerfilePath: test/services/worker/docker/Dockerfile

- name: inherits-root
path: test/services/inherits-root
# No per-service buildTool — should fall back to the root buildContext.

- name: simple
path: test/services/simple
# No buildTool defined - should use path as context fallback
# No per-service buildTool — same as inherits-root above; documents that
# the legacy fixture name still works without changes after the rewrite.
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ The simplest way to use this action - just provide the image repository and prim

*Must provide either (`image` + `base_tag`) OR `tags`

#### Skyhook Config (auto-resolve context + Dockerfile)
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `service_name` | Name of a service defined in `.skyhook/skyhook.yaml`. When set, `context` and `dockerfile` are resolved from that file (see [Skyhook Config](#skyhook-config) below). | No | - |

### Build Configuration

| Input | Description | Required | Default |
Expand Down Expand Up @@ -169,6 +174,71 @@ All parameters prefixed with `buildx_` are passed directly to docker/setup-build
| `metadata` | Build result metadata |
| `tags_list` | Newline-delimited list of image:tag combinations |

## Skyhook Config

When `service_name` is set, the action reads `.skyhook/skyhook.yaml` from the **calling workflow's checkout directory** (which must live under `code/` — i.e. `actions/checkout` is expected to have placed the source under `./code`) and uses it to derive the build context and Dockerfile. The explicit `context` / `dockerfile` inputs are ignored in this mode.

### Schema

The action looks at `buildTool.docker.{buildContext,dockerfilePath}` at two levels — root (applies to every service) and per-service (overrides the root):

```yaml
# .skyhook/skyhook.yaml
buildTool:
docker:
buildContext: shared # root-level default for every service
dockerfilePath: shared/Dockerfile

services:
- name: api
path: apps/api # used as a fallback for Dockerfile only

- name: worker
path: apps/worker
buildTool:
docker:
buildContext: apps/worker/src # per-service override
dockerfilePath: apps/worker/docker/Dockerfile
```

All paths are repo-root-relative. `.` and `./` are normalised to "no override" — use them when you mean "fall through to the next step in the chain".

### Resolution chain

| step | context | dockerfile |
|-------|-------------------------------|-----------------------------------------------|
| 1 | per-service `buildContext` | per-service `dockerfilePath` |
| 2 | root `buildContext` | root `dockerfilePath` |
| 3 | _(no further fallback)_ | `<services[].path>/Dockerfile` |
| 4 | `code` (entire checkout) | `code/Dockerfile` |

The two chains are independent: setting only `buildContext` does **not** make `dockerfile` resolve relative to it — `dockerfile` runs through its own chain.

### Override semantics (important)

Once `service_name` is set, the resolved values **always take precedence** over the action's own `context` / `dockerfile` inputs — even when no override is found in YAML and the chain falls through to `code` / `code/Dockerfile`. This is intentional so behaviour is predictable across the matrix of "service exists with overrides", "service exists without overrides", "service not found", and "no `.skyhook/skyhook.yaml` at all".

If you want the calling workflow's `context` / `dockerfile` inputs to be honoured, **don't set `service_name`** (manual mode).

### Deprecated field: `contextPath`

`buildTool.docker.contextPath` is the legacy alias for `buildContext`. It is still honoured for backwards compatibility, but the action emits a one-shot warning and you should rename it. `dockerfilePath` has no historical alias.

### Example

```yaml
- uses: actions/checkout@v4
with:
path: code # required: action expects sources under ./code

- uses: skyhook-io/docker-build-push-action@v1
with:
image: ghcr.io/${{ github.repository }}
base_tag: v1.2.3
service_name: worker # everything else (context, dockerfile)
# comes from .skyhook/skyhook.yaml
```

## Examples

### Using Automatic Tag Generation
Expand Down Expand Up @@ -233,6 +303,31 @@ All parameters prefixed with `buildx_` are passed directly to docker/setup-build
push: true
```

### Skyhook Config Mode

```yaml
# .skyhook/skyhook.yaml in your repo:
# services:
# - name: api
# path: apps/api
# buildTool:
# docker:
# buildContext: apps/api
# dockerfilePath: apps/api/Dockerfile

- uses: actions/checkout@v4
with:
path: code

- uses: skyhook-io/docker-build-push-action@v1
with:
image: ghcr.io/${{ github.repository }}
base_tag: v1.2.3
service_name: api # context + dockerfile come from .skyhook/skyhook.yaml
```

See [Skyhook Config](#skyhook-config) for the full schema, resolution chain, and override semantics.

### Build with Build Arguments

```yaml
Expand Down
80 changes: 10 additions & 70 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,76 +198,16 @@ runs:
id: skyhook_config
shell: bash
working-directory: code
run: |
set -euo pipefail

# Use the input if it exists;
if [[ -n "${{ inputs.service_name }}" ]]; then
SERVICE_NAME="${{ inputs.service_name }}"
fi

# If no service_name provided, skip config resolution
if [[ -z "$SERVICE_NAME" ]]; then
echo "No service_name provided, skipping config resolution"
echo "resolved_context=code" >> "$GITHUB_OUTPUT"
exit 0
fi

# Find the config file
CONFIG_FILE=""
if [[ -f ".skyhook/skyhook.yaml" ]]; then
CONFIG_FILE=".skyhook/skyhook.yaml"
fi

if [[ -z "$CONFIG_FILE" ]]; then
echo "::warning::service_name '$SERVICE_NAME' provided but .skyhook/skyhook.yaml not found. Falling back to input parameters."
echo "resolved_context=code" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Found config file: $CONFIG_FILE"

# Check if service exists
SERVICE_EXISTS=$(yq ".services[] | select(.name == \"$SERVICE_NAME\") | .name" "$CONFIG_FILE")
if [[ -z "$SERVICE_EXISTS" ]]; then
echo "::warning::Service '$SERVICE_NAME' not found in $CONFIG_FILE. Falling back to input parameters."
echo "resolved_context=code" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Found service '$SERVICE_NAME' in config"

# Extract build config using yq
# Context: only use buildTool.docker.contextPath if explicitly set, otherwise empty (defaults to repo root)
CONTEXT_PATH=$(yq ".services[] | select(.name == \"$SERVICE_NAME\") | .buildTool.docker.contextPath // \"\"" "$CONFIG_FILE")
DOCKERFILE_PATH=$(yq ".services[] | select(.name == \"$SERVICE_NAME\") | .buildTool.docker.dockerfilePath // \"\"" "$CONFIG_FILE")

# Normalize outputs to repo root (this step runs in working-directory: code)
REPO_PREFIX="code"

# Context: if contextPath is set use code/{contextPath}, else use code
if [[ -n "$CONTEXT_PATH" ]]; then
RESOLVED_CONTEXT="$REPO_PREFIX/$CONTEXT_PATH"

# If dockerfile is not set, default to {context}/Dockerfile
if [[ -z "$DOCKERFILE_PATH" ]]; then
DOCKERFILE_PATH="${CONTEXT_PATH}/Dockerfile"
echo "Defaulting dockerfile to: $REPO_PREFIX/$DOCKERFILE_PATH"
fi
else
RESOLVED_CONTEXT="$REPO_PREFIX"
fi
echo "Using context: $RESOLVED_CONTEXT"
echo "resolved_context=$RESOLVED_CONTEXT" >> "$GITHUB_OUTPUT"

if [[ -n "$DOCKERFILE_PATH" ]]; then
FULL_DOCKERFILE_PATH="$REPO_PREFIX/$DOCKERFILE_PATH"
echo "Using dockerfile from config: $FULL_DOCKERFILE_PATH"
echo "resolved_dockerfile=$FULL_DOCKERFILE_PATH" >> "$GITHUB_OUTPUT"
fi

echo "config_file=$CONFIG_FILE" >> "$GITHUB_OUTPUT"
echo "service_name=$SERVICE_NAME" >> "$GITHUB_OUTPUT"
env:
SERVICE_NAME: ${{ inputs.service_name }}
# Path prefix the consumer expects the resolved values to live under.
# This step runs inside `code/` (the calling workflow's checkout dir),
# so any path read from skyhook.yaml is repo-root-relative and gets
# `code/` prepended before being emitted as an output.
REPO_PREFIX: code
# Resolver lives in scripts/ so it can be unit-tested independently of
# GitHub Actions (see test/unit/test_resolve_skyhook_config.sh).
run: bash "$GITHUB_ACTION_PATH/scripts/resolve_skyhook_config.sh"

- name: Validate inputs
shell: bash
Expand Down
Loading
Loading