Skip to content
Merged
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
60 changes: 60 additions & 0 deletions .github/actions/build-and-serve/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Build and serve an example
description: >
Builds one example with nixpacks, runs it with PORT injected, and waits for a
real HTTP response. Shared by both matrices so the two groups are checked
identically.

inputs:
example:
description: Path of the example directory
required: true
port:
description: Port the app listens on
required: true

runs:
using: composite
steps:
- name: Install nixpacks
shell: bash
run: curl -fsSL https://nixpacks.com/install.sh | bash

- name: Build ${{ inputs.example }}
shell: bash
run: nixpacks build "${{ inputs.example }}" --name example:ci

- name: Serve a request
shell: bash
env:
PORT: ${{ inputs.port }}
run: |
set -euo pipefail

docker run -d --name example \
-e PORT="${PORT}" -p "8099:${PORT}" example:ci

# Compiled and JVM apps take noticeably longer to become ready than
# interpreted ones, so this polls rather than sleeping a fixed time.
for attempt in $(seq 1 90); do
if curl -sf -o /tmp/body.txt "http://127.0.0.1:8099/"; then
echo "responded after ${attempt}s:"
head -c 200 /tmp/body.txt
exit 0
fi
# Failing fast on a dead container beats waiting out the full timeout.
if [ "$(docker inspect -f '{{.State.Status}}' example)" = "exited" ]; then
echo "::error::container exited before serving a request"
docker logs example
exit 1
fi
sleep 1
done

echo "::error::no response after 90s"
docker logs example
exit 1

- name: Container logs on failure
if: failure()
shell: bash
run: docker logs example || true
137 changes: 137 additions & 0 deletions .github/workflows/starters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Language starters

# The minimal per-language starters under examples/starters are advertised as
# "fork it, connect it to Temps, deploy". That claim is only true if something
# checks it, so each is built the way Temps builds it — with nixpacks — and
# then asked to serve a request.
#
# A build that succeeds but produces a container which never answers is the
# failure this catches, and the one that is invisible without it.

on:
push:
branches: [main]
paths: ["examples/starters/**", ".github/workflows/starters.yml", ".github/actions/**"]
pull_request:
paths: ["examples/starters/**", ".github/workflows/starters.yml", ".github/actions/**"]
# Base images and package registries move underneath these examples even
# when nobody touches the repository.
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:

concurrency:
group: examples-${{ github.ref }}
cancel-in-progress: true

jobs:
# Examples nixpacks can build and run today. Regressions here are real.
supported:
name: ${{ matrix.example }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- { example: examples/starters/nodejs/express, port: 3000 }
- { example: examples/starters/nodejs/fastify, port: 3000 }
- { example: examples/starters/nodejs/nestjs, port: 3000 }
- { example: examples/starters/nextjs/app-router, port: 3000 }
- { example: examples/starters/vite/react, port: 3000 }
- { example: examples/starters/sveltekit, port: 3000 }
- { example: examples/starters/python/flask, port: 8000 }
- { example: examples/starters/python/fastapi, port: 8000 }
- { example: examples/starters/rust/actix, port: 3000 }
- { example: examples/starters/php/vanilla, port: 3000 }
- { example: examples/starters/php/laravel, port: 3000 }
- { example: examples/starters/elixir/phoenix, port: 4000 }
- { example: examples/starters/dockerfile, port: 3000 }
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: ./.github/actions/build-and-serve
with:
example: ${{ matrix.example }}
port: ${{ matrix.port }}

# Starters that are correct but which nixpacks cannot currently build.
#
# These ran as a 13-job matrix with continue-on-error, which does not stop
# GitHub painting each one red — a passing PR looked like a broken one. They
# are one always-green job now, reporting to the run summary instead.
#
# The point is not to hide them. It is to notice the day nixpacks catches up:
# any starter that starts building is called out as news at the top of the
# summary, because that is the signal worth acting on.
nixpacks-gaps:
name: Known nixpacks gaps (report only)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3

- name: Install nixpacks
run: curl -fsSL https://nixpacks.com/install.sh | bash

- name: Probe
run: |
set -uo pipefail

# starter|reason it is expected to fail
GAPS=(
"go/net-http|go.mod declares go 1.24; Nix ships an older Go"
"go/gin|go.mod declares go 1.24; Nix ships an older Go"
"dotnet/web|Nix provides dotnet-sdk 6.0.413 for a net9.0 project"
"astro|Nix npm is older than the required npm >=9.6.5"
"nuxt|Nix npm is older than the required npm >=9.6.5"
"swift/vapor|swift: command not found"
"deno|no build plan for a bare main.ts"
"java/spring-boot|no build plan for build.gradle without a wrapper"
"bun/bun-server|bun: not found"
"bun/elysia|bun: not found"
"nodejs/hono|bun: not found"
"python/django|container exits at startup"
"ruby/rails|build fails under the generated plan"
)

fixed=()
still=()

for entry in "${GAPS[@]}"; do
starter="${entry%%|*}"
reason="${entry#*|}"
if nixpacks build "examples/starters/${starter}" --name gap:ci >/dev/null 2>&1; then
fixed+=("${starter}")
else
still+=("${starter}|${reason}")
fi
done

{
echo "## Known nixpacks gaps"
echo
if [ "${#fixed[@]}" -gt 0 ]; then
echo "### :tada: These now build — nixpacks has caught up"
echo
echo "Move them into the \`supported\` matrix and delete them from this list."
echo
for s in "${fixed[@]}"; do echo "- \`${s}\`"; done
echo
fi
echo "### Still failing (${#still[@]} of ${#GAPS[@]})"
echo
echo "These are correct applications. They build and serve with a current"
echo "toolchain; nixpacks resolves runtimes from the Nix package set, which lags."
echo
echo "| Starter | Why |"
echo "|---|---|"
for entry in "${still[@]}"; do
echo "| \`${entry%%|*}\` | ${entry#*|} |"
done
} >> "$GITHUB_STEP_SUMMARY"

# Always green: a gap that is already known and documented is not a
# regression, and a red check here would train people to ignore it.
echo "${#fixed[@]} now building, ${#still[@]} still failing — see the run summary"
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ Production-ready example applications showcasing [Temps](https://temps.sh) SDK i
| [docker/rust-axum](./examples/docker/rust-axum) | Rust Axum, PostgreSQL | Notes/snippets API |
| [docker/java-spring](./examples/docker/java-spring) | Spring Boot 3.5, PostgreSQL, Redis | Contacts API with JPA |

### Language starters

Minimal one-file-per-language apps — the smallest thing that builds and serves
a request. See [examples/starters](./examples/starters) for the full list and
for which ones nixpacks can currently build.

| Starter | Stack |
|---------|-------|
| [starters/nodejs](./examples/starters/nodejs) | Express, Fastify, Hono, NestJS |
| [starters/python](./examples/starters/python) | Flask, FastAPI, Django |
| [starters/go](./examples/starters/go) | net/http, Gin |
| [starters/ruby](./examples/starters/ruby) | Rails 8 |
| [starters/php](./examples/starters/php) | Plain PHP, Laravel 12 |
| [starters/java](./examples/starters/java), [dotnet](./examples/starters/dotnet), [elixir](./examples/starters/elixir), [swift](./examples/starters/swift), [rust](./examples/starters/rust), [deno](./examples/starters/deno), [bun](./examples/starters/bun) | one minimal app each |

## Getting Started

Each example lives in its own directory under `examples/` with its own README and setup instructions.
Expand Down
67 changes: 67 additions & 0 deletions examples/starters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Language starters

Minimal, single-purpose applications — one per language or runtime. Each is the
smallest thing that builds and answers an HTTP request, so it is a starting
point rather than a showcase. For richer, production-shaped examples see the
top level of [`examples/`](../).

Every starter listens on `$PORT` and binds `0.0.0.0`, which is what makes it
reachable once deployed.

| Starter | Stack |
|---|---|
| [nodejs/express](./nodejs/express) | Express (npm) |
| [nodejs/fastify](./nodejs/fastify) | Fastify (pnpm) |
| [nodejs/hono](./nodejs/hono) | Hono (Bun) |
| [nodejs/nestjs](./nodejs/nestjs) | NestJS |
| [nextjs/app-router](./nextjs/app-router) | Next.js App Router |
| [bun/bun-server](./bun/bun-server) | Bun native HTTP server |
| [bun/elysia](./bun/elysia) | Elysia |
| [deno](./deno) | Deno |
| [vite/react](./vite/react) | Vite + React (static) |
| [astro](./astro) | Astro (node adapter) |
| [nuxt](./nuxt) | Nuxt 3 |
| [sveltekit](./sveltekit) | SvelteKit (node adapter) |
| [python/flask](./python/flask) | Flask + Gunicorn |
| [python/fastapi](./python/fastapi) | FastAPI |
| [python/django](./python/django) | Django + WhiteNoise |
| [go/net-http](./go/net-http) | Go `net/http` |
| [go/gin](./go/gin) | Gin |
| [rust/actix](./rust/actix) | Actix Web |
| [ruby/rails](./ruby/rails) | Rails 8 (API-only) |
| [php/vanilla](./php/vanilla) | Plain PHP |
| [php/laravel](./php/laravel) | Laravel 12 |
| [java/spring-boot](./java/spring-boot) | Spring Boot 3 |
| [dotnet/web](./dotnet/web) | ASP.NET Core minimal API |
| [elixir/phoenix](./elixir/phoenix) | Phoenix 1.7 (API-only) |
| [swift/vapor](./swift/vapor) | Vapor 4 |
| [dockerfile](./dockerfile) | Custom multi-stage Dockerfile |

## Verification

`.github/workflows/starters.yml` builds every starter with nixpacks and waits
for a real HTTP response — on push, on pull requests, and weekly, because base
images and package registries move even when this directory does not.

The workflow has two jobs. `supported` is blocking. `nixpacks-gaps` is a
report: it probes the starters below and always exits green, writing the
outcome to the run summary — including calling out any that have *started*
building, which is the signal worth acting on.

Those starters are correct and build fine with a current toolchain; nixpacks
resolves runtimes from the Nix package set, which lags upstream. As of
2026-08-01:

| Starter | Why nixpacks cannot build it |
|---|---|
| `go/net-http`, `go/gin` | `go.mod` declares `go 1.24`; Nix ships an older Go and the toolchain auto-download fails |
| `dotnet/web` | Nix provides dotnet-sdk 6.0.413 against a `net9.0` project |
| `astro`, `nuxt` | Nix npm is older than the `npm >=9.6.5` these require |
| `swift/vapor` | `swift: command not found` |
| `deno` | no build plan for a bare `main.ts` |
| `java/spring-boot` | no build plan for `build.gradle` without a wrapper |
| `bun/*`, `nodejs/hono` | `bun: not found` |
| `python/django` | container exits at startup under the generated start command |

They are kept visible rather than deleted, so the day nixpacks catches up the
CI tells us.
13 changes: 13 additions & 0 deletions examples/starters/astro/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import node from '@astrojs/node';
import { defineConfig } from 'astro/config';

// `output: 'server'` needs an adapter; without one `astro build` fails.
// The node adapter emits dist/server/entry.mjs, which is what `start` runs.
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
server: {
host: '0.0.0.0',
port: Number(process.env.PORT ?? 4321),
},
});
15 changes: 15 additions & 0 deletions examples/starters/astro/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "temps-astro-example",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"start": "node ./dist/server/entry.mjs"
},
"dependencies": {
"@astrojs/node": "^9.0.0",
"astro": "^5.1.0"
}
}
14 changes: 14 additions & 0 deletions examples/starters/astro/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
const message = "Hello from Astro on Temps!";
---

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro on Temps</title>
</head>
<body>
<h1>{message}</h1>
</body>
</html>
20 changes: 20 additions & 0 deletions examples/starters/bun/bun-server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const port = parseInt(process.env.PORT || '3000');

const server = Bun.serve({
port,
fetch(req) {
const url = new URL(req.url);

if (url.pathname === '/') {
return Response.json({ message: 'Hello from Bun on Temps!' });
}

if (url.pathname === '/health') {
return Response.json({ status: 'ok' });
}

return new Response('Not Found', { status: 404 });
},
});

console.log(`Listening on port ${server.port}`);
7 changes: 7 additions & 0 deletions examples/starters/bun/bun-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "temps-bun-server-example",
"version": "1.0.0",
"scripts": {
"start": "bun run index.ts"
}
}
11 changes: 11 additions & 0 deletions examples/starters/bun/elysia/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "temps-elysia-example",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "bun run src/index.ts"
},
"dependencies": {
"elysia": "^1.3.2"
}
}
10 changes: 10 additions & 0 deletions examples/starters/bun/elysia/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Elysia, t } from 'elysia';

const port = parseInt(process.env.PORT || '3000');

const app = new Elysia()
.get('/', () => ({ message: 'Hello from Elysia on Temps!' }))
.get('/health', () => ({ status: 'ok' }))
.listen(port);

console.log(`Listening on port ${app.server?.port}`);
Loading
Loading