Skip to content

Commit f6573ff

Browse files
committed
add example
1 parent 26fb83b commit f6573ff

8 files changed

Lines changed: 311 additions & 0 deletions

File tree

.github/workflows/docker.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,37 @@ jobs:
136136
test -f .docker-action-smoke/spritesheet.png
137137
test -f .docker-action-smoke/spritesheet.json
138138
139+
- name: Build example project config
140+
if: startsWith(env.RELEASE_TAG, 'v')
141+
working-directory: example
142+
run: node scripts/build-sprite-config.mjs
143+
144+
- name: Smoke test example project with published Docker action
145+
if: startsWith(env.RELEASE_TAG, 'v')
146+
id: example_sprites
147+
uses: ./action
148+
with:
149+
config: example/sprite-sheet-helper.generated.json
150+
fail-on-warnings: "false"
151+
152+
- name: Save example action summary
153+
if: startsWith(env.RELEASE_TAG, 'v') && always()
154+
env:
155+
SUMMARY_JSON: ${{ steps.example_sprites.outputs['summary-json'] }}
156+
run: |
157+
mkdir -p example/dist
158+
node -e 'const fs = require("fs"); fs.writeFileSync("example/dist/sprite-sheet-helper-summary.json", `${process.env.SUMMARY_JSON || "{}"}\n`);'
159+
160+
- name: Verify example project output
161+
if: startsWith(env.RELEASE_TAG, 'v')
162+
run: |
163+
test -f example/sprite-sheet-helper.generated.json
164+
test -f example/dist/sprites/example/spritesheet.png
165+
test -f example/dist/sprites/example/spritesheet_normal.png
166+
test -f example/dist/sprites/example/spritesheet.json
167+
test -f example/dist/sprite-sheet-helper-summary.json
168+
find example/dist -maxdepth 5 -type f | sort
169+
139170
- name: Update floating action tags
140171
if: startsWith(env.RELEASE_TAG, 'v')
141172
env:
@@ -163,4 +194,6 @@ jobs:
163194
path: |
164195
.docker-smoke/
165196
.docker-action-smoke/
197+
example/dist/
198+
example/sprite-sheet-helper.generated.json
166199
retention-days: 3

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ The action also supports config-driven batches:
243243
244244
On release tags like `v0.4.0`, CI publishes Docker image tags `v0.4.0`, `v0.4`, `v0`, and `latest`, then updates the floating action refs `v0.4` and `v0` after the Docker action smoke test passes. Branch builds do not update published image tags or action refs.
245245

246+
### CI example project
247+
248+
See [`example/`](example/) for a standalone GitHub repository template that discovers every model in a `models/` folder, runs the Docker Action on push to `main`, and uploads generated sprite artifacts.
249+
246250
### Example output — `--format love2d`
247251

