Skip to content

fix(skyhook): align resolver with canonical schema; stop overriding workflow inputs - #4

Closed
eliran-ops wants to merge 4 commits into
mainfrom
fix/skyhook-context-resolution
Closed

fix(skyhook): align resolver with canonical schema; stop overriding workflow inputs#4
eliran-ops wants to merge 4 commits into
mainfrom
fix/skyhook-context-resolution

Conversation

@eliran-ops

@eliran-ops eliran-ops commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

What

Fixes three independent bugs in the Resolve build config from .skyhook/skyhook.yaml step that combined to make the action silently override inputs.context with code whenever service_name was set, regardless of what the caller (or the YAML) actually configured.

# Bug Net effect
1 Read buildTool.docker.contextPath. The canonical schema (koala-backend/internal/conf/skyhook.go::SkyhookDockerBuild) uses buildContext. Customers following the canonical schema saw their context overrides ignored.
2 Lookups only scanned .services[] — no fallback to top-level buildTool.docker.*. Monorepos that defined a single repo-wide build context at the root never saw it applied.
3 Every error / no-config path emitted resolved_context=code, and the consumer expression put resolved_context first in the `

Empirically observed on a real run (skyhook-dev/nbjkgj): the workflow correctly resolved context: code/./src from a root-level buildContext: ./src, the action then emitted Using context: code and overrode it, and the build failed with `buildx failed with: ERROR: failed to read dockerfile: open Dockerfile: no such file or directory`.

How

  • Field name. Read canonical `buildContext` first, fall back to legacy `contextPath` for back-compat. Emit a one-time `::warning::` when the legacy name is what got picked up.
  • Root-level fallback. Per-service > root > empty, mirroring `SkyhookConfig.ResolveBuildContext`.
  • `.` / `./` normalization. Both collapse to "no override" — same behaviour as the canonical Go resolver and the workflow-side resolver in `build_image.yml`. Keeps all three layers in agreement.
  • No more unconditional `code` override. When skyhook.yaml provides nothing applicable, emit nothing — the consumer's `||` then correctly falls through to `inputs.context` / `inputs.dockerfile`.
  • Path tidiness. Strip a leading `./` so we emit `code/src` not `code/./src`.
  • `yq v4` correctness. Use `strenv()` for env-var injection — the jq-style `--arg` flag is not supported on yq v4 and silently produces wrong queries.

Where to start reading

  • `scripts/resolve_skyhook_config.sh` — the resolver, extracted from the inline bash in `action.yml` so it can be unit-tested independently of GitHub Actions.
  • `action.yml` — the `Resolve build config` step now just `bash "$GITHUB_ACTION_PATH/scripts/resolve_skyhook_config.sh"`.
  • `test/unit/test_resolve_skyhook_config.sh` — 8 cases covering canonical vs. legacy field names, root-vs-service precedence, the `./` normalization, the no-override-anywhere case, unknown service, and empty service_name.
  • `.github/workflows/test.yml` — new `test-skyhook-resolver-unit` job runs the unit tests in CI (fast; no Docker required).
  • `.skyhook/skyhook.yaml` — extended to exercise canonical `buildContext`, legacy `contextPath`, and the root-level fallback simultaneously.

Out of scope

  • The pre-existing docker-based skyhook tests in `.github/workflows/test.yml` have been failing on `main` for an unrelated reason (the action runs the resolver under `working-directory: code`, but the test workflow checks out at the workspace root, not under `code/`). Not touched here — separate fix.
  • README does not currently document the skyhook-config mode at all; intentionally not adding docs in this PR to keep the diff focused on the bug fix. Happy to follow up.

Test plan

  • `bash test/unit/test_resolve_skyhook_config.sh` — 8/8 pass locally
  • CI `test-skyhook-resolver-unit` job passes
  • End-to-end: re-run `Build and Deploy` on `skyhook-dev/nbjkgj` against `@fix/skyhook-context-resolution` and confirm `Using context: code/test/services/web/src` (or equivalent) instead of the previous `Using context: code`

Made with Cursor


Note

Medium Risk
Changes the action’s build context/Dockerfile resolution logic for service_name, which can alter what gets built and where Dockerfile is read from. Added unit tests reduce regression risk, but mis-resolution would still break builds for existing users.

Overview
Fixes Skyhook config mode so service_name resolves build context/dockerfile from .skyhook/skyhook.yaml using the canonical buildTool.docker.buildContext (with legacy contextPath support), root-vs-service precedence, and ././ normalization.

The resolver is extracted from action.yml into scripts/resolve_skyhook_config.sh, adds a deprecation warning for contextPath, and consistently emits resolved_context/resolved_dockerfile (including fallbacks via services[].path). CI now runs fast unit tests for this resolver, the test .skyhook/skyhook.yaml fixture is expanded to cover all branches, and the README documents the Skyhook config schema and resolution semantics.

Reviewed by Cursor Bugbot for commit 87f5e5f. Bugbot is set up for automated code reviews on this repo. Configure here.

…orkflow inputs

Three bugs in the skyhook config resolution path were causing every
caller's `inputs.context` to be silently clobbered, regardless of what
they actually configured in `.skyhook/skyhook.yaml`:

1. **Wrong field name.** The action read
   `buildTool.docker.contextPath`, but the canonical schema in
   `koala-backend/internal/conf/skyhook.go::SkyhookDockerBuild` defines
   the field as `buildContext`. Customers following the schema saw
   their context overrides ignored entirely.

2. **No root-level fallback.** The yq lookups only scanned
   `.services[]`, with no fallback to top-level
   `buildTool.docker.*`. Monorepos that defined a single repo-wide
   build context at the root never saw it applied.

3. **Unconditional `resolved_context=code`.** Every error / no-config
   path emitted `resolved_context=code`, and the consumer expression
   put `resolved_context` first in the `||` chain. Net effect:
   whenever `service_name` was set, the action's resolver would always
   override `inputs.context` with `code`, even when the calling
   workflow had carefully resolved the right value (e.g. PR
   koala-backend#1319's new bash resolver in `build_image.yml`).

This change:

- Reads canonical `buildContext` first, falls back to legacy
  `contextPath` for back-compat, and emits a one-time deprecation
  warning when the legacy name is what got picked up.
- Adds the missing root-level fallback (per-service > root > empty),
  mirroring `SkyhookConfig.ResolveBuildContext`.
- Normalizes `.` and `./` to "no override" — same behaviour as the
  canonical Go resolver and the workflow-side resolver in
  `build_image.yml`. Keeps all three layers in agreement.
- Stops emitting `resolved_context` when nothing in skyhook.yaml
  applies, so the consumer's `||` correctly falls through to
  `inputs.context` / `inputs.dockerfile`.
- Strips a leading `./` from extracted values so we emit `code/src`
  instead of `code/./src`.

The resolver bash is extracted to `scripts/resolve_skyhook_config.sh`
so it can be unit-tested independently of GitHub Actions. A new CI job
(`test-skyhook-resolver-unit`) runs eight cases covering canonical
vs. legacy field names, root-vs-service precedence, the `./`
normalization, the no-override-anywhere case, and unknown / empty
service names. Uses `yq v4`'s `strenv()` for env-var injection — the
jq-style `--arg` flag is not supported on yq v4 and silently produces
wrong queries.

The shipped `.skyhook/skyhook.yaml` test fixture is extended to
exercise the canonical field, the legacy field, and the root-level
fallback simultaneously.

The pre-existing docker-based skyhook tests in `.github/workflows/test.yml`
have been failing on `main` for an unrelated reason (`working-directory:
code` doesn't exist when the test workflow checks out at the repo
root, not under `code/`); this PR does not touch that.

Made-with: Cursor
…file

Aligns the action's resolution chain with koala-backend PR #1319's
workflow-side resolver so both layers compute the same value:

  step │ context                  │ dockerfile
  ─────┼──────────────────────────┼─────────────────────────────────
   1   │ per-service buildContext │ per-service dockerfilePath
   2   │ root        buildContext │ root        dockerfilePath
   3   │ code                     │ code/<SERVICE_DIR>/Dockerfile
   4   │ code                     │ code/Dockerfile  (no SERVICE_DIR)

Notable choices:

- Context has no SERVICE_DIR step. The build context defaults to the
  entire checkout when nothing in YAML overrides it; .dockerignore /
  Dockerfile COPY paths govern what's actually included. Avoids the
  surprise of narrowing context to a sub-directory the user didn't ask
  for.
- Dockerfile chains independently of context. When only buildContext is
  overridden, the Dockerfile still falls through to
  code/<SERVICE_DIR>/Dockerfile (or code/Dockerfile) — it does NOT
  auto-default to <context>/Dockerfile.
- The resolver now ALWAYS emits resolved_context / resolved_dockerfile
  whenever service_name is set. Manual mode (no service_name) is
  unchanged: noop, consumer's `||` falls through to inputs.context /
  inputs.dockerfile. Bug 3 ("action overrides workflow input") becomes
  moot because both resolvers compute the same value.

A new optional `service_dir` action input feeds the SERVICE_DIR
fallback for callers that want it (e.g. koala-backend's generated
`build_image.yml`). Standalone callers can omit it; the dockerfile
then defaults to code/Dockerfile.

Unit tests:
- Replaced the "no-override-anywhere → no outputs" case with two new
  cases that pin down the SERVICE_DIR vs no-SERVICE_DIR fallback.
- Updated the legacy-contextPath case to assert the dockerfile falls
  through to code/Dockerfile (proving context and dockerfile are
  resolved independently).
- Updated the "unknown service" case to assert the SERVICE_DIR-based
  dockerfile fallback still applies.

9/9 cases green locally.

Made-with: Cursor
The previous commit added a `service_dir` action input as the
SERVICE_DIR fallback for the dockerfile chain. It was redundant: the
action already reads the YAML, and the value `service_dir` was
expected to carry is exactly `services[name=$SERVICE_NAME].path`.

This commit removes the input and reads `services[].path` from the
YAML directly. The chain semantics are unchanged:

  step │ context                  │ dockerfile
  ─────┼──────────────────────────┼────────────────────────────────
   1   │ per-service buildContext │ per-service dockerfilePath
   2   │ root        buildContext │ root        dockerfilePath
   3   │ code                     │ code/<service.path>/Dockerfile
   4   │ code                     │ code/Dockerfile  (no service.path)

Net effect:
- One fewer surface area on the action; service_name is the only
  skyhook-mode input now.
- One fewer place where caller-passed value and YAML value can drift.
- Test fixture extended with a second "no buildTool" service that
  also has no `path`, so step-4 (code/Dockerfile) has its own
  unit-test coverage.

9/9 unit tests still green.

Made-with: Cursor
README had zero docs for skyhook config mode, so users hitting the
new `contextPath` deprecation warning landed on a 404 anchor and had
no way to learn the resolution chain or override semantics. Resolver
also silently swallowed yq stderr, hiding malformed YAML behind a
"no overrides" fallback that looked identical to a clean config.

- README: new `## Skyhook Config` section (schema, resolution chain,
  override semantics, deprecated alias) + `service_name` input row +
  example. Anchor matches the URL emitted by the deprecation warning.
- Resolver: drop `2>/dev/null` from yq calls so parse errors land in
  the job log; keep `|| true` so the chain still falls through.
- Tests: assert the new stderr-surfacing contract on a malformed
  fixture, and lock the README↔resolver anchor consistency so the
  warning link can't silently rot in a future docs rewrite.

Made-with: Cursor

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 87f5e5f. Configure here.

fi
}

log() { printf '%s\n' "$*" >&2; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Workflow warning commands sent to stderr not stdout

Medium Severity

The log() helper writes to stderr (>&2), but all three ::warning:: workflow commands are routed through it. GitHub Actions only processes ::warning:: annotations from stdout — stderr output appears as plain text in the log without creating actual warning annotations in the UI. The old inline code used echo (stdout) for these warnings and they worked correctly. The deprecation warning, "service not found" warning, and "config file not found" warning will all fail to produce annotations.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 87f5e5f. Configure here.

@eliran-ops eliran-ops closed this May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants