Skip to content

Commit 2ebe1c0

Browse files
adds GH action
1 parent 6749403 commit 2ebe1c0

File tree

5 files changed

+209
-0
lines changed

5 files changed

+209
-0
lines changed

.github/workflows/build-apps.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Build Apps & Push to GHCR
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths:
7+
- 'apps/**'
8+
- '.github/workflows/build-apps.yml'
9+
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
packages: write
15+
16+
jobs:
17+
build:
18+
runs-on: ubuntu-latest
19+
strategy:
20+
matrix:
21+
app: [items]
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Set up QEMU
26+
uses: docker/setup-qemu-action@v3
27+
28+
- name: Set up Docker Buildx
29+
uses: docker/setup-buildx-action@v3
30+
31+
- name: Log in to GHCR
32+
uses: docker/login-action@v3
33+
with:
34+
registry: ghcr.io
35+
username: ${{ github.actor }}
36+
password: ${{ secrets.GITHUB_TOKEN }}
37+
38+
- name: Docker metadata
39+
id: meta
40+
uses: docker/metadata-action@v5
41+
with:
42+
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.app }}
43+
tags: |
44+
type=sha
45+
type=raw,value=latest
46+
labels: |
47+
org.opencontainers.image.source=${{ github.repository }}
48+
49+
- name: Debug tree
50+
run: |
51+
echo "Building ${{ matrix.app }}"
52+
ls -la apps/${{ matrix.app }}
53+
54+
- name: Build & Push
55+
uses: docker/build-push-action@v6
56+
with:
57+
context: ./apps/${{ matrix.app }}
58+
file: ./apps/${{ matrix.app }}/Dockerfile
59+
push: true
60+
tags: ${{ steps.meta.outputs.tags }}
61+
labels: ${{ steps.meta.outputs.labels }}
62+
cache-from: type=gha
63+
cache-to: type=gha,mode=max
64+
# platforms: linux/amd64,linux/arm64

apps/items/.dockerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.git
2+
.gitignore
3+
node_modules
4+
npm-debug.log
5+
Dockerfile*
6+
.dockerignore
7+
*.md
8+
**/.DS_Store

apps/items/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# syntax=docker/dockerfile:1
2+
FROM oven/bun:1.1-alpine
3+
WORKDIR /app
4+
COPY package.json bun.lockb* ./
5+
RUN bun install --production --frozen-lockfile || bun install --production
6+
COPY . .
7+
ENV PORT=8080
8+
EXPOSE 8080
9+
CMD ["bun", "run", "src/index.ts"]

apps/items/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "items",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "bun --hot run src/index.ts",
8+
"start": "bun run src/index.ts"
9+
},
10+
"dependencies": {}
11+
}

apps/items/src/index.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// src/index.ts
2+
type Item = { id: number; name: string };
3+
4+
let items: Item[] = [{ id: 1, name: "first" }];
5+
6+
const PORT = Number(process.env.PORT || 8080);
7+
const API_PREFIX = "/v1";
8+
9+
function json(data: unknown, init: ResponseInit = {}) {
10+
return new Response(JSON.stringify(data), {
11+
headers: { "content-type": "application/json" },
12+
...init,
13+
});
14+
}
15+
16+
function notFound() {
17+
return json({ error: "not found" }, { status: 404 });
18+
}
19+
20+
const openapi = {
21+
openapi: "3.0.3",
22+
info: { title: "items API", version: "1.0.0", description: "Simple items service built with Bun" },
23+
servers: [{ url: API_PREFIX }],
24+
paths: {
25+
"/health": {
26+
get: {
27+
summary: "Health check",
28+
responses: {
29+
"200": {
30+
description: "OK",
31+
content: { "application/json": { schema: { type: "object", properties: { status: { type: "string" } } } } },
32+
},
33+
},
34+
},
35+
},
36+
"/items": {
37+
get: {
38+
summary: "List items",
39+
responses: {
40+
"200": {
41+
description: "Array of items",
42+
content: { "application/json": { schema: { type: "object", properties: { items: { type: "array", items: { $ref: "#/components/schemas/Item" } } } } } },
43+
},
44+
},
45+
},
46+
post: {
47+
summary: "Create item",
48+
requestBody: {
49+
required: true,
50+
content: { "application/json": { schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } } },
51+
},
52+
responses: {
53+
"201": { description: "Created", content: { "application/json": { schema: { $ref: "#/components/schemas/Item" } } } },
54+
"400": { description: "Invalid input" },
55+
},
56+
},
57+
},
58+
},
59+
components: {
60+
schemas: { Item: { type: "object", properties: { id: { type: "integer" }, name: { type: "string" } }, required: ["id", "name"] } },
61+
},
62+
} as const;
63+
64+
const swaggerHtml = `<!doctype html>
65+
<html>
66+
<head>
67+
<meta charset="utf-8"/>
68+
<title>items API – Swagger UI</title>
69+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
70+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
71+
</head>
72+
<body>
73+
<div id="swagger-ui"></div>
74+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
75+
<script>
76+
window.onload = () => {
77+
window.ui = SwaggerUIBundle({ url: '${API_PREFIX}/openapi.json', dom_id: '#swagger-ui' });
78+
};
79+
</script>
80+
</body>
81+
</html>`;
82+
83+
const swaggerResponse = new Response(swaggerHtml, { headers: { "content-type": "text/html; charset=utf-8" } });
84+
85+
async function handle(req: Request): Promise<Response> {
86+
const url = new URL(req.url);
87+
let path = url.pathname;
88+
89+
// Swagger UI & OpenAPI JSON
90+
if (path === "/docs" && req.method === "GET") return swaggerResponse;
91+
if (path === `${API_PREFIX}/openapi.json` && req.method === "GET") return json(openapi);
92+
93+
if (!path.startsWith(API_PREFIX)) return notFound();
94+
path = path.slice(API_PREFIX.length) || "/";
95+
96+
if (path === "/health" && req.method === "GET") return json({ status: "ok" });
97+
if (path === "/items" && req.method === "GET") return json({ items });
98+
99+
if (path === "/items" && req.method === "POST") {
100+
try {
101+
const body = (await req.json()) as Partial<Item>;
102+
if (!body?.name || typeof body.name !== "string") return json({ error: "name required" }, { status: 400 });
103+
const nextId = (items.at(-1)?.id ?? 0) + 1;
104+
const item = { id: nextId, name: body.name };
105+
items.push(item);
106+
return json(item, { status: 201 });
107+
} catch {
108+
return json({ error: "invalid json" }, { status: 400 });
109+
}
110+
}
111+
112+
return notFound();
113+
}
114+
115+
const server = Bun.serve({ port: PORT, fetch: handle });
116+
console.log(`items service listening on http://localhost:${server.port}`);
117+
console.log(`Swagger UI: http://localhost:${server.port}/docs`);

0 commit comments

Comments
 (0)