Skip to content

Add Render deployment (Docker cron job + one-off jobs)#1

Merged
aaronbrethorst merged 3 commits into
mainfrom
dockerfile
May 25, 2026
Merged

Add Render deployment (Docker cron job + one-off jobs)#1
aaronbrethorst merged 3 commits into
mainfrom
dockerfile

Conversation

@aaronbrethorst
Copy link
Copy Markdown
Member

@aaronbrethorst aaronbrethorst commented May 25, 2026

Summary

  • Deploys oba-validator to Render as a Docker cron job scheduled for Feb 29 (0 2 29 2 *) so it effectively never runs on its own; real validations are launched on demand as Render one-off jobs.
  • The image is key-agnostic — nothing server-specific is baked in, so one deployment validates any OBA server. The per-server apiKey rides inside the config supplied at invocation time.
  • Mirrors obacloud's existing one-off-job pattern: Render splits a job's startCommand on whitespace with no shell (first token = the executable, overriding ENTRYPOINT), so the config JSON is base64-encoded into one token and decoded by entrypoint.sh.
  • Adds GitHub Actions CI (tests + Docker image) so the deployment and the code stay verified now and going forward.

Deployment artifacts: Dockerfile (multi-stage pure-Go → alpine + ca-certificates), entrypoint.sh, render.yaml Blueprint, .dockerignore, a docker-build Makefile target, a README "Deploying to Render" section, and a design spec.

CI (.github/workflows/ci.yml, two parallel jobs):

  • testgofmt check, go vet, go mod tidy cleanliness, go mod verify, go test -race ./... (the validator fans out concurrently, so the race detector earns its place), and shellcheck entrypoint.sh.
  • docker — builds the deployment image and smoke-tests it offline: base64 of {} exercises entrypoint.sh → oba-validator → config.Load and must exit 2 (config error), so a broken Dockerfile/entrypoint fails the PR rather than the Render deploy.
  • Triggers on push-to-main and PRs; uses go-version-file: go.mod (auto-tracks the pinned Go version); concurrency cancellation + per-job timeouts. The live integration test (OBA_VALIDATOR_LIVE) is intentionally excluded — it depends on a real OBA server.

Test plan

