Skip to content

Add new step type: step.static_file for serving files from disk#250

Merged
intel352 merged 5 commits intomainfrom
copilot/add-static-file-step-type
Mar 3, 2026
Merged

Add new step type: step.static_file for serving files from disk#250
intel352 merged 5 commits intomainfrom
copilot/add-static-file-step-type

Conversation

Copy link
Contributor

Copilot AI commented Mar 3, 2026

  • Create module/pipeline_step_static_file.go implementation
  • Add unit tests in module/pipeline_step_static_file_test.go
    • Fix ignored os.WriteFile error in TestStaticFileStep_MissingContentType
    • Fix dropped io.ReadAll error and missing resp.Body.Close() in TestStaticFileStep_ServesFile
    • Add TestStaticFileStep_ConfigRelativePath to cover _config_dir-relative path resolution
  • Register in plugins/pipelinesteps/plugin.go (StepTypes + StepFactories)
  • Add to schema/schema.go coreModuleTypes
  • Add schema entry in schema/module_schema.go
    • Improve output description to accurately describe HTTP-writer vs no-writer behavior
  • Add to cmd/wfctl/type_registry.go
  • Update plugins/pipelinesteps/plugin_test.go expectedSteps list
  • All tests pass
Original prompt

This section details on the original issue you should resolve

<issue_title>New step type: step.static_file for serving files from disk</issue_title>
<issue_description>## Use Case

Pipelines sometimes need to serve static files (OpenAPI specs, HTML pages, configuration files) as HTTP responses. Currently step.raw_response only supports inline body strings or body_from pipeline context references — it cannot read from a file on disk.

Proposed Solution

A new step.static_file step type:

- name: serve_spec
  type: step.static_file
  config:
    file: ./specs/chimera-api.yaml    # resolved relative to config file
    content_type: "application/yaml"
    cache_control: "public, max-age=3600"  # optional

Features:

  • Reads file at init time (not per-request) for performance
  • File path resolved relative to the config file's directory (using config.ResolvePathInConfig)
  • Content-Type must be specified
  • Optional Cache-Control header
  • Returns the file content as the HTTP response body

Alternatively, this could be a file or body_file config option on the existing step.raw_response:

- name: serve_spec
  type: step.raw_response
  config:
    content_type: "application/yaml"
    body_file: ./specs/chimera-api.yaml   # NEW: read body from file

Context

Chimera needs to serve an OpenAPI spec file at /openapi.yaml for Swagger UI consumption. The openapi module can do this but also registers routes that conflict with pipeline triggers (see #247). A step.static_file would allow serving the spec via a simple pipeline without route conflicts.

🤖 Generated with Claude Code</issue_description>

<agent_instructions>This would be a simple alternative to the full static.fileserver module that we have existing, by being able to more simply serve a smaller static file use case</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@intel352
Copy link
Contributor

intel352 commented Mar 3, 2026

@copilot retry

…disk

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 3, 2026

@copilot retry

Implemented step.static_file in commit 97951e5.

The new step type:

  • Reads the file at factory creation time (init, not per-request) for performance
  • Resolves file path relative to the config file directory via config.ResolvePathInConfig
  • Requires content_type config
  • Supports optional cache_control header
  • Sets _response_handled and Stop: true after writing the HTTP response

Example usage:

- name: serve_spec
  type: step.static_file
  config:
    file: ./specs/chimera-api.yaml
    content_type: "application/yaml"
    cache_control: "public, max-age=3600"

Copilot AI changed the title [WIP] Add new step type: step.static_file for serving files from disk Add new step type: step.static_file for serving files from disk Mar 3, 2026
@intel352 intel352 marked this pull request as ready for review March 3, 2026 21:00
Copilot AI review requested due to automatic review settings March 3, 2026 21:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new built-in pipeline step type, step.static_file, to serve a single file from disk as an HTTP response (with init-time file loading and config-relative path resolution), intended as a lightweight alternative to static.fileserver for simple one-file routes.

Changes:

  • Implement step.static_file step factory + execution behavior (HTTP-writer mode + no-writer fallback).
  • Register the new step type across plugin manifests/factories, schema registries, and wfctl type registry.
  • Add unit tests and update plugin step factory expectations.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
module/pipeline_step_static_file.go Implements the new step.static_file step (init-time file read; writes HTTP response or returns body in output when no writer).
module/pipeline_step_static_file_test.go Adds unit tests for serving content, no-writer output mode, and config validation/error cases.
plugins/pipelinesteps/plugin.go Registers step.static_file in StepTypes and StepFactories.
plugins/pipelinesteps/plugin_test.go Updates expectedSteps list to include step.static_file.
schema/schema.go Adds step.static_file to coreModuleTypes.
schema/module_schema.go Registers the module schema for step.static_file (config fields + description).
cmd/wfctl/type_registry.go Adds step.static_file to wfctl known step types/config keys.

func TestStaticFileStep_MissingContentType(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "data.txt")
_ = os.WriteFile(filePath, []byte("hello"), 0o600)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

os.WriteFile errors are ignored here. If the write fails, this test may pass/fail for the wrong reason (it will later error on missing content_type rather than setup failure). Please assert the write succeeded (like the other tests in this file).

Suggested change
_ = os.WriteFile(filePath, []byte("hello"), 0o600)
if err := os.WriteFile(filePath, []byte("hello"), 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}

Copilot uses AI. Check for mistakes.
Category: "pipeline",
Description: "Serves a static file from disk as an HTTP response; file is read at init time for performance",
Inputs: []ServiceIODef{{Name: "context", Type: "PipelineContext", Description: "Pipeline context (HTTP response writer)"}},
Outputs: []ServiceIODef{{Name: "result", Type: "StepResult", Description: "HTTP response with file content"}},
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The schema output description is a bit misleading/inconsistent with other response steps: when an HTTP writer is present, the file content is written to the response but is not present in StepResult.Output. Consider wording this like step.json_response (e.g., “writes an HTTP response and stops the pipeline”; optionally mention the no-writer fallback returns body).

Suggested change
Outputs: []ServiceIODef{{Name: "result", Type: "StepResult", Description: "HTTP response with file content"}},
Outputs: []ServiceIODef{{Name: "result", Type: "StepResult", Description: "Writes an HTTP response with file content and stops the pipeline; if no HTTP writer is available, returns the file content in StepResult.Output as body"}},

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +28
factory := NewStaticFileStepFactory()
step, err := factory("serve_spec", map[string]any{
"file": filePath,
"content_type": "application/yaml",
"cache_control": "public, max-age=3600",
}, nil)
if err != nil {
t.Fatalf("factory error: %v", err)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The tests don’t cover the advertised config-relative path resolution via _config_dir/config.ResolvePathInConfig (i.e., passing a relative file path and setting _config_dir in the step config). Adding a test for this would guard the primary feature this step is introducing.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +58
resp := recorder.Result()
if resp.StatusCode != 200 {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); ct != "application/yaml" {
t.Errorf("expected Content-Type application/yaml, got %q", ct)
}
if cc := resp.Header.Get("Cache-Control"); cc != "public, max-age=3600" {
t.Errorf("expected Cache-Control header, got %q", cc)
}

body, _ := io.ReadAll(resp.Body)
if string(body) != content {
t.Errorf("expected body %q, got %q", content, string(body))
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Avoid ignoring I/O errors in tests: io.ReadAll(resp.Body) drops the returned error, and resp.Body isn’t closed. Checking the read error and closing the body will prevent false positives and resource leaks if the response body can’t be read for some reason.

Copilot uses AI. Check for mistakes.
@intel352
Copy link
Contributor

intel352 commented Mar 3, 2026

@copilot apply changes based on the comments in this thread

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 3, 2026

@copilot apply changes based on the comments in this thread

Applied all 4 review changes in commit c159f2b:

  1. Fixed ignored os.WriteFile error in TestStaticFileStep_MissingContentType
  2. Fixed dropped io.ReadAll error and added resp.Body.Close() in TestStaticFileStep_ServesFile
  3. Added TestStaticFileStep_ConfigRelativePath test covering _config_dir-relative path resolution via config.ResolvePathInConfig
  4. Updated schema output description to accurately describe both HTTP-writer and no-writer behavior

@intel352 intel352 merged commit ef8b897 into main Mar 3, 2026
14 checks passed
@intel352 intel352 deleted the copilot/add-static-file-step-type branch March 3, 2026 22:41
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.

New step type: step.static_file for serving files from disk

3 participants