248252
```text
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Generate Sprite Assets
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths:
7+
- "models/**"
8+
- "scripts/build-sprite-config.mjs"
9+
- ".github/workflows/generate-sprites.yml"
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
sprites:
17+
name: Generate sprites from models
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 90
20+
21+
env:
22+
SPRITE_FORMAT: spritesheet
23+
SPRITE_WORKFLOW: topdown-4dir
24+
SPRITE_FRAMES: "4"
25+
SPRITE_FPS: "10"
26+
SPRITE_WIDTH: "64"
27+
SPRITE_HEIGHT: "64"
28+
SPRITE_NORMAL_MAP: "true"
29+
SPRITE_ATLAS_LAYOUT: rows
30+
SPRITE_ATLAS_PADDING: "0"
31+
SPRITE_ATLAS_BLEED: "0"
32+
SPRITE_ATLAS_SCALE: "1"
33+
SPRITE_MAX_ATLAS_SIZE: "2048"
34+
SPRITE_MULTI_PAGE: "false"
35+
36+
steps:
37+
- name: Checkout
38+
uses: actions/checkout@v6
39+
40+
- name: Set up Node.js
41+
uses: actions/setup-node@v6
42+
with:
43+
node-version: 22
44+
45+
- name: Build Sprite Sheet Helper config
46+
id: config
47+
run: node scripts/build-sprite-config.mjs
48+
49+
- name: Generate sprites
50+
if: steps.config.outputs['has-jobs'] == 'true'
51+
id: sprites
52+
uses: Kyonru/sprite-sheet-helper/action@v0
53+
with:
54+
config: sprite-sheet-helper.generated.json
55+
fail-on-warnings: "false"
56+
57+
- name: Save run summary
58+
if: always() && steps.config.outputs['has-jobs'] == 'true'
59+
env:
60+
SUMMARY_JSON: ${{ steps.sprites.outputs['summary-json'] }}
61+
run: |
62+
mkdir -p dist
63+
node -e 'const fs = require("fs"); fs.writeFileSync("dist/sprite-sheet-helper-summary.json", `${process.env.SUMMARY_JSON || "{}"}\n`);'
64+
65+
- name: List generated files
66+
if: always() && steps.config.outputs['has-jobs'] == 'true'
67+
run: find dist -maxdepth 5 -type f | sort
68+
69+
- name: Upload sprite assets
70+
if: always() && steps.config.outputs['has-jobs'] == 'true'
71+
uses: actions/upload-artifact@v6
72+
with:
73+
name: sprite-assets
74+
path: |
75+
dist/sprites/
76+
dist/sprite-sheet-helper-summary.json
77+
sprite-sheet-helper.generated.json
78+
retention-days: 14
79+
80+
- name: No models found
81+
if: steps.config.outputs['has-jobs'] != 'true'
82+
run: echo "No supported model files were found under models/."

example/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
sprite-sheet-helper.generated.json

example/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Sprite Sheet Helper CI Example
2+
3+
This is a tiny repository template for generating sprite assets from every model in `models/` with the Sprite Sheet Helper GitHub Action.
4+
5+
Copy this `example/` folder into a new GitHub repository, add your `.fbx`, `.glb`, `.gltf`, or `.obj` files to `models/`, and push to `main`. The workflow will discover every model, generate a batch config, run the Action, and upload the generated sprites as a workflow artifact.
6+
7+
## What It Does
8+
9+
- Runs on every push to `main`.
10+
- Finds all supported model files under `models/`.
11+
- Generates `sprite-sheet-helper.generated.json`.
12+
- Runs `Kyonru/sprite-sheet-helper/action@v0`.
13+
- Writes output under `dist/sprites/<model-name>/`.
14+
- Uploads the generated sprites and run summary as the `sprite-assets` artifact.
15+
16+
The included `models/example.fbx` is just a smoke-test model so the template works immediately.
17+
18+
This template is also used by Sprite Sheet Helper's release pipeline before the floating Action refs are updated, so regressions in the published Docker Action path should fail the release smoke test.
19+
20+
## Customize Defaults
21+
22+
Edit `.github/workflows/generate-sprites.yml` and adjust the `SPRITE_*` environment values:
23+
24+
```yaml
25+
env:
26+
SPRITE_FORMAT: spritesheet
27+
SPRITE_WORKFLOW: topdown-4dir
28+
SPRITE_FRAMES: "4"
29+
SPRITE_FPS: "10"
30+
SPRITE_WIDTH: "64"
31+
SPRITE_HEIGHT: "64"
32+
SPRITE_NORMAL_MAP: "true"
33+
```
34+
35+
Useful formats include `spritesheet`, `love2d-lua`, `bevy`, `phaser`, `godot`, `pygame`, `raylib`, and `unity`.
36+
37+
## Folder Layout
38+
39+
```text
40+
.
41+
.github/workflows/generate-sprites.yml
42+
models/
43+
example.fbx
44+
scripts/
45+
build-sprite-config.mjs
46+
```
47+
48+
Generated files are ignored locally by `.gitignore`, but uploaded by CI as artifacts.
49+
50+
## Local Dry Run
51+
52+
You can inspect the generated config without running the Action:
53+
54+
```bash
55+
node scripts/build-sprite-config.mjs
56+
cat sprite-sheet-helper.generated.json
57+
```

example/models/example.fbx

229 KB
Binary file not shown.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { appendFileSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
2+
import { basename, extname, join, relative, sep } from "node:path";
3+
4+
const root = process.cwd();
5+
const modelsDir = join(root, "models");
6+
const configPath = join(root, "sprite-sheet-helper.generated.json");
7+
const supportedExtensions = new Set([".fbx", ".glb", ".gltf", ".obj"]);
8+
9+
function envString(name, fallback) {
10+
const value = process.env[name];
11+
return value === undefined || value === "" ? fallback : value;
12+
}
13+
14+
function envNumber(name, fallback) {
15+
const value = envString(name, String(fallback));
16+
const parsed = Number(value);
17+
if (!Number.isFinite(parsed)) {
18+
throw new Error(`${name} must be a number. Received: ${value}`);
19+
}
20+
return parsed;
21+
}
22+
23+
function envInteger(name, fallback) {
24+
const value = envNumber(name, fallback);
25+
if (!Number.isInteger(value)) {
26+
throw new Error(`${name} must be an integer. Received: ${value}`);
27+
}
28+
return value;
29+
}
30+
31+
function envBoolean(name, fallback) {
32+
const value = envString(name, fallback ? "true" : "false").toLowerCase();
33+
if (["true", "1", "yes", "on"].includes(value)) return true;
34+
if (["false", "0", "no", "off"].includes(value)) return false;
35+
throw new Error(`${name} must be true or false. Received: ${value}`);
36+
}
37+
38+
function toRepoPath(path) {
39+
return relative(root, path).split(sep).join("/");
40+
}
41+
42+
function toModelPath(path) {
43+
return relative(modelsDir, path).split(sep).join("/");
44+
}
45+
46+
function slugify(path, usedIds) {
47+
const withoutExtension = toModelPath(path).replace(/\.[^.]+$/, "");
48+
const base =
49+
withoutExtension
50+
.toLowerCase()
51+
.replace(/[^a-z0-9]+/g, "-")
52+
.replace(/^-+|-+$/g, "") || basename(path, extname(path)).toLowerCase();
53+
54+
let id = base;
55+
let suffix = 2;
56+
while (usedIds.has(id)) {
57+
id = `${base}-${suffix}`;
58+
suffix += 1;
59+
}
60+
usedIds.add(id);
61+
return id;
62+
}
63+
64+
function walkModels(dir) {
65+
let entries;
66+
try {
67+
entries = readdirSync(dir, { withFileTypes: true });
68+
} catch (error) {
69+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
70+
return [];
71+
}
72+
throw error;
73+
}
74+
75+
const files = [];
76+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
77+
const path = join(dir, entry.name);
78+
if (entry.isDirectory()) {
79+
files.push(...walkModels(path));
80+
continue;
81+
}
82+
if (!entry.isFile()) continue;
83+
if (supportedExtensions.has(extname(entry.name).toLowerCase())) {
84+
files.push(path);
85+
}
86+
}
87+
return files;
88+
}
89+
90+
const modelFiles = walkModels(modelsDir);
91+
const usedIds = new Set();
92+
const jobs = modelFiles.map((path) => {
93+
const id = slugify(path, usedIds);
94+
return {
95+
id,
96+
input: toRepoPath(path),
97+
output: `dist/sprites/${id}`,
98+
};
99+
});
100+
101+
const config = {
102+
defaults: {
103+
format: envString("SPRITE_FORMAT", "spritesheet"),
104+
workflow: envString("SPRITE_WORKFLOW", "topdown-4dir"),
105+
frames: envInteger("SPRITE_FRAMES", 4),
106+
fps: envInteger("SPRITE_FPS", 10),
107+
width: envInteger("SPRITE_WIDTH", 64),
108+
height: envInteger("SPRITE_HEIGHT", 64),
109+
normalMap: envBoolean("SPRITE_NORMAL_MAP", true),
110+
atlasLayout: envString("SPRITE_ATLAS_LAYOUT", "rows"),
111+
atlasPadding: envInteger("SPRITE_ATLAS_PADDING", 0),
112+
atlasBleed: envInteger("SPRITE_ATLAS_BLEED", 0),
113+
atlasScale: envNumber("SPRITE_ATLAS_SCALE", 1),
114+
maxAtlasSize: envInteger("SPRITE_MAX_ATLAS_SIZE", 2048),
115+
multiPage: envBoolean("SPRITE_MULTI_PAGE", false),
116+
},
117+
jobs,
118+
};
119+
120+
mkdirSync("dist", { recursive: true });
121+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
122+
123+
if (process.env.GITHUB_OUTPUT) {
124+
appendFileSync(process.env.GITHUB_OUTPUT, `has-jobs=${jobs.length > 0}\n`);
125+
appendFileSync(process.env.GITHUB_OUTPUT, `job-count=${jobs.length}\n`);
126+
}
127+
128+
console.log(`Found ${jobs.length} model${jobs.length === 1 ? "" : "s"}.`);
129+
for (const job of jobs) {
130+
console.log(`- ${job.id}: ${job.input} -> ${job.output}`);
131+
}

src/docs/ci.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ The action outputs `status`, `summary-json`, `files`, `warnings`, and `elapsed-m
6565

6666
Release tags like `v0.4.0` publish Docker image tags `v0.4.0`, `v0.4`, `v0`, and `latest`. After the Docker action smoke test passes, CI updates the floating action refs `v0.4` and `v0`, so workflows can pin either a patch release or the current major/minor line. Branch builds do not update published image tags or action refs.
6767

68+
The repository includes a standalone `example/` project template that discovers every model in a `models/` folder, runs the Docker Action on push to `main`, and uploads generated sprite artifacts.
69+
6870
## GitHub Pages Docs
6971

7072
The documentation site is built with Zensical from the root `docs` directory. Pages in `docs` mirror the app documentation from `src/docs` with symlinks.

0 commit comments

Comments
 (0)