Skip to content

feat: auto-generate --allow-host-service-ports from services: port mappings #23756

@Mossaka

Description

@Mossaka

Problem

When a workflow uses GitHub Actions services: with port mappings (e.g., PostgreSQL, Redis), the compiled workflow runs the agent inside AWF's isolated network. The agent cannot reach service containers without explicit --allow-host-service-ports configuration.

Currently, there is no way for the compiler to automatically detect service ports and pass them to AWF. Users would need to manually configure port access, which defeats the purpose of the declarative services: block.

Why host port mappings are required

The agent runs inside AWF's agent container on an isolated Docker network (awf-net). GitHub Actions services: containers run on the runner's Docker network. These are separate networks — the agent cannot reach services by hostname. The only path is through host port mappings, where the runner maps service container ports to host ports, and AWF allows traffic to the host gateway on those specific ports.

Services declared without ports: are unreachable from the agent:

services:
  redis:
    image: redis
    # No ports: — unreachable from AWF agent

Services with ports: are reachable via the host gateway:

services:
  redis:
    image: redis
    ports:
      - 6379:6379  # Reachable via host gateway

Proposed Solution

Runtime port resolution via GitHub Actions expressions

Instead of parsing port numbers at compile time, the compiler should emit ${{ job.services.<service_id>.ports['<container_port>'] }} expressions. These are resolved by the GitHub Actions runner at runtime, before the shell executes the command.

This handles all port mapping styles:

  • "5432:5432" (explicit) — expression resolves to 5432
  • "5432" (dynamic host port) — expression resolves to the runtime-assigned port
  • "49152:5432" (remapped) — expression resolves to 49152

Compiler behavior

  1. During compilation, parse the services: block to extract service IDs and container ports
  2. For each service with ports:, generate expressions: ${{ job.services.<id>.ports['<container_port>'] }}
  3. Emit --allow-host-service-ports "<comma-separated expressions>" in the AWF command
  4. For services without ports:, emit a compile-time warning

Example

Input workflow:

services:
  postgres:
    image: postgres:15
    ports:
      - 5432:5432
  redis:
    image: redis:7
    ports:
      - 6379:6379

Compiled AWF command includes:

--allow-host-service-ports "${{ job.services.postgres.ports['5432'] }},${{ job.services.redis.ports['6379'] }}"

At runtime, resolves to:

--allow-host-service-ports "5432,6379"

Port parsing rules

Port spec Container port Notes
"5432:5432" 5432 Explicit mapping
"5432" 5432 Dynamic host port
"49152:5432" 5432 Remapped host port
"5432/tcp" 5432 TCP (default)
"5432/udp" skip UDP not supported, warn
"6000-6010:6000-6010" 6000-6010 Range expansion (with cap)
5432 (integer) 5432 YAML may parse as int

Shell quoting consideration

The ${{ }} expressions contain single quotes (e.g., ports['5432']). These must be emitted as raw strings in the compiled YAML, not passed through shell escaping functions. The GitHub Actions runner substitutes ${{ }} expressions before the shell interprets the command, so the single quotes are part of the expression syntax, not shell syntax. The value should be double-quoted in the output to protect the comma-separated result from word splitting.

Implementation Notes

Files to modify

  • New file for port extraction logic (parsing port specs, building expressions)
  • WorkflowData struct — add field to carry the extracted port expressions
  • Service processing — call port extraction after services are merged (handles imports)
  • AWF command builder — add --allow-host-service-ports as a raw (non-shell-escaped) arg

Edge cases to handle

  • Multiple services with multiple ports each → comma-separated expressions
  • Services without ports → compile-time warning
  • No services at all → no-op
  • UDP-only ports → skip with warning
  • Port ranges → expand with reasonable cap
  • Integer port values in YAML → handle type coercion
  • Services from imports → already handled by existing merge logic

Test plan

  • Unit tests: port spec parsing for all syntaxes
  • Unit tests: expression generation for single/multi service+port combinations
  • Integration test: compiled YAML contains correct --allow-host-service-ports expressions
  • Negative tests: services without ports emit warnings, UDP ports skipped
  • Smoke test: add a Redis service to an existing smoke workflow (e.g., services: { redis: { image: redis:7, ports: ["6379:6379"] } }). The smoke agent should be instructed to connect to Redis on localhost:6379 (e.g., redis-cli ping or echo PING | nc localhost 6379) and verify it gets a PONG response. The agent receives no special context about AWF or port configuration — it just tries to use Redis as any normal workflow would. This validates the full end-to-end path: compiler extracts ports → expressions resolve at runtime → AWF allows traffic → agent reaches the service.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions