gitlab
This commit is contained in:
@@ -69,23 +69,30 @@ module WarpEngine
|
|||||||
|
|
||||||
case configured
|
case configured
|
||||||
when nil, :woodpecker, "woodpecker" then woodpecker_adapter
|
when nil, :woodpecker, "woodpecker" then woodpecker_adapter
|
||||||
|
when :gitlab, "gitlab" then gitlab_adapter
|
||||||
when :none, "none", :null then null_adapter
|
when :none, "none", :null then null_adapter
|
||||||
else configured
|
else configured
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def woodpecker? = adapter.is_a?(Woodpecker::Adapter)
|
def woodpecker? = adapter.is_a?(Woodpecker::Adapter)
|
||||||
|
def gitlab? = adapter.is_a?(Gitlab::Adapter)
|
||||||
|
|
||||||
def woodpecker_adapter
|
def woodpecker_adapter
|
||||||
@woodpecker_adapter ||= Woodpecker::Adapter.new
|
@woodpecker_adapter ||= Woodpecker::Adapter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def gitlab_adapter
|
||||||
|
@gitlab_adapter ||= Gitlab::Adapter.new
|
||||||
|
end
|
||||||
|
|
||||||
def null_adapter
|
def null_adapter
|
||||||
@null_adapter ||= Null.new
|
@null_adapter ||= Null.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset!
|
def reset!
|
||||||
@woodpecker_adapter = nil
|
@woodpecker_adapter = nil
|
||||||
|
@gitlab_adapter = nil
|
||||||
@null_adapter = nil
|
@null_adapter = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -93,3 +100,4 @@ module WarpEngine
|
|||||||
end
|
end
|
||||||
|
|
||||||
require "warp_engine/ci/woodpecker"
|
require "warp_engine/ci/woodpecker"
|
||||||
|
require "warp_engine/ci/gitlab"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
require "warp_engine/ci/gitlab/client"
|
||||||
|
require "warp_engine/ci/gitlab/signature_verifier"
|
||||||
|
require "warp_engine/ci/gitlab/pipeline_config"
|
||||||
|
require "warp_engine/ci/gitlab/adapter"
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
module WarpEngine
|
||||||
|
module CI
|
||||||
|
module Gitlab
|
||||||
|
class Adapter
|
||||||
|
attr_reader :url, :update_server
|
||||||
|
|
||||||
|
def initialize(url: ENV["GITLAB_URL"],
|
||||||
|
api_token: ENV["GITLAB_API_TOKEN"],
|
||||||
|
webhook_secret: ENV["GITLAB_WEBHOOK_SECRET"],
|
||||||
|
platforms: {},
|
||||||
|
update_server: nil)
|
||||||
|
@url = url.presence
|
||||||
|
@api_token = api_token.presence
|
||||||
|
@webhook_secret = webhook_secret.presence
|
||||||
|
@update_server = update_server.presence
|
||||||
|
@config = PipelineConfig.new(platforms: platforms || {})
|
||||||
|
end
|
||||||
|
|
||||||
|
def name = "GitLab"
|
||||||
|
|
||||||
|
def configured? = @url.present? && @api_token.present?
|
||||||
|
|
||||||
|
def platforms = @config.platforms
|
||||||
|
|
||||||
|
def built_kinds(platform) = @config.built_kinds(platform)
|
||||||
|
|
||||||
|
def client
|
||||||
|
@client ||= Client.new(url: @url, token: @api_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def repos
|
||||||
|
Array(client.list_repos).map { |remote| to_repo(remote) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def repo(project_id)
|
||||||
|
to_repo(client.get_repo(project_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def activate_repo(project_id)
|
||||||
|
client.activate_repo(project_id)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def deactivate_repo(project_id)
|
||||||
|
client.deactivate_repo(project_id)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def runs(project_id, page: 1)
|
||||||
|
Array(client.list_pipelines(project_id, page: page)).map { |run| to_run(run) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def run(project_id, number)
|
||||||
|
remote = if number.to_s == "latest"
|
||||||
|
client.latest_pipeline(project_id)
|
||||||
|
else
|
||||||
|
client.get_pipeline(project_id, number)
|
||||||
|
end
|
||||||
|
remote && to_run(remote)
|
||||||
|
end
|
||||||
|
|
||||||
|
def trigger(project_id, branch: "main")
|
||||||
|
to_run(client.trigger_pipeline(project_id, branch: branch || "main"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def secret_names(project_id)
|
||||||
|
Array(client.list_secrets(project_id)).map { |var| var["key"] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def secret_set(project_id, name:, value:)
|
||||||
|
if secret_names(project_id).include?(name)
|
||||||
|
client.update_secret(project_id, name, value: value)
|
||||||
|
else
|
||||||
|
client.create_secret(project_id, name: name, value: value)
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def secret_delete(project_id, name)
|
||||||
|
client.delete_secret(project_id, name)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_config_request(request)
|
||||||
|
SignatureVerifier.new(request, webhook_secret: @webhook_secret).valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_marker(params)
|
||||||
|
configs = params[:configuration].presence || params[:configs].presence || []
|
||||||
|
Array(configs).each do |config|
|
||||||
|
parsed = parse_marker(config[:data].to_s)
|
||||||
|
next unless parsed
|
||||||
|
|
||||||
|
return { platform: parsed["platform"].to_s,
|
||||||
|
name: parsed["name"].presence || params.dig(:repo, :name).to_s }
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def pipeline_config(platform:, name:, update_server:)
|
||||||
|
@config.render(platform: platform, name: name, update_server: update_server)
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_response(platform:, config:)
|
||||||
|
{ configs: [ { name: platform, data: config } ] }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_marker(data)
|
||||||
|
parsed = begin
|
||||||
|
YAML.safe_load(data)
|
||||||
|
rescue Psych::Exception
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
parsed if parsed.is_a?(Hash) && parsed.key?("platform")
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_repo(remote)
|
||||||
|
return nil if remote.nil?
|
||||||
|
|
||||||
|
namespace = remote.dig("namespace", "path") || remote.dig("namespace", "name")
|
||||||
|
Repo.new(id: remote["id"], name: remote["path"] || remote["name"],
|
||||||
|
owner: namespace, active: true, raw: remote)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_run(remote)
|
||||||
|
return nil if remote.nil?
|
||||||
|
|
||||||
|
created = remote["created_at"] ? Time.zone.parse(remote["created_at"]) : nil
|
||||||
|
Run.new(
|
||||||
|
number: remote["id"],
|
||||||
|
status: remote["status"],
|
||||||
|
branch: remote["ref"],
|
||||||
|
message: nil,
|
||||||
|
created_at: created,
|
||||||
|
url: remote["web_url"],
|
||||||
|
raw: remote
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
require "net/http"
|
||||||
|
require "json"
|
||||||
|
require "uri"
|
||||||
|
|
||||||
|
module WarpEngine
|
||||||
|
module CI
|
||||||
|
module Gitlab
|
||||||
|
class Client
|
||||||
|
def initialize(url:, token:)
|
||||||
|
@base_url = url.to_s.chomp("/")
|
||||||
|
@token = token
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_repos
|
||||||
|
get("/api/v4/projects", params: { membership: true, simple: true, per_page: 100 })
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_repo(project_id)
|
||||||
|
get("/api/v4/projects/#{project_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def activate_repo(_project_id)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def deactivate_repo(_project_id)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_secrets(project_id)
|
||||||
|
get("/api/v4/projects/#{project_id}/variables")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_secret(project_id, name:, value:)
|
||||||
|
post("/api/v4/projects/#{project_id}/variables",
|
||||||
|
body: { key: name, value: value, protected: false, masked: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_secret(project_id, secret_name, value:)
|
||||||
|
put("/api/v4/projects/#{project_id}/variables/#{secret_name}",
|
||||||
|
body: { value: value })
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_secret(project_id, secret_name)
|
||||||
|
delete("/api/v4/projects/#{project_id}/variables/#{secret_name}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_pipelines(project_id, page: 1, per_page: 25)
|
||||||
|
get("/api/v4/projects/#{project_id}/pipelines",
|
||||||
|
params: { page: page, per_page: per_page })
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_pipeline(project_id)
|
||||||
|
results = get("/api/v4/projects/#{project_id}/pipelines",
|
||||||
|
params: { per_page: 1, sort: "desc" })
|
||||||
|
Array(results).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_pipeline(project_id, pipeline_id)
|
||||||
|
get("/api/v4/projects/#{project_id}/pipelines/#{pipeline_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def trigger_pipeline(project_id, branch: "main")
|
||||||
|
post("/api/v4/projects/#{project_id}/pipeline",
|
||||||
|
body: { ref: branch })
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get(path, params: {})
|
||||||
|
uri = build_uri(path, params)
|
||||||
|
execute(uri, Net::HTTP::Get.new(uri))
|
||||||
|
end
|
||||||
|
|
||||||
|
def post(path, body: {})
|
||||||
|
uri = build_uri(path)
|
||||||
|
request = Net::HTTP::Post.new(uri)
|
||||||
|
request.body = body.to_json
|
||||||
|
request.content_type = "application/json"
|
||||||
|
execute(uri, request)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put(path, body: {})
|
||||||
|
uri = build_uri(path)
|
||||||
|
request = Net::HTTP::Put.new(uri)
|
||||||
|
request.body = body.to_json
|
||||||
|
request.content_type = "application/json"
|
||||||
|
execute(uri, request)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(path)
|
||||||
|
uri = build_uri(path)
|
||||||
|
execute(uri, Net::HTTP::Delete.new(uri))
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_uri(path, params = {})
|
||||||
|
uri = URI.parse("#{@base_url}#{path}")
|
||||||
|
uri.query = URI.encode_www_form(params) if params.any?
|
||||||
|
uri
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(uri, request)
|
||||||
|
request["PRIVATE-TOKEN"] = @token
|
||||||
|
request["Accept"] = "application/json"
|
||||||
|
|
||||||
|
response = Net::HTTP.start(uri.hostname, uri.port,
|
||||||
|
use_ssl: uri.scheme == "https",
|
||||||
|
open_timeout: 10,
|
||||||
|
read_timeout: 30) do |http|
|
||||||
|
http.request(request)
|
||||||
|
end
|
||||||
|
|
||||||
|
handle_response(uri, response)
|
||||||
|
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout,
|
||||||
|
Net::ReadTimeout, SocketError => e
|
||||||
|
raise CI::ConnectionError, "Cannot reach GitLab at #{@base_url}: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_response(uri, response)
|
||||||
|
case response
|
||||||
|
when Net::HTTPSuccess, Net::HTTPNoContent
|
||||||
|
return nil if response.body.blank?
|
||||||
|
|
||||||
|
begin
|
||||||
|
JSON.parse(response.body)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
raise CI::ApiError.new(
|
||||||
|
"Expected JSON from #{uri.path} but got: #{response.body.truncate(80)}",
|
||||||
|
status: response.code.to_i, body: response.body
|
||||||
|
)
|
||||||
|
end
|
||||||
|
when Net::HTTPNotFound
|
||||||
|
raise CI::ApiError.new("Not found: #{uri.path}", status: 404, body: response.body)
|
||||||
|
else
|
||||||
|
raise CI::ApiError.new(
|
||||||
|
"GitLab API error #{response.code}: #{response.body&.truncate(200)}",
|
||||||
|
status: response.code.to_i,
|
||||||
|
body: response.body
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
require "erb"
|
||||||
|
require "pathname"
|
||||||
|
|
||||||
|
module WarpEngine
|
||||||
|
module CI
|
||||||
|
module Gitlab
|
||||||
|
class PipelineConfig
|
||||||
|
PLATFORM_FORMAT = /\A[a-z0-9_-]+\z/
|
||||||
|
|
||||||
|
BUILT_KINDS = {
|
||||||
|
"tic80" => %w[cartridge source html docs win_x64 linux_x64 mac_x64],
|
||||||
|
"ebitengine" => %w[html win_x86 win_x64 linux_x64 linux_arm64],
|
||||||
|
"godot" => %w[html win_x86 win_x64 linux_x64 mac_universal],
|
||||||
|
"love" => %w[html win_x64 linux_x64 mac_universal],
|
||||||
|
"bevy" => %w[html win_x64 linux_x64 linux_arm64],
|
||||||
|
"c64" => %w[cartridge],
|
||||||
|
"phaser" => %w[html]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def initialize(platforms: {})
|
||||||
|
@platforms = platforms.to_h { |key, spec| [ key.to_s, spec.to_h.symbolize_keys ] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def platforms
|
||||||
|
@platforms
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(platform:, name:, update_server:)
|
||||||
|
platform = platform.to_s
|
||||||
|
return nil unless platform.match?(PLATFORM_FORMAT)
|
||||||
|
|
||||||
|
spec = @platforms[platform]
|
||||||
|
return nil if spec.nil?
|
||||||
|
|
||||||
|
path = self.class.templates_dir.join(platform, "pipeline.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
|
||||||
|
|
||||||
|
def built_kinds(platform)
|
||||||
|
platform = platform.to_s
|
||||||
|
return nil unless @platforms.key?(platform)
|
||||||
|
|
||||||
|
BUILT_KINDS[platform]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.templates_dir
|
||||||
|
Pathname.new(__dir__).join("platforms")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# Generated pipeline — WarpEngine /build/config (platform: bevy, name: <%= name %>)
|
||||||
|
stages:
|
||||||
|
- version
|
||||||
|
- build
|
||||||
|
- binaries
|
||||||
|
- upload
|
||||||
|
- publish
|
||||||
|
|
||||||
|
version:
|
||||||
|
stage: version
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .version
|
||||||
|
- metadata.json
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
binaries:
|
||||||
|
stage: binaries
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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"
|
||||||
|
# linux-arm64: Raspberry Pi, Odroid, retro handhelds. pkg-config has to
|
||||||
|
# be told it may cross, and pointed at the arm64 .pc files, otherwise
|
||||||
|
# alsa-sys/libudev-sys pick up the host x86_64 libraries.
|
||||||
|
echo "==> Building linux-arm64 binary"
|
||||||
|
PKG_CONFIG_ALLOW_CROSS=1 \
|
||||||
|
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig \
|
||||||
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
||||||
|
cargo build --release --target aarch64-unknown-linux-gnu
|
||||||
|
pack_binary "linux-arm64" "target/aarch64-unknown-linux-gnu/release/<%= name %>" "<%= name %>"
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
upload:
|
||||||
|
stage: upload
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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 linux-arm64; 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
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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,74 @@
|
|||||||
|
# Generated pipeline — WarpEngine /build/config (platform: c64, name: <%= name %>)
|
||||||
|
stages:
|
||||||
|
- version
|
||||||
|
- build
|
||||||
|
- artifact
|
||||||
|
- publish
|
||||||
|
|
||||||
|
version:
|
||||||
|
stage: version
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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"
|
||||||
|
BRANCH=$(echo "$BRANCH" | tr '/' '-')
|
||||||
|
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ] && [ -n "$BRANCH" ]; then
|
||||||
|
VERSION="dev-$VERSION-$BRANCH"
|
||||||
|
fi
|
||||||
|
echo "VERSION is: $VERSION"
|
||||||
|
echo $VERSION > .version
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .version
|
||||||
|
- metadata.json
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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.*
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.prg"
|
||||||
|
- "*.metadata.json"
|
||||||
|
|
||||||
|
artifact:
|
||||||
|
stage: artifact
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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,118 @@
|
|||||||
|
# Generated pipeline — WarpEngine /build/config (platform: ebitengine, name: <%= name %>)
|
||||||
|
stages:
|
||||||
|
- version
|
||||||
|
- build
|
||||||
|
- binaries
|
||||||
|
- artifact
|
||||||
|
- publish
|
||||||
|
|
||||||
|
version:
|
||||||
|
stage: version
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .version
|
||||||
|
- metadata.json
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
binaries:
|
||||||
|
stage: binaries
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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)
|
||||||
|
# helper: builds one target + zips it with a single root folder
|
||||||
|
# (unix zip keeps the executable bit)
|
||||||
|
binary_build() {
|
||||||
|
B_GOOS="$1"; B_GOARCH="$2"; B_CGO="$3"; B_EXT="$4"; B_TARGET="$5"; B_CC="$6"
|
||||||
|
PKG_DIR="<%= name %>-$VERSION-$B_TARGET"
|
||||||
|
echo "==> Building $PKG_DIR"
|
||||||
|
rm -rf "$PKG_DIR" "$PKG_DIR.zip"
|
||||||
|
mkdir -p "$PKG_DIR"
|
||||||
|
# cgo cross-compile needs an explicit cross gcc; a native build must
|
||||||
|
# not see CC at all, otherwise go picks the wrong compiler
|
||||||
|
if [ -n "$B_CC" ]; then export CC="$B_CC"; else unset CC; fi
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
# CI (linux builder) builds these four:
|
||||||
|
binary_build "windows" "386" "0" ".exe" "win-x86"
|
||||||
|
binary_build "windows" "amd64" "0" ".exe" "win-x64"
|
||||||
|
binary_build "linux" "amd64" "1" "" "linux-x64"
|
||||||
|
# linux-arm64: Raspberry Pi, Odroid, retro handhelds — cgo cross-build
|
||||||
|
# against the arm64 X11/GL/ALSA headers in the builder image
|
||||||
|
binary_build "linux" "arm64" "1" "" "linux-arm64" "aarch64-linux-gnu-gcc"
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
artifact:
|
||||||
|
stage: artifact
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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,114 @@
|
|||||||
|
# Generated pipeline — WarpEngine /build/config (platform: godot, name: <%= name %>)
|
||||||
|
stages:
|
||||||
|
- version
|
||||||
|
- build
|
||||||
|
- upload
|
||||||
|
- publish
|
||||||
|
|
||||||
|
version:
|
||||||
|
stage: version
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .version
|
||||||
|
- metadata.json
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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)
|
||||||
|
# exports a win/linux target + zips it with a single root folder
|
||||||
|
# (embed_pck makes the export a single executable)
|
||||||
|
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: from linux Godot can only export macOS into a .zip (holding the
|
||||||
|
# .app); repackage it to the root-folder convention (zip -ry keeps
|
||||||
|
# exec bits and symlinks)
|
||||||
|
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"
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
upload:
|
||||||
|
stage: upload
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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,191 @@
|
|||||||
|
# Generated pipeline — WarpEngine /build/config (platform: love, name: <%= name %>)
|
||||||
|
stages:
|
||||||
|
- version
|
||||||
|
- export
|
||||||
|
- binaries
|
||||||
|
- upload
|
||||||
|
- publish
|
||||||
|
|
||||||
|
version:
|
||||||
|
stage: version
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .version
|
||||||
|
- metadata.json
|
||||||
|
|
||||||
|
export:
|
||||||
|
stage: export
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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
|
||||||
|
# The love-builder CI image pre-fetches love.js here; local builds
|
||||||
|
# fall back to GitHub.
|
||||||
|
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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
binaries:
|
||||||
|
stage: binaries
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
VERSION=$(cat .version)
|
||||||
|
# The export step deleted the .love, rebuild it here (in make the
|
||||||
|
# binary-* targets' love prerequisite did the same).
|
||||||
|
mkdir -p dist
|
||||||
|
zip -r dist/<%= name %>.love . \
|
||||||
|
--exclude "*.git*" \
|
||||||
|
--exclude "bin/*" \
|
||||||
|
--exclude "dist/*" \
|
||||||
|
--exclude "Makefile" \
|
||||||
|
--exclude ".version" \
|
||||||
|
--exclude "metadata.json" \
|
||||||
|
--exclude "*.zip"
|
||||||
|
# The love-builder CI image pre-fetches the dist files to
|
||||||
|
# /opt/love-dist; local builds fall back to GitHub.
|
||||||
|
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"
|
||||||
|
# The AppImage runtime is glibc-dynamic and cannot run on alpine
|
||||||
|
# (musl), so we do not run the runtime: the offset is computed from
|
||||||
|
# readelf (shoff + shentsize*shnum) and the squashfs is extracted
|
||||||
|
# with unsquashfs -o.
|
||||||
|
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"
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
upload:
|
||||||
|
stage: upload
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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,106 @@
|
|||||||
|
# Generated pipeline — WarpEngine /build/config (platform: phaser, name: <%= name %>)
|
||||||
|
stages:
|
||||||
|
- version
|
||||||
|
- build
|
||||||
|
- upload
|
||||||
|
- publish
|
||||||
|
|
||||||
|
version:
|
||||||
|
stage: version
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .version
|
||||||
|
- metadata.json
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: <%= builder %>
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
VERSION=$(cat .version)
|
||||||
|
# Ketfele projektforma el egymas mellett: a sima JS (a forrasok
|
||||||
|
# osszefuzve, a Phaser CDN-rol) es a bundleres (Vite + TypeScript),
|
||||||
|
# ami maga allitja elo a kesz webes csomagot.
|
||||||
|
if [ -f package.json ] && grep -q '"build"' package.json; then
|
||||||
|
echo "==> Bundled project — npm ci && npm run build"
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
# A Vite kimenete onmagaban teljes: index.html + a beforgatott
|
||||||
|
# assetek. A vite.config base-enek relativnak kell lennie, mert a
|
||||||
|
# jatek a /file/<nev>-<verzio>/ alkonyvtarbol szolgal ki.
|
||||||
|
if [ ! -f dist/index.html ]; then
|
||||||
|
echo "ERROR: a build nem hagyott dist/index.html-t" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> Packaging web build for $VERSION"
|
||||||
|
(cd dist && zip -r "../<%= name %>-$VERSION.html.zip" .)
|
||||||
|
echo "==> Cleaning temporary files"
|
||||||
|
rm -rf dist
|
||||||
|
else
|
||||||
|
echo "==> Checking JS syntax"
|
||||||
|
# A bundleres projektben nincs src/*.js, es a shell ilyenkor a
|
||||||
|
# mintat adja tovabb literalkent — a node MODULE_NOT_FOUND-dal
|
||||||
|
# szall el rajta.
|
||||||
|
for f in src/*.js; do [ -e "$f" ] || continue; 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/build/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
|
||||||
|
fi
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
upload:
|
||||||
|
stage: upload
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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,232 @@
|
|||||||
|
# Generated pipeline — WarpEngine /build/config (platform: tic80, name: <%= name %>)
|
||||||
|
# The version comes from the source (inc/meta/meta.header.lua "-- version:"
|
||||||
|
# comment) — WarpEngine parses tic80 metadata from the Lua header too, hence
|
||||||
|
# no metadata.json.
|
||||||
|
stages:
|
||||||
|
- version
|
||||||
|
- lint
|
||||||
|
- minify
|
||||||
|
- docs
|
||||||
|
- export
|
||||||
|
- binaries
|
||||||
|
- upload
|
||||||
|
- publish
|
||||||
|
|
||||||
|
version:
|
||||||
|
stage: version
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
VERSION=$(sed -n "s/^-- version: //p" inc/meta/meta.header.lua | head -n 1 | tr -d "[:space:]")
|
||||||
|
BRANCH="$CI_COMMIT_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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .version
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: lint
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- apk add --no-cache lua5.4 lua5.4-dev luarocks gcc musl-dev
|
||||||
|
- ln -sf /usr/bin/lua5.4 /usr/bin/lua
|
||||||
|
- ln -sf /usr/bin/luarocks-5.4 /usr/bin/luarocks
|
||||||
|
- luarocks install luacheck
|
||||||
|
- |
|
||||||
|
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
|
||||||
|
|
||||||
|
minify:
|
||||||
|
stage: minify
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- apk add --no-cache lua5.4 curl
|
||||||
|
- ln -sf /usr/bin/lua5.4 /usr/bin/lua
|
||||||
|
- |
|
||||||
|
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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "<%= name %>.lua"
|
||||||
|
- "<%= name %>.original.lua"
|
||||||
|
|
||||||
|
docs:
|
||||||
|
stage: docs
|
||||||
|
image: alpine
|
||||||
|
script:
|
||||||
|
- apk add --no-cache lua5.4 lua5.4-dev luarocks gcc musl-dev zip
|
||||||
|
- ln -sf /usr/bin/lua5.4 /usr/bin/lua
|
||||||
|
- ln -sf /usr/bin/luarocks-5.4 /usr/bin/luarocks
|
||||||
|
- luarocks install ldoc
|
||||||
|
- |
|
||||||
|
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"
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*-docs.zip"
|
||||||
|
|
||||||
|
export:
|
||||||
|
stage: export
|
||||||
|
image: <%= builder %>
|
||||||
|
variables:
|
||||||
|
XDG_RUNTIME_DIR: /tmp
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "<%= name %>.lua"
|
||||||
|
- "*.tic"
|
||||||
|
- "*.html.zip"
|
||||||
|
|
||||||
|
binaries:
|
||||||
|
stage: binaries
|
||||||
|
image: <%= builder %>
|
||||||
|
variables:
|
||||||
|
XDG_RUNTIME_DIR: /tmp
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
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"
|
||||||
|
# unix zip preserves the executable bit
|
||||||
|
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 %>
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "*.zip"
|
||||||
|
|
||||||
|
upload:
|
||||||
|
stage: upload
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
image: alpine
|
||||||
|
variables:
|
||||||
|
UPDATE_SERVER: "<%= update_server %>"
|
||||||
|
UPDATE_SECRET: "$application_token"
|
||||||
|
script:
|
||||||
|
- 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"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
module WarpEngine
|
||||||
|
module CI
|
||||||
|
module Gitlab
|
||||||
|
class SignatureVerifier
|
||||||
|
def initialize(request, webhook_secret:)
|
||||||
|
@request = request
|
||||||
|
@webhook_secret = webhook_secret
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
token = @request.headers["X-Gitlab-Token"]
|
||||||
|
return false if token.blank? || @webhook_secret.blank?
|
||||||
|
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(token, @webhook_secret)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe WarpEngine::CI::Woodpecker::PipelineConfig do
|
shared_examples "pipeline config built kinds" do |config_class|
|
||||||
BUILT_KINDS = described_class::BUILT_KINDS
|
built_kinds = config_class::BUILT_KINDS
|
||||||
|
|
||||||
def self.fragment_for(kind)
|
def self.fragment_for(kind)
|
||||||
case kind
|
case kind
|
||||||
@@ -12,17 +12,17 @@ RSpec.describe WarpEngine::CI::Woodpecker::PipelineConfig do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def config_for(platform)
|
define_method(:config_for) do |platform|
|
||||||
described_class.new(platforms: { platform => { builder: "builder:latest", exporter: "exporter:latest" } })
|
config_class.new(platforms: { platform => { builder: "builder:latest", exporter: "exporter:latest" } })
|
||||||
end
|
end
|
||||||
|
|
||||||
it "knows every platform in the registry" do
|
it "knows every platform in the registry" do
|
||||||
expect(BUILT_KINDS.keys).to match_array(WarpEngine::Platform.names)
|
expect(built_kinds.keys).to match_array(WarpEngine::Platform.names)
|
||||||
end
|
end
|
||||||
|
|
||||||
WarpEngine::Platform::NAMES.each do |platform|
|
WarpEngine::Platform::NAMES.each do |platform|
|
||||||
context platform do
|
context platform do
|
||||||
let(:kinds) { BUILT_KINDS.fetch(platform) }
|
let(:kinds) { built_kinds.fetch(platform) }
|
||||||
let(:rendered) do
|
let(:rendered) do
|
||||||
config_for(platform).render(platform: platform, name: "example", update_server: "https://example.test")
|
config_for(platform).render(platform: platform, name: "example", update_server: "https://example.test")
|
||||||
end
|
end
|
||||||
@@ -52,3 +52,11 @@ RSpec.describe WarpEngine::CI::Woodpecker::PipelineConfig do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
RSpec.describe WarpEngine::CI::Woodpecker::PipelineConfig do
|
||||||
|
include_examples "pipeline config built kinds", described_class
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.describe WarpEngine::CI::Gitlab::PipelineConfig do
|
||||||
|
include_examples "pipeline config built kinds", described_class
|
||||||
|
end
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
require "webmock/rspec"
|
||||||
|
|
||||||
|
RSpec.describe WarpEngine::CI::Gitlab::Client do
|
||||||
|
let(:base_url) { "https://gitlab.example.test" }
|
||||||
|
let(:token) { "gl-test-token" }
|
||||||
|
let(:client) { described_class.new(url: base_url, token: token) }
|
||||||
|
|
||||||
|
def stub_gl(method, path, status: 200, body: nil, request_body: nil)
|
||||||
|
stub = stub_request(method, "#{base_url}#{path}")
|
||||||
|
.with(headers: { "PRIVATE-TOKEN" => token, "Accept" => "application/json" })
|
||||||
|
stub = stub.with(body: request_body) if request_body
|
||||||
|
stub.to_return(status: status, body: body&.to_json, headers: { "Content-Type" => "application/json" })
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "repos" do
|
||||||
|
it "lists repos" do
|
||||||
|
repos = [{ "id" => 1, "path" => "game1" }]
|
||||||
|
stub_gl(:get, "/api/v4/projects?membership=true&per_page=100&simple=true", body: repos)
|
||||||
|
|
||||||
|
expect(client.list_repos).to eq(repos)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gets a repo" do
|
||||||
|
repo = { "id" => 42, "path" => "mygame" }
|
||||||
|
stub_gl(:get, "/api/v4/projects/42", body: repo)
|
||||||
|
|
||||||
|
expect(client.get_repo(42)).to eq(repo)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "activate is a no-op" do
|
||||||
|
expect(client.activate_repo(42)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deactivate is a no-op" do
|
||||||
|
expect(client.deactivate_repo(42)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "secrets (variables)" do
|
||||||
|
it "lists variables" do
|
||||||
|
vars = [{ "key" => "application_token" }]
|
||||||
|
stub_gl(:get, "/api/v4/projects/1/variables", body: vars)
|
||||||
|
|
||||||
|
expect(client.list_secrets(1)).to eq(vars)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a variable" do
|
||||||
|
stub_gl(:post, "/api/v4/projects/1/variables", status: 201, body: { "key" => "application_token" })
|
||||||
|
|
||||||
|
result = client.create_secret(1, name: "application_token", value: "secret123")
|
||||||
|
expect(result["key"]).to eq("application_token")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates a variable" do
|
||||||
|
stub_gl(:put, "/api/v4/projects/1/variables/application_token", status: 200, body: { "key" => "application_token" })
|
||||||
|
|
||||||
|
result = client.update_secret(1, "application_token", value: "newsecret")
|
||||||
|
expect(result["key"]).to eq("application_token")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes a variable" do
|
||||||
|
stub_gl(:delete, "/api/v4/projects/1/variables/application_token", status: 204)
|
||||||
|
|
||||||
|
expect(client.delete_secret(1, "application_token")).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "pipelines" do
|
||||||
|
it "lists pipelines" do
|
||||||
|
pipelines = [{ "id" => 1, "status" => "success" }]
|
||||||
|
stub_gl(:get, "/api/v4/projects/42/pipelines?page=1&per_page=25", body: pipelines)
|
||||||
|
|
||||||
|
expect(client.list_pipelines(42)).to eq(pipelines)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gets latest pipeline" do
|
||||||
|
pipeline = { "id" => 5, "status" => "running" }
|
||||||
|
stub_gl(:get, "/api/v4/projects/42/pipelines?per_page=1&sort=desc", body: [pipeline])
|
||||||
|
|
||||||
|
expect(client.latest_pipeline(42)).to eq(pipeline)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil when no pipelines exist" do
|
||||||
|
stub_gl(:get, "/api/v4/projects/42/pipelines?per_page=1&sort=desc", body: [])
|
||||||
|
|
||||||
|
expect(client.latest_pipeline(42)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gets a specific pipeline" do
|
||||||
|
pipeline = { "id" => 99, "status" => "success" }
|
||||||
|
stub_gl(:get, "/api/v4/projects/42/pipelines/99", body: pipeline)
|
||||||
|
|
||||||
|
expect(client.get_pipeline(42, 99)).to eq(pipeline)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "triggers a pipeline" do
|
||||||
|
pipeline = { "id" => 6, "status" => "created" }
|
||||||
|
stub_gl(:post, "/api/v4/projects/42/pipeline", body: pipeline)
|
||||||
|
|
||||||
|
expect(client.trigger_pipeline(42, branch: "main")).to eq(pipeline)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "error handling" do
|
||||||
|
it "raises ApiError on 404" do
|
||||||
|
stub_gl(:get, "/api/v4/projects/999", status: 404, body: { "error" => "not found" })
|
||||||
|
|
||||||
|
expect { client.get_repo(999) }.to raise_error(WarpEngine::CI::ApiError) { |e|
|
||||||
|
expect(e.status).to eq(404)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises ApiError on 500" do
|
||||||
|
stub_gl(:get, "/api/v4/projects?membership=true&per_page=100&simple=true", status: 500, body: { "error" => "internal" })
|
||||||
|
|
||||||
|
expect { client.list_repos }.to raise_error(WarpEngine::CI::ApiError) { |e|
|
||||||
|
expect(e.status).to eq(500)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises ConnectionError on connection refused" do
|
||||||
|
stub_request(:get, "#{base_url}/api/v4/projects?membership=true&per_page=100&simple=true").to_raise(Errno::ECONNREFUSED)
|
||||||
|
|
||||||
|
expect { client.list_repos }.to raise_error(WarpEngine::CI::ConnectionError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises ConnectionError on timeout" do
|
||||||
|
stub_request(:get, "#{base_url}/api/v4/projects?membership=true&per_page=100&simple=true").to_timeout
|
||||||
|
|
||||||
|
expect { client.list_repos }.to raise_error(WarpEngine::CI::ConnectionError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises ApiError when a 200 response is not JSON" do
|
||||||
|
stub_request(:get, "#{base_url}/api/v4/projects?membership=true&per_page=100&simple=true")
|
||||||
|
.to_return(status: 200, body: "<!doctype html><html></html>",
|
||||||
|
headers: { "Content-Type" => "text/html" })
|
||||||
|
|
||||||
|
expect { client.list_repos }.to raise_error(WarpEngine::CI::ApiError, /Expected JSON/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe WarpEngine::CI::Gitlab::SignatureVerifier do
|
||||||
|
let(:webhook_secret) { "gl-webhook-secret-token" }
|
||||||
|
|
||||||
|
def mock_request(token:)
|
||||||
|
instance_double(ActionDispatch::Request,
|
||||||
|
headers: { "X-Gitlab-Token" => token })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "accepts a request with matching token" do
|
||||||
|
request = mock_request(token: webhook_secret)
|
||||||
|
verifier = described_class.new(request, webhook_secret: webhook_secret)
|
||||||
|
|
||||||
|
expect(verifier).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects a request with wrong token" do
|
||||||
|
request = mock_request(token: "wrong-token")
|
||||||
|
verifier = described_class.new(request, webhook_secret: webhook_secret)
|
||||||
|
|
||||||
|
expect(verifier).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects a request with no token" do
|
||||||
|
request = mock_request(token: nil)
|
||||||
|
verifier = described_class.new(request, webhook_secret: webhook_secret)
|
||||||
|
|
||||||
|
expect(verifier).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects a request with blank token" do
|
||||||
|
request = mock_request(token: "")
|
||||||
|
verifier = described_class.new(request, webhook_secret: webhook_secret)
|
||||||
|
|
||||||
|
expect(verifier).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects when webhook secret is not configured" do
|
||||||
|
request = mock_request(token: "anything")
|
||||||
|
verifier = described_class.new(request, webhook_secret: nil)
|
||||||
|
|
||||||
|
expect(verifier).not_to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
+112
@@ -19,6 +19,13 @@ RSpec.describe WarpEngine::CI do
|
|||||||
expect(WarpEngine.ci).not_to be_configured
|
expect(WarpEngine.ci).not_to be_configured
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "drives GitLab when configured" do
|
||||||
|
WarpEngine.config.ci_adapter = :gitlab
|
||||||
|
|
||||||
|
expect(WarpEngine.ci).to be_a(WarpEngine::CI::Gitlab::Adapter)
|
||||||
|
expect(WarpEngine.ci.name).to eq("GitLab")
|
||||||
|
end
|
||||||
|
|
||||||
it "hands back whatever object the host named" do
|
it "hands back whatever object the host named" do
|
||||||
own = Class.new { def name = "Forge Runner" }.new
|
own = Class.new { def name = "Forge Runner" }.new
|
||||||
WarpEngine.config.ci_adapter = own
|
WarpEngine.config.ci_adapter = own
|
||||||
@@ -133,4 +140,109 @@ RSpec.describe WarpEngine::CI do
|
|||||||
.to eq({ configs: [ { name: "godot", data: "steps: []" } ] })
|
.to eq({ configs: [ { name: "godot", data: "steps: []" } ] })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe WarpEngine::CI::Gitlab::Adapter do
|
||||||
|
let(:client) { instance_double(WarpEngine::CI::Gitlab::Client) }
|
||||||
|
let(:adapter) do
|
||||||
|
described_class.new(url: "https://gitlab.test", api_token: "tok",
|
||||||
|
webhook_secret: "gl-secret",
|
||||||
|
platforms: { "godot" => { builder: "registry.test/godot:1" } },
|
||||||
|
update_server: "https://games.test")
|
||||||
|
end
|
||||||
|
|
||||||
|
before { allow(adapter).to receive(:client).and_return(client) }
|
||||||
|
|
||||||
|
it "is inactive without a server and a token" do
|
||||||
|
expect(described_class.new(url: nil, api_token: nil)).not_to be_configured
|
||||||
|
expect(adapter).to be_configured
|
||||||
|
end
|
||||||
|
|
||||||
|
it "normalizes a repository from GitLab JSON" do
|
||||||
|
allow(client).to receive(:list_repos).and_return([
|
||||||
|
{ "id" => 7, "path" => "game", "name" => "game",
|
||||||
|
"namespace" => { "path" => "org", "name" => "Org" } }
|
||||||
|
])
|
||||||
|
|
||||||
|
repo = adapter.repos.first
|
||||||
|
|
||||||
|
expect(repo.id).to eq(7)
|
||||||
|
expect(repo.name).to eq("game")
|
||||||
|
expect(repo.full_name).to eq("org/game")
|
||||||
|
expect(repo).to be_active
|
||||||
|
end
|
||||||
|
|
||||||
|
it "normalizes a run from GitLab pipeline JSON" do
|
||||||
|
allow(client).to receive(:trigger_pipeline).and_return(
|
||||||
|
{ "id" => 42, "status" => "success", "ref" => "main",
|
||||||
|
"created_at" => "2025-08-06T12:00:00.000Z",
|
||||||
|
"web_url" => "https://gitlab.test/org/game/-/pipelines/42" }
|
||||||
|
)
|
||||||
|
|
||||||
|
run = adapter.trigger(7, branch: "main")
|
||||||
|
|
||||||
|
expect(run.number).to eq(42)
|
||||||
|
expect(run).to be_success
|
||||||
|
expect(run.created_at).to be_present
|
||||||
|
expect(run.url).to eq("https://gitlab.test/org/game/-/pipelines/42")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a variable the project does not have yet" do
|
||||||
|
allow(client).to receive(:list_secrets).and_return([])
|
||||||
|
allow(client).to receive(:create_secret)
|
||||||
|
|
||||||
|
adapter.secret_set(7, name: "application_token", value: "plain")
|
||||||
|
|
||||||
|
expect(client).to have_received(:create_secret).with(
|
||||||
|
7, name: "application_token", value: "plain"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates a variable the project already has" do
|
||||||
|
allow(client).to receive(:list_secrets).and_return([ { "key" => "application_token" } ])
|
||||||
|
allow(client).to receive(:update_secret)
|
||||||
|
|
||||||
|
adapter.secret_set(7, name: "application_token", value: "plain")
|
||||||
|
|
||||||
|
expect(client).to have_received(:update_secret).with(7, "application_token", value: "plain")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reads the platform marker out of a config request" do
|
||||||
|
params = ActionController::Parameters.new(
|
||||||
|
repo: { name: "mygame" },
|
||||||
|
configs: [ { name: ".gitlab-ci.yml", data: "platform: godot\n" } ]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(adapter.config_marker(params)).to eq({ platform: "godot", name: "mygame" })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ignores a config request that is not a marker" do
|
||||||
|
params = ActionController::Parameters.new(
|
||||||
|
configs: [ { name: ".gitlab-ci.yml", data: "stages:\n - build\n" } ]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(adapter.config_marker(params)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "renders only the platforms it was given" do
|
||||||
|
expect(adapter.pipeline_config(platform: "godot", name: "mygame",
|
||||||
|
update_server: "https://games.test")).to include("registry.test/godot:1")
|
||||||
|
expect(adapter.pipeline_config(platform: "amiga", name: "mygame",
|
||||||
|
update_server: "https://games.test")).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shapes the response the way the CI server expects" do
|
||||||
|
expect(adapter.config_response(platform: "godot", config: "stages: []"))
|
||||||
|
.to eq({ configs: [ { name: "godot", data: "stages: []" } ] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "verifies webhook requests via X-Gitlab-Token" do
|
||||||
|
good_request = instance_double(ActionDispatch::Request,
|
||||||
|
headers: { "X-Gitlab-Token" => "gl-secret" })
|
||||||
|
bad_request = instance_double(ActionDispatch::Request,
|
||||||
|
headers: { "X-Gitlab-Token" => "wrong" })
|
||||||
|
|
||||||
|
expect(adapter.verify_config_request(good_request)).to be(true)
|
||||||
|
expect(adapter.verify_config_request(bad_request)).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user