Deployment, exercised against the live KCM/Puget Sound config (public sample key org.onebusaway.iphone):

  • make docker-build builds clean
  • Render pathdocker run --rm oba-validator <base64-config> runs a full validation; exit code propagates through exec (validation failure → 1)
  • Render-override simulation — same with --entrypoint /app/entrypoint.sh → same report
  • Local ergonomicsdocker run --rm oba-validator '<raw-json>' ("{"-prefixed) passes through → full report
  • Flag forwarding... <base64> --json emits JSON
  • Failure modes — no arg → exit 2 (usage); non-base64/non-{ arg → loud base64 decode error, non-zero exit

CI, verified locally and on a real Actions run (green):

  • Every step run locally: gofmt, vet, go mod tidy (no diff), go mod verify, go test -race ./..., shellcheck, docker build + smoke test (exit 2)
  • Two live CI runs passed; resolved a Node 20 deprecation by bumping checkout/setup-go to v6 (Node 24) and re-verified — no warnings remain

Review notes

  • The invocation model was corrected during review. The first cut assumed ENTRYPOINT was preserved (single-quoted JSON appended as args). Two review agents flagged it; obacloud's production integration (api_key_service_job.rb, create_job.rb) confirmed Render overrides ENTRYPOINT, whitespace-splits with no shell, and the established fix is base64 + an entrypoint.sh decoder.
  • Not yet exercised on Render itself — deployment verification was local Docker plus matching obacloud's proven pattern. Worth one real one-off job to confirm end-to-end before relying on it.
  • The rare scheduled fire (~once every 4 years) runs entrypoint.sh with no args → exit 2 (a harmless "failed" run). Left as-is for simplicity; a dockerCommand no-op could make it a clean exit 0 if that matters.

🤖 Generated with Claude Code

Deploy oba-validator to Render as a Docker cron job scheduled for Feb 29
(0 2 29 2 *) so it effectively never runs on its own; real validations are
launched on demand as Render one-off jobs.

Render splits a job's startCommand on whitespace with no shell and uses the
first token as the executable, so the JSON config is base64-encoded into one
token and decoded by entrypoint.sh (mirroring obacloud's existing one-off-job
pattern). No API key is baked in — keys are per-server and ride inside the
config.

- Dockerfile: multi-stage pure-Go build, alpine + ca-certificates
- entrypoint.sh: base64-decodes the config; raw JSON ("{"-prefixed) passed through
- render.yaml: Blueprint for the Docker cron job (no env vars by design)
- .dockerignore, Makefile docker-build target, README + design spec

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

This PR adds containerized deployment support for oba-validator on Render. It introduces a multi-stage Docker build, an entrypoint wrapper that base64-decodes runtime-supplied JSON config, a Render cron job definition, a Makefile docker-build target, and comprehensive deployment documentation explaining how one-off jobs invoke the validator with config passed as an encoded command-line argument.

Changes

Render Deployment Support

Layer / File(s) Summary
Docker build infrastructure
Dockerfile, .dockerignore, Makefile
Multi-stage Go build compiles static binary in Alpine; runtime image installs CA certs and includes entrypoint.sh; .dockerignore excludes unnecessary paths; Makefile adds docker-build target and IMAGE variable.
Entrypoint base64 config wrapper
entrypoint.sh
POSIX shell script validates arguments, detects raw JSON vs. base64-encoded config, decodes as needed, and execs oba-validator with decoded config as sole positional argument and remaining args as flags.
Render cron service definition
render.yaml
Defines Docker cron job scheduled at 0 2 29 2 * (never fires); intended for one-off job invocation with full per-server config (including apiKey) passed as base64-encoded command argument.
Deployment guide and design specification
README.md, docs/superpowers/specs/2026-05-25-render-deployment-design.md
README adds Development section with docker-build target and full Deploying to Render section with example docker run and Render API curl/Ruby invocations; design doc comprehensively specifies invocation model, artifact wiring, cache behavior, exit codes, and verification steps.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding Render deployment with Docker cron job and one-off jobs support, which is the primary objective and central to all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dockerfile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
docs/superpowers/specs/2026-05-25-render-deployment-design.md (1)

51-53: 💤 Low value

Add language identifier to fenced code block.

The fenced code block at line 51 should specify a language identifier for better Markdown tooling compatibility and syntax highlighting.

📝 Proposed fix
-```
+```plaintext
 /app/entrypoint.sh <base64-of-compact-config-json>
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/superpowers/specs/2026-05-25-render-deployment-design.md around lines
51 - 53, Add a language identifier to the fenced code block that contains
"/app/entrypoint.sh " so Markdown tooling can
provide proper highlighting; update the opening fence from to something likeplaintext or ```bash to indicate the content type and keep the rest of the
block unchanged.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @Dockerfile:

  • Around line 1-34: Add a non-root runtime user and switch to it in the
    Dockerfile: create a user/group (e.g., obauser), chown /app and the binary
    (/app/oba-validator and /app/entrypoint.sh) to that user, ensure entrypoint.sh
    remains executable, and add USER obauser before ENTRYPOINT so the container does
    not run as root; reference the Dockerfile stages that COPY --from=builder
    /build/oba-validator and COPY entrypoint.sh /app/entrypoint.sh and ensure
    permissions/ownership changes happen after those COPY steps.

Nitpick comments:
In @docs/superpowers/specs/2026-05-25-render-deployment-design.md:

  • Around line 51-53: Add a language identifier to the fenced code block that
    contains "/app/entrypoint.sh " so Markdown
    tooling can provide proper highlighting; update the opening fence from to something likeplaintext or ```bash to indicate the content type and keep the
    rest of the block unchanged.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro Plus

**Run ID**: `2a7941b0-0524-42ae-b356-fb803373f093`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between adef7fef357c7701e9b2a590a7ce2a6ccc5b25e7 and 8669417b9a1c7460b889a7f9a02150828db6e749.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `.dockerignore`
* `Dockerfile`
* `Makefile`
* `README.md`
* `docs/superpowers/specs/2026-05-25-render-deployment-design.md`
* `entrypoint.sh`
* `render.yaml`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread Dockerfile
Comment on lines +1 to +34
# Stage 1: build the Go binary
FROM golang:1-alpine AS builder

WORKDIR /build

# Cache module downloads across builds
COPY go.mod go.sum ./
RUN go mod download

# Build a static binary (no cgo) for the runtime image
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o oba-validator ./cmd/oba-validator

# Stage 2: minimal runtime
FROM alpine:3

# HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates.
RUN apk add --no-cache ca-certificates

WORKDIR /app
COPY --from=builder /build/oba-validator /app/oba-validator
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

# entrypoint.sh base64-decodes its argument into the config JSON before invoking
# the validator. This is required because a Render one-off job's startCommand is
# split on whitespace and passed as argv (no shell), so the JSON — which has
# spaces and special characters — must be base64-encoded by the caller:
# `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first
# token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying
# to Render" in the README. No API key is baked in — keys are per-server and
# travel in the config.
ENTRYPOINT ["/app/entrypoint.sh"]
CMD []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a non-root user for runtime security.

The container runs as root, which increases the attack surface if the validator processes untrusted input or has vulnerabilities. Since one-off jobs accept base64-encoded config from external callers, running as a non-root user follows the principle of least privilege.

🔒 Proposed fix to add a non-root user
 # Stage 2: minimal runtime
 FROM alpine:3
 
 # HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates.
 RUN apk add --no-cache ca-certificates
 
+# Run as non-root user
+RUN addgroup -S appgroup && adduser -S appuser -G appgroup
+
 WORKDIR /app
 COPY --from=builder /build/oba-validator /app/oba-validator
 COPY entrypoint.sh /app/entrypoint.sh
 RUN chmod +x /app/entrypoint.sh
 
+USER appuser
+
 # entrypoint.sh base64-decodes its argument into the config JSON before invoking
 # the validator. This is required because a Render one-off job's startCommand is
 # split on whitespace and passed as argv (no shell), so the JSON — which has
 # spaces and special characters — must be base64-encoded by the caller:
 # `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first
 # token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying
 # to Render" in the README. No API key is baked in — keys are per-server and
 # travel in the config.
 ENTRYPOINT ["/app/entrypoint.sh"]
 CMD []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Stage 1: build the Go binary
FROM golang:1-alpine AS builder
WORKDIR /build
# Cache module downloads across builds
COPY go.mod go.sum ./
RUN go mod download
# Build a static binary (no cgo) for the runtime image
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o oba-validator ./cmd/oba-validator
# Stage 2: minimal runtime
FROM alpine:3
# HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates.
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /build/oba-validator /app/oba-validator
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# entrypoint.sh base64-decodes its argument into the config JSON before invoking
# the validator. This is required because a Render one-off job's startCommand is
# split on whitespace and passed as argv (no shell), so the JSON — which has
# spaces and special characters — must be base64-encoded by the caller:
# `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first
# token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying
# to Render" in the README. No API key is baked in — keys are per-server and
# travel in the config.
ENTRYPOINT ["/app/entrypoint.sh"]
CMD []
# Stage 1: build the Go binary
FROM golang:1-alpine AS builder
WORKDIR /build
# Cache module downloads across builds
COPY go.mod go.sum ./
RUN go mod download
# Build a static binary (no cgo) for the runtime image
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o oba-validator ./cmd/oba-validator
# Stage 2: minimal runtime
FROM alpine:3
# HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates.
RUN apk add --no-cache ca-certificates
# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /build/oba-validator /app/oba-validator
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
USER appuser
# entrypoint.sh base64-decodes its argument into the config JSON before invoking
# the validator. This is required because a Render one-off job's startCommand is
# split on whitespace and passed as argv (no shell), so the JSON — which has
# spaces and special characters — must be base64-encoded by the caller:
# `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first
# token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying
# to Render" in the README. No API key is baked in — keys are per-server and
# travel in the config.
ENTRYPOINT ["/app/entrypoint.sh"]
CMD []
🧰 Tools
🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 1 - 34, Add a non-root runtime user and switch to it
in the Dockerfile: create a user/group (e.g., obauser), chown /app and the
binary (/app/oba-validator and /app/entrypoint.sh) to that user, ensure
entrypoint.sh remains executable, and add USER obauser before ENTRYPOINT so the
container does not run as root; reference the Dockerfile stages that COPY
--from=builder /build/oba-validator and COPY entrypoint.sh /app/entrypoint.sh
and ensure permissions/ownership changes happen after those COPY steps.

aaronbrethorst and others added 2 commits May 25, 2026 10:28
Two parallel jobs on push-to-main and pull_request:

- test: gofmt check, go vet, `go mod tidy` cleanliness, `go mod verify`,
  `go test -race ./...` (the validator fans out concurrently, so the race
  detector is worth running), and shellcheck on entrypoint.sh.
- docker: build the deployment image and smoke-test it offline — base64 of
  "{}" exercises entrypoint.sh -> oba-validator -> config.Load and must exit 2.

The live integration test (OBA_VALIDATOR_LIVE) is intentionally not run in CI;
it depends on a real OBA server. Includes concurrency cancellation and per-job
timeouts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
actions/checkout@v4 and actions/setup-go@v5 run on Node.js 20, which GitHub
forces off by June 2, 2026. The v6 majors run on Node 24.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@aaronbrethorst aaronbrethorst merged commit cec626b into main May 25, 2026
4 checks passed
@aaronbrethorst aaronbrethorst deleted the dockerfile branch May 25, 2026 18:55
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.

1 participant