Serve Woodpecker pipeline configs from the engine (/build/config)
This commit is contained in:
@@ -8,6 +8,22 @@ Rails.application.config.to_prepare do
|
||||
c.application_token_source = :database
|
||||
c.application_token_owner_class = "AdminUser"
|
||||
|
||||
# Woodpecker configuration extension (/build/config): a kiszolgált
|
||||
# platformok és builder image-eik. Image-bump = itt egy sor, deployjal
|
||||
# minden repóra kigördül. A tic80 akkor kerülhet be, ha elkészült a
|
||||
# tic80-builder image (lua+luacheck+ldoc toolchain) a tic80-tools repóban.
|
||||
c.ci_platforms = {
|
||||
"godot" => { builder: "git.teletypegames.org/internal/godot-builder:4.6" },
|
||||
"phaser" => { builder: "git.teletypegames.org/internal/phaser-builder:latest" },
|
||||
"love" => { builder: "git.teletypegames.org/internal/love-builder:latest" },
|
||||
"bevy" => { builder: "git.teletypegames.org/internal/bevy-builder:latest" },
|
||||
"c64" => { builder: "git.teletypegames.org/internal/c64-builder:latest" },
|
||||
"ebitengine" => { builder: "git.teletypegames.org/internal/ebitengine-builder:latest" }
|
||||
# "tic80" => { builder: "git.teletypegames.org/internal/tic80-builder:latest",
|
||||
# exporter: "git.teletypegames.org/internal/tic80pro:latest" }
|
||||
}
|
||||
c.ci_extension_public_key_url = "https://ci.teletypegames.org/api/signature/public-key"
|
||||
|
||||
c.image_owners = [
|
||||
{
|
||||
label: "member",
|
||||
|
||||
@@ -258,6 +258,33 @@ accepts both:
|
||||
When switching to `:database`, create the tokens and move your pipelines to
|
||||
them first — the flip invalidates the shared secret immediately.
|
||||
|
||||
## CI pipeline configs (Woodpecker)
|
||||
|
||||
WarpEngine can act as a [Woodpecker configuration extension](https://woodpecker-ci.org/docs/usage/extensions/configuration-extension):
|
||||
instead of a copy-pasted `.woodpecker.yaml` in every game repo, the repo holds a
|
||||
one-line marker and the engine serves the full per-platform pipeline
|
||||
(version → build → upload → publish, calling `/build/upload` + `/build/publish`
|
||||
with the `application_token` Woodpecker secret):
|
||||
|
||||
```yaml
|
||||
# .woodpecker.yaml in a game repo
|
||||
platform: godot
|
||||
```
|
||||
|
||||
- `POST /build/config` — the extension endpoint Woodpecker calls on every
|
||||
pipeline start (httpsig/ed25519-signed request, verified against
|
||||
`ci_extension_public_key(_url)`). Non-marker configs get a `204` so the
|
||||
repo's own YAML keeps running — opt-in migration, and putting a full
|
||||
pipeline back into the repo is the opt-out.
|
||||
- `GET /build/config?platform=godot` — renders the same pipeline as a preview.
|
||||
|
||||
Configuration: `ci_platforms` maps platform names to builder images
|
||||
(`{ "godot" => { builder: "..." }, "tic80" => { builder: ..., exporter: ... } }`);
|
||||
an empty map (default) disables the feature. Set the Woodpecker side with
|
||||
`WOODPECKER_CONFIG_EXTENSION_ENDPOINT=https://your-host/build/config` (or
|
||||
per-repo in Settings → Extensions). Templates live in
|
||||
`lib/warp_engine/ci_templates/*.yaml.erb`.
|
||||
|
||||
## Public API
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
module WarpEngine
|
||||
module Build
|
||||
# Woodpecker configuration-extension endpoint: a CI-szerver pipeline-indításkor
|
||||
# POST-olja a repo marker-fájlját, és a platformhoz tartozó teljes pipeline
|
||||
# YAML-t kapja vissza. A GET ugyanazt rendereli previewként.
|
||||
class ConfigsController < ApiController
|
||||
resource_description do
|
||||
short "Woodpecker CI pipeline configs"
|
||||
end
|
||||
|
||||
api :GET, "/build/config", "Preview the generated pipeline config for a platform"
|
||||
param :platform, String, required: true, desc: "Platform (a configured ci_platforms key, e.g. tic80)"
|
||||
param :name, String, required: false, desc: "Software name substituted into the pipeline (default: example)"
|
||||
returns code: 200, desc: "Pipeline YAML (text/yaml)"
|
||||
error code: 404, desc: "Unknown platform"
|
||||
def show
|
||||
yaml = render_config(platform: params[:platform], name: params[:name].presence || "example")
|
||||
return render json: { error: "Unknown platform" }, status: :not_found if yaml.nil?
|
||||
|
||||
render plain: yaml, content_type: "text/yaml"
|
||||
end
|
||||
|
||||
api :POST, "/build/config", "Woodpecker configuration extension endpoint"
|
||||
description <<~DESC
|
||||
Called by the Woodpecker server on every pipeline start (httpsig-signed request).
|
||||
If the repo's .woodpecker.yaml is a marker (has a `platform:` key), responds with
|
||||
the generated pipeline; otherwise responds 204 so the repo's own config runs.
|
||||
DESC
|
||||
returns code: 200, desc: %(JSON: {"configs": [{"name": ..., "data": "<pipeline YAML>"}]})
|
||||
returns code: 204, desc: "Not a marker config — keep the repo's own configuration"
|
||||
error code: 403, desc: "Missing or invalid request signature"
|
||||
error code: 422, desc: "Marker requests an unknown platform"
|
||||
def create
|
||||
unless WarpEngine::CiSignatureVerifier.new(request).valid?
|
||||
return render json: { error: "Invalid signature" }, status: :forbidden
|
||||
end
|
||||
|
||||
marker = find_marker
|
||||
return head :no_content if marker.nil?
|
||||
|
||||
platform = marker["platform"].to_s
|
||||
name = marker["name"].presence || repo_name
|
||||
yaml = render_config(platform: platform, name: name)
|
||||
if yaml.nil?
|
||||
return render json: { error: "Unknown platform: #{platform}" }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
render json: { configs: [ { name: platform, data: yaml } ] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_config(platform:, name:)
|
||||
WarpEngine::CiConfigService.new.render(
|
||||
platform: platform,
|
||||
name: name,
|
||||
update_server: WarpEngine.config.ci_update_server.presence || request.base_url
|
||||
)
|
||||
end
|
||||
|
||||
# Az első beküldött config, ami markernek parse-olható (Hash `platform` kulccsal).
|
||||
# A doksi szerint a kulcs "configuration", az example-config-service "configs"-ot
|
||||
# használ — mindkettőt elfogadjuk.
|
||||
def find_marker
|
||||
configs = params[:configuration].presence || params[:configs].presence || []
|
||||
configs.each do |config|
|
||||
data = config[:data].to_s
|
||||
parsed = begin
|
||||
YAML.safe_load(data)
|
||||
rescue Psych::Exception
|
||||
nil
|
||||
end
|
||||
return parsed if parsed.is_a?(Hash) && parsed.key?("platform")
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def repo_name
|
||||
params.dig(:repo, :name).to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
require "erb"
|
||||
|
||||
module WarpEngine
|
||||
# A /build/config platform-template-jeinek renderelése: a pipeline-logika
|
||||
# a lib/warp_engine/ci_templates/<platform>.yaml.erb fájlokban él, a
|
||||
# platformonkénti builder image-eket a WarpEngine.config.ci_platforms adja.
|
||||
class CiConfigService
|
||||
PLATFORM_FORMAT = /\A[a-z0-9_-]+\z/
|
||||
|
||||
# A renderelt pipeline YAML, vagy nil, ha a platform nem kiszolgált.
|
||||
def render(platform:, name:, update_server:)
|
||||
platform = platform.to_s
|
||||
return nil unless platform.match?(PLATFORM_FORMAT)
|
||||
|
||||
spec = platform_spec(platform)
|
||||
return nil if spec.nil?
|
||||
|
||||
path = templates_dir.join("#{platform}.yaml.erb")
|
||||
return nil unless path.exist?
|
||||
|
||||
ERB.new(path.read, trim_mode: "-").result_with_hash(
|
||||
name: name.to_s,
|
||||
update_server: update_server.to_s,
|
||||
builder: spec[:builder],
|
||||
exporter: spec[:exporter]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def platform_spec(platform)
|
||||
spec = WarpEngine.config.ci_platforms.stringify_keys[platform]
|
||||
spec&.symbolize_keys
|
||||
end
|
||||
|
||||
def templates_dir
|
||||
WarpEngine::Engine.root.join("lib", "warp_engine", "ci_templates")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
require "openssl"
|
||||
require "base64"
|
||||
require "net/http"
|
||||
|
||||
module WarpEngine
|
||||
# A Woodpecker configuration-extension kéréseinek httpsig-ellenőrzése
|
||||
# (draft-cavage http-signatures, ed25519). A szerver az aláírt headerek
|
||||
# listáját a Signature headerben küldi — tipikusan "(request-target) date".
|
||||
class CiSignatureVerifier
|
||||
SIGNATURE_PARAM = /(\w+)="([^"]*)"/
|
||||
|
||||
@key_cache = {}
|
||||
@key_mutex = Mutex.new
|
||||
|
||||
class << self
|
||||
# A letöltött kulcs process-szinten cache-elt (URL-enként).
|
||||
def fetch_public_key(url)
|
||||
@key_mutex.synchronize do
|
||||
@key_cache[url] ||= Net::HTTP.get(URI.parse(url))
|
||||
end
|
||||
end
|
||||
|
||||
def reset_key_cache!
|
||||
@key_mutex.synchronize { @key_cache = {} }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(request)
|
||||
@request = request
|
||||
end
|
||||
|
||||
def valid?
|
||||
pem = public_key_pem
|
||||
if pem.blank?
|
||||
Rails.logger.error("[CiSignatureVerifier] nincs ci_extension_public_key(_url) konfigurálva — kérés elutasítva")
|
||||
return false
|
||||
end
|
||||
|
||||
params = signature_params
|
||||
return false if params.nil? || params["signature"].blank?
|
||||
|
||||
signing_string = build_signing_string(params.fetch("headers", "date"))
|
||||
return false if signing_string.nil?
|
||||
|
||||
key = OpenSSL::PKey.read(pem)
|
||||
key.verify(nil, Base64.decode64(params["signature"]), signing_string)
|
||||
rescue OpenSSL::PKey::PKeyError, ArgumentError => e
|
||||
Rails.logger.error("[CiSignatureVerifier] #{e.class}: #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def public_key_pem
|
||||
config = WarpEngine.config
|
||||
return config.ci_extension_public_key if config.ci_extension_public_key.present?
|
||||
return nil if config.ci_extension_public_key_url.blank?
|
||||
|
||||
self.class.fetch_public_key(config.ci_extension_public_key_url)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[CiSignatureVerifier] kulcs-letöltés sikertelen: #{e.class}: #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
# A Signature header (vagy az "Authorization: Signature ..." forma) paraméterei.
|
||||
def signature_params
|
||||
header = @request.headers["Signature"].presence
|
||||
if header.nil?
|
||||
auth = @request.headers["Authorization"].to_s
|
||||
header = auth.delete_prefix("Signature ") if auth.start_with?("Signature ")
|
||||
end
|
||||
return nil if header.blank?
|
||||
|
||||
header.scan(SIGNATURE_PARAM).to_h
|
||||
end
|
||||
|
||||
def build_signing_string(headers_list)
|
||||
lines = headers_list.split(" ").map do |name|
|
||||
if name == "(request-target)"
|
||||
"(request-target): #{@request.request_method.downcase} #{@request.fullpath}"
|
||||
else
|
||||
value = @request.headers[name]
|
||||
return nil if value.nil?
|
||||
"#{name.downcase}: #{value}"
|
||||
end
|
||||
end
|
||||
lines.join("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,6 +10,8 @@ WarpEngine::Engine.routes.draw do
|
||||
|
||||
post "build/upload", to: "build/uploads#create"
|
||||
post "build/publish", to: "build/publish#create"
|
||||
get "build/config", to: "build/configs#show"
|
||||
post "build/config", to: "build/configs#create"
|
||||
|
||||
get "file/*path", to: "files#show", format: false
|
||||
end
|
||||
|
||||
@@ -17,6 +17,17 @@ Rails.application.config.to_prepare do
|
||||
# c.application_token_source = :database
|
||||
# c.application_token_owner_class = "AdminUser"
|
||||
|
||||
# Woodpecker configuration extension (/build/config): a kiszolgált
|
||||
# platformok builder image-ei és a CI-szerver aláíró kulcsa. Üres
|
||||
# ci_platforms (default) = a feature inaktív.
|
||||
# c.ci_platforms = {
|
||||
# "godot" => { builder: "registry.example/godot-builder:4.6" },
|
||||
# "tic80" => { builder: "registry.example/tic80-builder:1.0",
|
||||
# exporter: "registry.example/tic80pro:1.0" }
|
||||
# }
|
||||
# c.ci_extension_public_key_url = "https://ci.example.org/api/signature/public-key"
|
||||
# c.ci_update_server = nil # nil: a kérés base_url-je
|
||||
|
||||
# A /build/upload (és az admin file manager) méretplafonja bájtban (default: 500MB).
|
||||
# c.max_upload_size = 500 * 1024 * 1024
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Generált pipeline — WarpEngine /build/config (platform: bevy, name: <%= name %>)
|
||||
steps:
|
||||
- name: version
|
||||
image: alpine
|
||||
commands:
|
||||
- apk add --no-cache git jq
|
||||
- |
|
||||
if [ -f metadata.json ]; then
|
||||
VERSION=$(jq -r '.version' metadata.json)
|
||||
else
|
||||
VERSION=$(git rev-parse --short HEAD)
|
||||
fi
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
||||
VERSION="dev-$VERSION-$BRANCH"
|
||||
fi
|
||||
echo "VERSION is: $VERSION"
|
||||
echo $VERSION > .version
|
||||
|
||||
- name: build
|
||||
image: <%= builder %>
|
||||
pull: true
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
mkdir -p dist
|
||||
cargo build --release --target wasm32-unknown-unknown
|
||||
wasm-bindgen --target web --no-typescript \
|
||||
--out-dir dist --out-name game target/wasm32-unknown-unknown/release/<%= name %>.wasm
|
||||
echo "==> Downloading index.html"
|
||||
curl -sSL https://git.teletypegames.org/tools/bevy-tools/raw/branch/master/web/index.html -o dist/index.html
|
||||
echo "==> Packaging HTML/WASM for $VERSION"
|
||||
zip -r "<%= name %>-$VERSION.html.zip" -j dist/game_bg.wasm dist/game.js dist/index.html
|
||||
echo "==> Cleaning temporary files"
|
||||
rm -f dist/game_bg.wasm dist/game.js dist/index.html
|
||||
|
||||
- name: binaries
|
||||
image: <%= builder %>
|
||||
pull: true
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
# Native binaries. linux-x64: glibc build in the debian-based builder
|
||||
# image; win-x64: mingw-w64 cross-compile (x86_64-pc-windows-gnu).
|
||||
# Mac needs osxcross, it is not built here.
|
||||
# The zip gets the assets/ dir too if the project has one — bevy loads
|
||||
# it at runtime, it is not embedded in the binary.
|
||||
set -e
|
||||
pack_binary() {
|
||||
P_SLUG="$1"; P_BIN="$2"; P_NAME="$3"
|
||||
PKG_DIR="<%= name %>-$VERSION-$P_SLUG"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip"
|
||||
mkdir -p "$PKG_DIR"
|
||||
cp "$P_BIN" "$PKG_DIR/$P_NAME"
|
||||
chmod +x "$PKG_DIR/$P_NAME"
|
||||
if [ -d assets ]; then cp -r assets "$PKG_DIR/assets"; fi
|
||||
zip -qr "$PKG_DIR.zip" "$PKG_DIR"
|
||||
rm -rf "$PKG_DIR"
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
}
|
||||
echo "==> Building linux-x64 binary"
|
||||
cargo build --release
|
||||
pack_binary "linux-x64" "target/release/<%= name %>" "<%= name %>"
|
||||
echo "==> Building win-x64 binary"
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc \
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
pack_binary "win-x64" "target/x86_64-pc-windows-gnu/release/<%= name %>.exe" "<%= name %>.exe"
|
||||
|
||||
- name: upload
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
FILE="<%= name %>-$VERSION.html.zip"
|
||||
META_SRC="metadata.json"
|
||||
META_DST="<%= name %>-$VERSION.metadata.json"
|
||||
cp $META_SRC $META_DST
|
||||
BINS=""
|
||||
for slug in win-x64 linux-x64; do
|
||||
[ -f "<%= name %>-$VERSION-$slug.zip" ] && BINS="$BINS <%= name %>-$VERSION-$slug.zip"
|
||||
done
|
||||
for f in $FILE $META_DST $BINS; do
|
||||
curl -fsS -H "X-Update-Secret: $UPDATE_SECRET" \
|
||||
-F "file=@$f" \
|
||||
"$UPDATE_SERVER/build/upload?name=<%= name %>&version=$VERSION" || exit 1
|
||||
done
|
||||
|
||||
- name: publish
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
curl -fsS -X POST -H "X-Update-Secret: $UPDATE_SECRET" "$UPDATE_SERVER/build/publish?name=<%= name %>&platform=bevy&version=$VERSION"
|
||||
@@ -0,0 +1,59 @@
|
||||
# Generált pipeline — WarpEngine /build/config (platform: c64, name: <%= name %>)
|
||||
steps:
|
||||
- name: version
|
||||
image: alpine
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' metadata.json | head -n 1)
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "ERROR: no \"version\" field in metadata.json!"
|
||||
exit 1
|
||||
fi
|
||||
BRANCH=${CI_COMMIT_BRANCH:-${WOODPECKER_BRANCH}}
|
||||
BRANCH=$(echo "$BRANCH" | tr '/' '-')
|
||||
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ] && [ -n "$BRANCH" ]; then
|
||||
VERSION="dev-$VERSION-$BRANCH"
|
||||
fi
|
||||
echo "VERSION is: $VERSION"
|
||||
echo $VERSION > .version
|
||||
|
||||
- name: build
|
||||
image: <%= builder %>
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
acme -f cbm -o <%= name %>.prg main.asm
|
||||
echo "==> Creating versioned files for $VERSION"
|
||||
cp <%= name %>.prg <%= name %>-$VERSION.prg
|
||||
cp metadata.json <%= name %>-$VERSION.metadata.json
|
||||
ls -lh <%= name %>-$VERSION.*
|
||||
|
||||
- name: artifact
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Uploading artifacts for version $VERSION"
|
||||
for f in <%= name %>-$VERSION.prg <%= name %>-$VERSION.metadata.json; do
|
||||
curl -fsS -H "X-Update-Secret: $UPDATE_SECRET" \
|
||||
-F "file=@$f" \
|
||||
"$UPDATE_SERVER/build/upload?name=<%= name %>&version=$VERSION" || exit 1
|
||||
done
|
||||
|
||||
- name: publish
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Publishing version $VERSION"
|
||||
curl -fsS -X POST -H "X-Update-Secret: $UPDATE_SECRET" "$UPDATE_SERVER/build/publish?name=<%= name %>&platform=c64&version=$VERSION"
|
||||
@@ -0,0 +1,95 @@
|
||||
# Generált pipeline — WarpEngine /build/config (platform: ebitengine, name: <%= name %>)
|
||||
steps:
|
||||
- name: version
|
||||
image: alpine
|
||||
commands:
|
||||
- apk add --no-cache git jq
|
||||
- |
|
||||
if [ -f metadata.json ]; then
|
||||
VERSION=$(jq -r '.version' metadata.json)
|
||||
else
|
||||
VERSION=$(git rev-parse --short HEAD)
|
||||
fi
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
||||
VERSION="dev-$VERSION-$BRANCH"
|
||||
fi
|
||||
echo "VERSION is: $VERSION"
|
||||
echo $VERSION > .version
|
||||
|
||||
- name: build
|
||||
image: <%= builder %>
|
||||
pull: true
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
mkdir -p dist
|
||||
GOOS=js GOARCH=wasm go build -o dist/game.wasm .
|
||||
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" dist/wasm_exec.js
|
||||
echo "==> Downloading index.html"
|
||||
curl -sSL https://git.teletypegames.org/tools/ebitengine-tools/raw/branch/master/web/index.html -o dist/index.html
|
||||
echo "==> Packaging HTML/WASM for $VERSION"
|
||||
zip -r "<%= name %>-$VERSION.html.zip" -j dist/game.wasm dist/wasm_exec.js dist/index.html
|
||||
echo "==> Cleaning temporary files"
|
||||
rm -f dist/game.wasm dist/wasm_exec.js dist/index.html
|
||||
|
||||
- name: binaries
|
||||
image: <%= builder %>
|
||||
pull: true
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
# win-x86 / win-x64: pure Go cross-compile (Windowson nem kell cgo)
|
||||
# linux-x64: cgo build, linux/amd64 hoston fut (builder image, X11/GL dev libekkel)
|
||||
# belső helper: egy target buildje + zip egyetlen gyökérmappával
|
||||
# (a zipet unixos zip készíti, így a végrehajtási bit megmarad)
|
||||
binary_build() {
|
||||
B_GOOS="$1"; B_GOARCH="$2"; B_CGO="$3"; B_EXT="$4"; B_TARGET="$5"
|
||||
PKG_DIR="<%= name %>-$VERSION-$B_TARGET"
|
||||
echo "==> Building $PKG_DIR"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip"
|
||||
mkdir -p "$PKG_DIR"
|
||||
CGO_ENABLED=$B_CGO GOOS=$B_GOOS GOARCH=$B_GOARCH go build -o "$PKG_DIR/<%= name %>$B_EXT" .
|
||||
if [ -f LICENSE ]; then cp LICENSE "$PKG_DIR/"; fi
|
||||
if [ -f README.md ]; then cp README.md "$PKG_DIR/"; fi
|
||||
zip -r "$PKG_DIR.zip" "$PKG_DIR" >/dev/null
|
||||
rm -rf "$PKG_DIR"
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
}
|
||||
# a CI (linux builder) ezt a hármat építi:
|
||||
binary_build "windows" "386" "0" ".exe" "win-x86"
|
||||
binary_build "windows" "amd64" "0" ".exe" "win-x64"
|
||||
binary_build "linux" "amd64" "1" "" "linux-x64"
|
||||
|
||||
- name: artifact
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
FILE="<%= name %>-$VERSION.html.zip"
|
||||
META_SRC="metadata.json"
|
||||
META_DST="<%= name %>-$VERSION.metadata.json"
|
||||
BINS=$(ls <%= name %>-$VERSION-*.zip 2>/dev/null || true)
|
||||
cp $META_SRC $META_DST
|
||||
for f in $FILE $META_DST $BINS; do
|
||||
curl -fsS -H "X-Update-Secret: $UPDATE_SECRET" \
|
||||
-F "file=@$f" \
|
||||
"$UPDATE_SERVER/build/upload?name=<%= name %>&version=$VERSION" || exit 1
|
||||
done
|
||||
|
||||
- name: publish
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
curl -fsS -X POST -H "X-Update-Secret: $UPDATE_SECRET" "$UPDATE_SERVER/build/publish?name=<%= name %>&platform=ebitengine&version=$VERSION"
|
||||
@@ -0,0 +1,100 @@
|
||||
# Generált pipeline — WarpEngine /build/config (platform: godot, name: <%= name %>)
|
||||
steps:
|
||||
- name: version
|
||||
image: alpine
|
||||
commands:
|
||||
- apk add --no-cache git jq
|
||||
- |
|
||||
if [ -f metadata.json ]; then
|
||||
VERSION=$(jq -r '.version' metadata.json)
|
||||
else
|
||||
VERSION=$(git rev-parse --short HEAD)
|
||||
fi
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
||||
VERSION="dev-$VERSION-$BRANCH"
|
||||
fi
|
||||
echo "VERSION is: $VERSION"
|
||||
echo $VERSION > .version
|
||||
|
||||
- name: build
|
||||
image: <%= builder %>
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Importing project"
|
||||
godot --headless --import
|
||||
echo "==> Exporting web build (Web preset)"
|
||||
mkdir -p dist/web
|
||||
godot --headless --export-release "Web" dist/web/index.html
|
||||
echo "==> Packaging web build for $VERSION"
|
||||
(cd dist/web && zip -r "../../<%= name %>-$VERSION.html.zip" .)
|
||||
rm -rf dist/web
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
# win/linux target exportja + zip egyetlen gyökérmappával (embed_pck
|
||||
# miatt az export egyetlen futtatható fájl)
|
||||
binary_build() {
|
||||
B_PRESET="$1"; B_EXT="$2"; B_TARGET="$3"
|
||||
PKG_DIR="<%= name %>-$VERSION-$B_TARGET"
|
||||
echo "==> Building $PKG_DIR"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip"
|
||||
mkdir -p "$PKG_DIR"
|
||||
godot --headless --export-release "$B_PRESET" "$(pwd)/$PKG_DIR/<%= name %>$B_EXT"
|
||||
if [ -f LICENSE ]; then cp LICENSE "$PKG_DIR/"; fi
|
||||
if [ -f README.md ]; then cp README.md "$PKG_DIR/"; fi
|
||||
zip -r "$PKG_DIR.zip" "$PKG_DIR" >/dev/null
|
||||
rm -rf "$PKG_DIR"
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
}
|
||||
# mac: a Godot linuxról csak .zip-be tud macOS-t exportálni (benne a
|
||||
# .app), átcsomagoljuk a gyökérmappás konvencióra (zip -ry: exec bitek
|
||||
# és symlinkek megmaradnak)
|
||||
binary_build_mac() {
|
||||
B_PRESET="$1"; B_TARGET="$2"
|
||||
PKG_DIR="<%= name %>-$VERSION-$B_TARGET"
|
||||
echo "==> Building $PKG_DIR"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip"
|
||||
mkdir -p "$PKG_DIR"
|
||||
godot --headless --export-release "$B_PRESET" "$(pwd)/$PKG_DIR/<%= name %>-mac-tmp.zip"
|
||||
(cd "$PKG_DIR" && unzip -q "<%= name %>-mac-tmp.zip" && rm "<%= name %>-mac-tmp.zip")
|
||||
if [ -f LICENSE ]; then cp LICENSE "$PKG_DIR/"; fi
|
||||
if [ -f README.md ]; then cp README.md "$PKG_DIR/"; fi
|
||||
zip -ry "$PKG_DIR.zip" "$PKG_DIR" >/dev/null
|
||||
rm -rf "$PKG_DIR"
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
}
|
||||
binary_build "Windows x86" ".exe" "win-x86"
|
||||
binary_build "Windows x64" ".exe" "win-x64"
|
||||
binary_build "Linux x64" "" "linux-x64"
|
||||
binary_build_mac "Mac universal" "mac-universal"
|
||||
|
||||
- name: upload
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
cp metadata.json "<%= name %>-$VERSION.metadata.json"
|
||||
BINS=$(ls <%= name %>-$VERSION-*.zip 2>/dev/null || true)
|
||||
for f in "<%= name %>-$VERSION.html.zip" "<%= name %>-$VERSION.metadata.json" $BINS; do
|
||||
curl -fsS -H "X-Update-Secret: $UPDATE_SECRET" \
|
||||
-F "file=@$f" \
|
||||
"$UPDATE_SERVER/build/upload?name=<%= name %>&version=$VERSION" || exit 1
|
||||
done
|
||||
|
||||
- name: publish
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
curl -fsS -X POST -H "X-Update-Secret: $UPDATE_SECRET" "$UPDATE_SERVER/build/publish?name=<%= name %>&platform=godot&version=$VERSION"
|
||||
@@ -0,0 +1,174 @@
|
||||
# Generált pipeline — WarpEngine /build/config (platform: love, name: <%= name %>)
|
||||
steps:
|
||||
- name: version
|
||||
image: alpine
|
||||
commands:
|
||||
- apk add --no-cache git jq
|
||||
- |
|
||||
if [ -f metadata.json ]; then
|
||||
VERSION=$(jq -r '.version' metadata.json)
|
||||
else
|
||||
VERSION=$(git rev-parse --short HEAD)
|
||||
fi
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
||||
VERSION="dev-$VERSION-$BRANCH"
|
||||
fi
|
||||
echo "VERSION is: $VERSION"
|
||||
echo $VERSION > .version
|
||||
|
||||
- name: export
|
||||
image: <%= builder %>
|
||||
pull: true
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
mkdir -p dist
|
||||
echo "==> Building .love package"
|
||||
zip -r dist/<%= name %>.love . \
|
||||
--exclude "*.git*" \
|
||||
--exclude "bin/*" \
|
||||
--exclude "dist/*" \
|
||||
--exclude "Makefile" \
|
||||
--exclude ".version" \
|
||||
--exclude "metadata.json" \
|
||||
--exclude "*.zip"
|
||||
mkdir -p dist/web
|
||||
# A love-builder CI image ide előre letölti a love.js-t; lokális
|
||||
# buildnél GitHubról jön.
|
||||
if [ -f /opt/lovejs.zip ]; then
|
||||
echo "==> Using cached love.js (/opt/lovejs.zip)"
|
||||
cp /opt/lovejs.zip dist/lovejs.zip
|
||||
else
|
||||
echo "==> Downloading love.js (2dengine)"
|
||||
curl -sSL https://github.com/2dengine/love.js/archive/refs/heads/master.zip -o dist/lovejs.zip
|
||||
fi
|
||||
unzip -o dist/lovejs.zip -d dist/lovejs-src
|
||||
rm -f dist/lovejs.zip
|
||||
echo "==> Assembling web bundle"
|
||||
cp -r dist/lovejs-src/*/. dist/web/
|
||||
rm -rf dist/lovejs-src
|
||||
cp dist/<%= name %>.love dist/web/<%= name %>.love
|
||||
echo "==> Patching player.js"
|
||||
sed -i.bak "s|uri = 'nogame\.love'|uri = '<%= name %>.love'|g" dist/web/player.js && rm dist/web/player.js.bak
|
||||
echo "==> Patching index.html"
|
||||
sed -i.bak 's|<base href="/play/">|<base href="/file/<%= name %>-'"$VERSION"'/">|g' dist/web/index.html && rm dist/web/index.html.bak
|
||||
echo "==> Web build ready in dist/web"
|
||||
echo "==> Packaging Love2D for $VERSION"
|
||||
zip -r <%= name %>-$VERSION.love.zip dist/<%= name %>.love
|
||||
echo "==> Packaging web build for $VERSION"
|
||||
(cd dist/web && zip -r ../../<%= name %>-$VERSION.html.zip .)
|
||||
echo "==> Cleaning temporary files"
|
||||
rm -f dist/<%= name %>.love
|
||||
rm -rf dist/web
|
||||
|
||||
- name: binaries
|
||||
image: <%= builder %>
|
||||
pull: true
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
# Az export step törölte a .love-ot, itt újraépítjük (make-ben a
|
||||
# binary-* targetek love-prerequisite-je tette ugyanezt).
|
||||
mkdir -p dist
|
||||
zip -r dist/<%= name %>.love . \
|
||||
--exclude "*.git*" \
|
||||
--exclude "bin/*" \
|
||||
--exclude "dist/*" \
|
||||
--exclude "Makefile" \
|
||||
--exclude ".version" \
|
||||
--exclude "metadata.json" \
|
||||
--exclude "*.zip"
|
||||
# A love-builder CI image a dist-fájlokat /opt/love-dist alá előre
|
||||
# letölti; lokális buildnél GitHubról jönnek.
|
||||
fetch_love() {
|
||||
if [ -f "/opt/love-dist/$1" ]; then
|
||||
echo "==> Using cached $1"
|
||||
cp "/opt/love-dist/$1" "dist/$1"
|
||||
elif [ ! -f "dist/$1" ]; then
|
||||
echo "==> Downloading $1"
|
||||
curl -sSL "https://github.com/love2d/love/releases/download/11.5/$1" -o "dist/$1"
|
||||
fi
|
||||
}
|
||||
echo "==> Fusing windows binary"
|
||||
fetch_love love-11.5-win64.zip
|
||||
PKG_DIR="<%= name %>-$VERSION-win-x64"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip" dist/win64
|
||||
unzip -q dist/love-11.5-win64.zip -d dist/win64
|
||||
SRC=$(dirname $(find dist/win64 -name love.exe | head -n 1))
|
||||
mkdir -p "$PKG_DIR"
|
||||
cat "$SRC/love.exe" dist/<%= name %>.love > "$PKG_DIR/<%= name %>.exe"
|
||||
cp "$SRC"/*.dll "$PKG_DIR/"
|
||||
cp "$SRC/license.txt" "$PKG_DIR/" 2>/dev/null || true
|
||||
zip -qr "$PKG_DIR.zip" "$PKG_DIR"
|
||||
rm -rf "$PKG_DIR" dist/win64
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
echo "==> Fusing macOS app bundle"
|
||||
fetch_love love-11.5-macos.zip
|
||||
PKG_DIR="<%= name %>-$VERSION-mac-universal"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip" dist/macos
|
||||
unzip -q dist/love-11.5-macos.zip -d dist/macos
|
||||
mkdir -p "$PKG_DIR"
|
||||
mv dist/macos/love.app "$PKG_DIR/<%= name %>.app"
|
||||
cp dist/<%= name %>.love "$PKG_DIR/<%= name %>.app/Contents/Resources/"
|
||||
PLIST="$PKG_DIR/<%= name %>.app/Contents/Info.plist"
|
||||
sed -i.bak "s|<string>LÖVE</string>|<string><%= name %></string>|g" "$PLIST" && rm "$PLIST.bak"
|
||||
sed -i.bak "s|org\.love2d\.love|org.teletypegames.<%= name %>|g" "$PLIST" && rm "$PLIST.bak"
|
||||
zip -qry "$PKG_DIR.zip" "$PKG_DIR"
|
||||
rm -rf "$PKG_DIR" dist/macos
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
# Az AppImage runtime glibc-dinamikus, alpine (musl) alatt nem
|
||||
# futtatható, ezért nem a runtime-ot futtatjuk: az offsetet readelf-ből
|
||||
# számoljuk (shoff + shentsize*shnum), a squashfs-t unsquashfs -o
|
||||
# bontja ki.
|
||||
echo "==> Fusing linux AppImage"
|
||||
fetch_love love-11.5-x86_64.AppImage
|
||||
PKG_DIR="<%= name %>-$VERSION-linux-x64"
|
||||
APPIMAGE="dist/love-11.5-x86_64.AppImage"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip" squashfs-root dist/game.squashfs dist/runtime
|
||||
OFFSET=$(readelf -h "$APPIMAGE" | awk '/Start of section headers/{o=$5} /Size of section headers/{s=$5} /Number of section headers/{n=$5} END{print o+s*n}')
|
||||
unsquashfs -q -o $OFFSET -d squashfs-root "$APPIMAGE" >/dev/null
|
||||
cat squashfs-root/bin/love dist/<%= name %>.love > squashfs-root/bin/love.fused
|
||||
mv squashfs-root/bin/love.fused squashfs-root/bin/love
|
||||
chmod +x squashfs-root/bin/love
|
||||
mksquashfs squashfs-root dist/game.squashfs -root-owned -noappend -quiet -comp gzip
|
||||
head -c $OFFSET "$APPIMAGE" > dist/runtime
|
||||
mkdir -p "$PKG_DIR"
|
||||
cat dist/runtime dist/game.squashfs > "$PKG_DIR/<%= name %>.AppImage"
|
||||
chmod +x "$PKG_DIR/<%= name %>.AppImage"
|
||||
zip -qr "$PKG_DIR.zip" "$PKG_DIR"
|
||||
rm -rf "$PKG_DIR" squashfs-root dist/game.squashfs dist/runtime
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
|
||||
- name: upload
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
cp metadata.json "<%= name %>-$VERSION.metadata.json"
|
||||
BINS=""
|
||||
for slug in win-x64 mac-universal linux-x64; do
|
||||
[ -f "<%= name %>-$VERSION-$slug.zip" ] && BINS="$BINS <%= name %>-$VERSION-$slug.zip"
|
||||
done
|
||||
for f in "<%= name %>-$VERSION.love.zip" "<%= name %>-$VERSION.html.zip" "<%= name %>-$VERSION.metadata.json" $BINS; do
|
||||
curl -fsS -H "X-Update-Secret: $UPDATE_SECRET" \
|
||||
-F "file=@$f" \
|
||||
"$UPDATE_SERVER/build/upload?name=<%= name %>&version=$VERSION" || exit 1
|
||||
done
|
||||
|
||||
- name: publish
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
curl -fsS -X POST -H "X-Update-Secret: $UPDATE_SECRET" "$UPDATE_SERVER/build/publish?name=<%= name %>&platform=love&version=$VERSION"
|
||||
@@ -0,0 +1,69 @@
|
||||
# Generált pipeline — WarpEngine /build/config (platform: phaser, name: <%= name %>)
|
||||
steps:
|
||||
- name: version
|
||||
image: alpine
|
||||
commands:
|
||||
- apk add --no-cache git jq
|
||||
- |
|
||||
if [ -f metadata.json ]; then
|
||||
VERSION=$(jq -r '.version' metadata.json)
|
||||
else
|
||||
VERSION=$(git rev-parse --short HEAD)
|
||||
fi
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
||||
VERSION="dev-$VERSION-$BRANCH"
|
||||
fi
|
||||
echo "VERSION is: $VERSION"
|
||||
echo $VERSION > .version
|
||||
|
||||
- name: build
|
||||
image: <%= builder %>
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Checking JS syntax"
|
||||
for f in src/*.js; do node --check $f; done
|
||||
mkdir -p dist/web
|
||||
echo "==> Downloading Phaser 3.90.0"
|
||||
curl -sSL https://cdn.jsdelivr.net/npm/phaser@3.90.0/dist/phaser.min.js -o dist/web/phaser.min.js
|
||||
echo "==> Downloading index.html"
|
||||
curl -sSL https://git.teletypegames.org/tools/phaser-tools/raw/branch/master/web/index.html -o dist/web/index.html
|
||||
echo "==> Bundling game sources"
|
||||
cat src/*.js > dist/web/game.js
|
||||
echo "==> Packaging web build for $VERSION"
|
||||
(cd dist/web && zip -r "../../<%= name %>-$VERSION.html.zip" .)
|
||||
echo "==> Cleaning temporary files"
|
||||
rm -rf dist/web
|
||||
|
||||
- name: upload
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
FILE="<%= name %>-$VERSION.html.zip"
|
||||
META_SRC="metadata.json"
|
||||
META_DST="<%= name %>-$VERSION.metadata.json"
|
||||
cp $META_SRC $META_DST
|
||||
for f in $FILE $META_DST; do
|
||||
curl -fsS -H "X-Update-Secret: $UPDATE_SECRET" \
|
||||
-F "file=@$f" \
|
||||
"$UPDATE_SERVER/build/upload?name=<%= name %>&version=$VERSION" || exit 1
|
||||
done
|
||||
|
||||
- name: publish
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
curl -fsS -X POST -H "X-Update-Secret: $UPDATE_SECRET" "$UPDATE_SERVER/build/publish?name=<%= name %>&platform=phaser&version=$VERSION"
|
||||
@@ -0,0 +1,188 @@
|
||||
# Generált pipeline — WarpEngine /build/config (platform: tic80, name: <%= name %>)
|
||||
# A verzió a forrásból jön (inc/meta/meta.header.lua "-- version:" komment) — a
|
||||
# WarpEngine is a Lua headerből parsolja a tic80-metadatát, ezért nincs metadata.json.
|
||||
steps:
|
||||
- name: version
|
||||
image: alpine
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(sed -n "s/^-- version: //p" inc/meta/meta.header.lua | head -n 1 | tr -d "[:space:]")
|
||||
BRANCH=${CI_COMMIT_BRANCH:-${WOODPECKER_BRANCH}}
|
||||
BRANCH=$(echo "$BRANCH" | tr '/' '-')
|
||||
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ] && [ -n "$BRANCH" ]; then
|
||||
VERSION=dev-$VERSION-$BRANCH
|
||||
fi
|
||||
echo "VERSION is: $VERSION"
|
||||
echo $VERSION > .version
|
||||
|
||||
- name: lint
|
||||
image: <%= builder %>
|
||||
commands:
|
||||
- |
|
||||
echo "==> Merging..."
|
||||
rm -f /tmp/_lint_combined.lua /tmp/_lint_map.txt
|
||||
touch /tmp/_lint_combined.lua
|
||||
line=1
|
||||
while IFS= read -r f || [ -n "$f" ]; do
|
||||
f=$(printf '%s' "$f" | tr -d '\r')
|
||||
[ -z "$f" ] && continue
|
||||
before=$(wc -l < /tmp/_lint_combined.lua)
|
||||
cat "inc/$f" >> /tmp/_lint_combined.lua
|
||||
printf '\n' >> /tmp/_lint_combined.lua
|
||||
after=$(wc -l < /tmp/_lint_combined.lua)
|
||||
linecount=$((after - before))
|
||||
echo "$line $linecount inc/$f" >> /tmp/_lint_map.txt
|
||||
line=$((line + linecount))
|
||||
done < <%= name %>.inc
|
||||
echo "==> luacheck..."
|
||||
LINT_OUTPUT=$(luacheck --no-max-line-length /tmp/_lint_combined.lua 2>&1 | awk -v map=/tmp/_lint_map.txt '
|
||||
BEGIN {
|
||||
NR_map = 0;
|
||||
while ((getline line < map) > 0) {
|
||||
n = split(line, a, " ");
|
||||
start[NR_map] = a[1]+0;
|
||||
count[NR_map] = a[2]+0;
|
||||
fname[NR_map] = a[3];
|
||||
NR_map++;
|
||||
}
|
||||
}
|
||||
/^[^:]+:[0-9]+:[0-9]+:/ {
|
||||
colon1 = index($0, ":");
|
||||
rest1 = substr($0, colon1+1);
|
||||
colon2 = index(rest1, ":");
|
||||
absline = substr(rest1, 1, colon2-1) + 0;
|
||||
rest2 = substr(rest1, colon2+1);
|
||||
colon3 = index(rest2, ":");
|
||||
col = substr(rest2, 1, colon3-1);
|
||||
rest = substr(rest2, colon3);
|
||||
found = 0;
|
||||
for (i = 0; i < NR_map; i++) {
|
||||
end_line = start[i] + count[i] -1;
|
||||
if (absline >= start[i] && absline <= end_line) {
|
||||
relline = absline - start[i] + 1;
|
||||
print fname[i] ":" relline ":" col ":" rest;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) print $0;
|
||||
next;
|
||||
}
|
||||
{ print }
|
||||
')
|
||||
echo "$LINT_OUTPUT"
|
||||
NUM_ISSUES=$(echo "$LINT_OUTPUT" | grep -cE "^[^:]+:[0-9]+:[0-9]+:" || true)
|
||||
if [ "$NUM_ISSUES" -gt 0 ]; then
|
||||
echo "Total: $NUM_ISSUES issue(s) found, commit aborted."
|
||||
exit 1
|
||||
else
|
||||
echo "Checking /tmp/_lint_combined.lua OK"
|
||||
echo "Total: 0 warnings / 0 errors in 1 file"
|
||||
fi
|
||||
rm -f /tmp/_lint_combined.lua /tmp/_lint_map.txt
|
||||
|
||||
- name: minify
|
||||
image: <%= builder %>
|
||||
commands:
|
||||
- |
|
||||
rm -f <%= name %>.lua
|
||||
sed 's/\r$//' <%= name %>.inc | while read f; do
|
||||
cat "inc/$f" >> <%= name %>.lua
|
||||
echo "" >> <%= name %>.lua
|
||||
done
|
||||
test -f minify.lua || { echo "==> Downloading minify.lua"; curl -fsSL https://raw.githubusercontent.com/ztimar31/lua-minify-tic80/refs/heads/master/minify.lua -o minify.lua; }
|
||||
echo "==> Minifying <%= name %>.lua"
|
||||
cp <%= name %>.lua <%= name %>.original.lua
|
||||
lua minify.lua minify <%= name %>.original.lua > <%= name %>.lua
|
||||
|
||||
- name: docs
|
||||
image: <%= builder %>
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Generating docs from <%= name %>.original.lua"
|
||||
ldoc <%= name %>.original.lua -d docs
|
||||
echo "==> Zipping docs for version $VERSION"
|
||||
(cd docs && zip -r ../<%= name %>-$VERSION-docs.zip .)
|
||||
cp <%= name %>-$VERSION-docs.zip <%= name %>-docs.zip
|
||||
echo "==> Docs zip created"
|
||||
|
||||
- name: export
|
||||
image: <%= exporter %>
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: /tmp
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Exporting HTML for version $VERSION"
|
||||
tic80 --cli --skip --fs=. \
|
||||
--cmd="load <%= name %>.lua & save <%= name %>-$VERSION & export html <%= name %>-$VERSION.html & exit"
|
||||
if [ -f "<%= name %>-$VERSION.tic" ]; then
|
||||
cp <%= name %>-$VERSION.tic <%= name %>.tic
|
||||
fi
|
||||
if [ -f "<%= name %>-$VERSION.html.zip" ]; then
|
||||
cp <%= name %>-$VERSION.html.zip <%= name %>.html.zip
|
||||
fi
|
||||
echo "==> Generated files:"
|
||||
ls -lh <%= name %>-$VERSION.* <%= name %>.tic <%= name %>.html.zip 2>/dev/null || true
|
||||
|
||||
- name: binaries
|
||||
image: <%= exporter %>
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: /tmp
|
||||
commands:
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Exporting native players for version $VERSION"
|
||||
tic80 --cli --skip --fs=. \
|
||||
--cmd="load <%= name %>.lua & export win <%= name %>-win & export linux <%= name %>-linux & export mac <%= name %>-mac & exit"
|
||||
# unixos zip őrzi a végrehajtási bitet
|
||||
pack_binary() {
|
||||
SLUG="$1"; SRC_FILE="$2"; DST_FILE="$3"
|
||||
PKG_DIR="<%= name %>-$VERSION-$SLUG"
|
||||
rm -rf "$PKG_DIR" "$PKG_DIR.zip"
|
||||
mkdir -p "$PKG_DIR"
|
||||
mv "$SRC_FILE" "$PKG_DIR/$DST_FILE"
|
||||
chmod +x "$PKG_DIR/$DST_FILE"
|
||||
zip -r "$PKG_DIR.zip" "$PKG_DIR" >/dev/null
|
||||
rm -rf "$PKG_DIR"
|
||||
echo "==> $PKG_DIR.zip kesz"
|
||||
}
|
||||
pack_binary win-x64 <%= name %>-win.exe <%= name %>.exe
|
||||
pack_binary linux-x64 <%= name %>-linux <%= name %>
|
||||
pack_binary mac-x64 <%= name %>-mac <%= name %>
|
||||
|
||||
- name: upload
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Uploading artifacts for version $VERSION"
|
||||
cp <%= name %>.lua <%= name %>-$VERSION.lua
|
||||
BINS=""
|
||||
for slug in win-x64 linux-x64 mac-x64; do
|
||||
[ -f "<%= name %>-$VERSION-$slug.zip" ] && BINS="$BINS <%= name %>-$VERSION-$slug.zip"
|
||||
done
|
||||
for f in <%= name %>-$VERSION.lua <%= name %>-$VERSION.tic <%= name %>-$VERSION.html.zip <%= name %>-$VERSION-docs.zip $BINS; do
|
||||
curl -fsS -H "X-Update-Secret: $UPDATE_SECRET" \
|
||||
-F "file=@$f" \
|
||||
"$UPDATE_SERVER/build/upload?name=<%= name %>&version=$VERSION" || exit 1
|
||||
done
|
||||
|
||||
- name: publish
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: <%= update_server %>
|
||||
UPDATE_SECRET:
|
||||
from_secret: application_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
VERSION=$(cat .version)
|
||||
echo "==> Publishing version $VERSION"
|
||||
curl -fsS -X POST -H "X-Update-Secret: $UPDATE_SECRET" "$UPDATE_SERVER/build/publish?name=<%= name %>&platform=tic80&version=$VERSION"
|
||||
@@ -13,6 +13,13 @@ module WarpEngine
|
||||
# enforce_software_ownership: ha true, egy DB-token csak a saját ownerének
|
||||
# szoftvereit uploadolhatja/publisholhatja (unrestricted token kivétel).
|
||||
# Bekapcsolás CSAK backfill után: gazdátlan software-t bármely token elvihet.
|
||||
# ci_platforms: a /build/config által kiszolgált platformok:
|
||||
# { "godot" => { builder: "<image>" }, "tic80" => { builder: ..., exporter: ... } }
|
||||
# Üres map = a feature inaktív (POST → 204, GET → 404).
|
||||
# ci_extension_public_key(_url): a Woodpecker httpsig ed25519 publikus kulcsa
|
||||
# PEM-ben, vagy URL, ahonnan letölthető (pl. https://ci.../api/signature/public-key).
|
||||
# Egyik sincs beállítva → a POST /build/config minden kérést elutasít.
|
||||
# ci_update_server: az upload/publish stepekbe írt szerver-URL; nil → a kérés base_url-je.
|
||||
attr_accessor :file_container_path,
|
||||
:image_container_path,
|
||||
:update_secret,
|
||||
@@ -20,6 +27,10 @@ module WarpEngine
|
||||
:application_token_owner_class,
|
||||
:max_upload_size,
|
||||
:enforce_software_ownership,
|
||||
:ci_platforms,
|
||||
:ci_extension_public_key,
|
||||
:ci_extension_public_key_url,
|
||||
:ci_update_server,
|
||||
:image_owners
|
||||
|
||||
def initialize
|
||||
@@ -30,6 +41,10 @@ module WarpEngine
|
||||
@application_token_owner_class = nil
|
||||
@max_upload_size = 500 * 1024 * 1024
|
||||
@enforce_software_ownership = false
|
||||
@ci_platforms = {}
|
||||
@ci_extension_public_key = nil
|
||||
@ci_extension_public_key_url = nil
|
||||
@ci_update_server = nil
|
||||
@image_owners = []
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Build configs endpoint", type: :request do
|
||||
let(:signing_key) { OpenSSL::PKey.generate_key("ed25519") }
|
||||
let(:ci_platforms) do
|
||||
{
|
||||
"godot" => { builder: "registry.example/godot-builder:4.6" },
|
||||
"tic80" => { builder: "registry.example/tic80-builder:1.0",
|
||||
exporter: "registry.example/tic80pro:1.0" }
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(WarpEngine.config).to receive(:ci_platforms).and_return(ci_platforms)
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(signing_key.public_to_pem)
|
||||
end
|
||||
|
||||
def signed_headers(method: "post", path: "/build/config")
|
||||
date = Time.now.httpdate
|
||||
signing_string = "(request-target): #{method} #{path}\ndate: #{date}"
|
||||
signature = Base64.strict_encode64(signing_key.sign(nil, signing_string))
|
||||
{
|
||||
"Date" => date,
|
||||
"Signature" => %(keyId="woodpecker-ci-plugins",algorithm="ed25519",headers="(request-target) date",signature="#{signature}"),
|
||||
"Content-Type" => "application/json"
|
||||
}
|
||||
end
|
||||
|
||||
def extension_payload(marker_yaml, repo_name: "mygame")
|
||||
{
|
||||
repo: { name: repo_name },
|
||||
pipeline: { branch: "master" },
|
||||
configuration: [ { name: ".woodpecker.yaml", data: marker_yaml } ]
|
||||
}.to_json
|
||||
end
|
||||
|
||||
describe "GET /build/config" do
|
||||
it "renders the pipeline for a configured platform" do
|
||||
get "/build/config", params: { platform: "godot", name: "mygame" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
pipeline = YAML.safe_load(response.body)
|
||||
expect(pipeline["steps"]).to be_present
|
||||
expect(response.body).to include("registry.example/godot-builder:4.6")
|
||||
expect(response.body).to include("mygame")
|
||||
end
|
||||
|
||||
it "returns 404 for an unknown platform" do
|
||||
get "/build/config", params: { platform: "nope" }
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it "returns 404 when the feature is not configured" do
|
||||
allow(WarpEngine.config).to receive(:ci_platforms).and_return({})
|
||||
|
||||
get "/build/config", params: { platform: "godot" }
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it "rejects path traversal in the platform param" do
|
||||
get "/build/config", params: { platform: "../secrets" }
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /build/config" do
|
||||
it "returns the rendered pipeline for a marker config" do
|
||||
post "/build/config", params: extension_payload("platform: godot\n"),
|
||||
headers: signed_headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
configs = response.parsed_body["configs"]
|
||||
expect(configs.length).to eq(1)
|
||||
expect(configs.first["name"]).to eq("godot")
|
||||
pipeline = YAML.safe_load(configs.first["data"])
|
||||
expect(pipeline["steps"].map { |s| s["name"] }).to include("version", "publish")
|
||||
expect(configs.first["data"]).to include("mygame")
|
||||
end
|
||||
|
||||
it "uses the marker's name override instead of the repo name" do
|
||||
post "/build/config", params: extension_payload("platform: godot\nname: othername\n"),
|
||||
headers: signed_headers
|
||||
|
||||
expect(response.parsed_body["configs"].first["data"]).to include("othername")
|
||||
expect(response.parsed_body["configs"].first["data"]).not_to include("mygame")
|
||||
end
|
||||
|
||||
it "accepts the configs key used by older Woodpecker payloads" do
|
||||
payload = { repo: { name: "mygame" },
|
||||
configs: [ { name: ".woodpecker.yaml", data: "platform: godot\n" } ] }.to_json
|
||||
|
||||
post "/build/config", params: payload, headers: signed_headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "returns 204 for a non-marker config" do
|
||||
full_pipeline = "steps:\n - name: build\n image: alpine\n"
|
||||
|
||||
post "/build/config", params: extension_payload(full_pipeline),
|
||||
headers: signed_headers
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
|
||||
it "returns 204 when no configuration is sent" do
|
||||
post "/build/config", params: { repo: { name: "mygame" } }.to_json,
|
||||
headers: signed_headers
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
|
||||
it "returns 422 for a marker with an unknown platform" do
|
||||
post "/build/config", params: extension_payload("platform: amiga\n"),
|
||||
headers: signed_headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it "rejects a request with an invalid signature" do
|
||||
headers = signed_headers
|
||||
other_key = OpenSSL::PKey.generate_key("ed25519")
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(other_key.public_to_pem)
|
||||
|
||||
post "/build/config", params: extension_payload("platform: godot\n"), headers: headers
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it "rejects a request without a signature header" do
|
||||
post "/build/config", params: extension_payload("platform: godot\n"),
|
||||
headers: { "Content-Type" => "application/json" }
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it "rejects every request when no public key is configured" do
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(nil)
|
||||
allow(WarpEngine.config).to receive(:ci_extension_public_key_url).and_return(nil)
|
||||
|
||||
post "/build/config", params: extension_payload("platform: godot\n"),
|
||||
headers: signed_headers
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
describe "shipped templates" do
|
||||
it "renders every template to valid YAML with non-empty steps" do
|
||||
templates = Dir[WarpEngine::Engine.root.join("lib/warp_engine/ci_templates/*.yaml.erb")]
|
||||
expect(templates).not_to be_empty
|
||||
|
||||
templates.each do |path|
|
||||
platform = File.basename(path, ".yaml.erb")
|
||||
allow(WarpEngine.config).to receive(:ci_platforms).and_return(
|
||||
platform => { builder: "registry.example/builder:1", exporter: "registry.example/exporter:1" }
|
||||
)
|
||||
|
||||
yaml = WarpEngine::CiConfigService.new.render(
|
||||
platform: platform, name: "example", update_server: "https://games.example"
|
||||
)
|
||||
|
||||
expect(yaml).to be_present, "#{platform}: no template rendered"
|
||||
pipeline = YAML.safe_load(yaml)
|
||||
expect(pipeline["steps"]).to be_present, "#{platform}: no steps"
|
||||
pipeline["steps"].each do |step|
|
||||
expect(step["image"]).to be_present, "#{platform}/#{step['name']}: missing image"
|
||||
expect(step["commands"]).to be_present, "#{platform}/#{step['name']}: missing commands"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user