stack-service-base 0.0.97 → 0.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/stack-service-base/command_init.rb +3 -3
- data/lib/stack-service-base/examples/mcp_config.ru +40 -11
- data/lib/stack-service-base/mcp/mcp_helper.rb +33 -12
- data/lib/stack-service-base/mcp/mcp_processor.rb +80 -30
- data/lib/stack-service-base/mcp/mcp_tool_registry.rb +67 -17
- data/lib/stack-service-base/project_template/gitlab/.gitlab-ci.yml +42 -11
- data/lib/stack-service-base/project_template/gitlab/docker/Dockerfile.build +36 -0
- data/lib/stack-service-base/project_template/gitlab/docker/docker-compose.yml +1 -1
- data/lib/stack-service-base/project_template/gitlab/docker/local_build.sh +10 -0
- data/lib/stack-service-base/stack_template/gitlab-c/.gitlab-ci.yml +1 -0
- data/lib/stack-service-base/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3db078c7ea6acaca31ec7b32b7991fd213dcaacd03fcab42733f8f3d299b6456
|
|
4
|
+
data.tar.gz: 621b4b6c3b116f0f72555740ca83f9f62029e0a58b3d19655fdf77db45f27a5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ecbb4d62eb789d83cbbc72d422ed7316a5ac486d1cf3dbcf538cd253aefb156d3dcb71b5937e5bd9f99e9360c10310fea242d42abda559532d2ba73594b9213
|
|
7
|
+
data.tar.gz: 4d3d7862b719bcb04d26881dada64385c2a61f3bcad4e1ce13d080b26249fb6accaa31bf650b820d5988de99c132570e09c03b19bbd74121f7f076af8f7b2f28
|
|
@@ -66,7 +66,9 @@ SSBase::CommandLine::COMMANDS[:init] = Class.new do
|
|
|
66
66
|
|
|
67
67
|
def update_service_name(s_name)
|
|
68
68
|
$stdout.puts "Update service name: #{s_name}"
|
|
69
|
-
Dir.glob('
|
|
69
|
+
Dir.glob('**/{*,.*}', File::FNM_DOTMATCH).each do |file|
|
|
70
|
+
path_parts = file.split('/')
|
|
71
|
+
next if path_parts[0...-1].any? { |part| part.start_with?('.') }
|
|
70
72
|
next unless File.file? file
|
|
71
73
|
content = File.read(file)
|
|
72
74
|
include = content.include?('${service_name}') ? '(found)' : nil
|
|
@@ -80,5 +82,3 @@ SSBase::CommandLine::COMMANDS[:init] = Class.new do
|
|
|
80
82
|
def help = ['Create basic service file structure',
|
|
81
83
|
'[... to_compose <deploy name>] ']
|
|
82
84
|
end.new
|
|
83
|
-
|
|
84
|
-
|
|
@@ -4,9 +4,9 @@ require 'stack-service-base'
|
|
|
4
4
|
StackServiceBase.rack_setup self
|
|
5
5
|
|
|
6
6
|
SERVICES = {
|
|
7
|
-
|
|
8
|
-
status:
|
|
9
|
-
uptime: 72 * 3600,
|
|
7
|
+
'database-backend' => {
|
|
8
|
+
status: 'running',
|
|
9
|
+
uptime: 72 * 3600, # seconds
|
|
10
10
|
last_restart: Time.now - 72 * 3600
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -16,25 +16,54 @@ helpers McpHelper
|
|
|
16
16
|
|
|
17
17
|
Tool :search do
|
|
18
18
|
description 'Search for a term in the database'
|
|
19
|
-
input query: { type:
|
|
19
|
+
input query: { type: 'string', description: 'Term to search for', required: true }
|
|
20
20
|
call do |inputs|
|
|
21
21
|
query = inputs[:query]
|
|
22
|
-
{ results: [{id:
|
|
22
|
+
{ results: [{id: 'doc-1', title: '...', url: '...'}] }
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
Tool :fetch do
|
|
27
27
|
description 'Fetch a resource from the database'
|
|
28
|
-
input resource_id: { type:
|
|
28
|
+
input resource_id: { type: 'string', description: 'Resource ID to fetch', required: true }
|
|
29
29
|
call do |inputs|
|
|
30
30
|
id = inputs[:id]
|
|
31
|
-
{ id:
|
|
31
|
+
{ id: 'doc-1', title: '...', text: 'full text...', url: 'https://example.com/doc', metadata: { source: 'vector_store' } }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Tool :schema_echo do
|
|
36
|
+
description 'Echo a value using a direct JSON schema'
|
|
37
|
+
input_schema type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
value: { type: 'string', description: 'Value to echo' }
|
|
40
|
+
},
|
|
41
|
+
required: ['value']
|
|
42
|
+
annotations readOnlyHint: true
|
|
43
|
+
call do |inputs|
|
|
44
|
+
{ value: inputs[:value] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Tool :full_response_echo do
|
|
49
|
+
description 'Echo a value using a complete MCP tool response'
|
|
50
|
+
input value: { type: 'string', description: 'Value to echo', required: true }
|
|
51
|
+
call do |inputs|
|
|
52
|
+
{
|
|
53
|
+
content: [
|
|
54
|
+
{ type: 'text', text: inputs[:value] }
|
|
55
|
+
],
|
|
56
|
+
structuredContent: {
|
|
57
|
+
value: inputs[:value]
|
|
58
|
+
},
|
|
59
|
+
isError: false
|
|
60
|
+
}
|
|
32
61
|
end
|
|
33
62
|
end
|
|
34
63
|
|
|
35
64
|
Tool :service_status do
|
|
36
65
|
description 'Check current status of a service'
|
|
37
|
-
input service_name: { type:
|
|
66
|
+
input service_name: { type: 'string', description: 'Service name to inspect', required: true }
|
|
38
67
|
call do |inputs|
|
|
39
68
|
service_name = inputs[:service_name]
|
|
40
69
|
service = SERVICES[service_name]
|
|
@@ -50,14 +79,14 @@ end
|
|
|
50
79
|
|
|
51
80
|
Tool :restart_service do
|
|
52
81
|
description 'Restart a service'
|
|
53
|
-
input service_name: { type:
|
|
54
|
-
force: { type:
|
|
82
|
+
input service_name: { type: 'string', description: 'Service name to restart', required: true },
|
|
83
|
+
force: { type: 'boolean', default: false, description: 'Force restart if graceful fails' }
|
|
55
84
|
call do |inputs|
|
|
56
85
|
service_name = inputs[:service_name]
|
|
57
86
|
service = SERVICES[service_name]
|
|
58
87
|
rpc_error!(-32000, "Unknown service #{service_name}") unless service
|
|
59
88
|
|
|
60
|
-
service[:status] =
|
|
89
|
+
service[:status] = 'running'
|
|
61
90
|
service[:last_restart] = Time.now
|
|
62
91
|
service[:uptime] = 0
|
|
63
92
|
{
|
|
@@ -4,6 +4,21 @@ require_relative 'mcp_tool_registry'
|
|
|
4
4
|
MCP_PROCESSOR = McpProcessor.new
|
|
5
5
|
|
|
6
6
|
module McpHelper
|
|
7
|
+
VALID_TRANSPORTS = [:sse, :json].freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def transport
|
|
11
|
+
@transport ||= :sse
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def transport=(value)
|
|
15
|
+
value = value.to_sym
|
|
16
|
+
raise ArgumentError, "Unknown MCP transport: #{value}" unless VALID_TRANSPORTS.include?(value)
|
|
17
|
+
|
|
18
|
+
@transport = value
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
7
22
|
def self.included(base)
|
|
8
23
|
base.class_eval do
|
|
9
24
|
|
|
@@ -18,28 +33,34 @@ module McpHelper
|
|
|
18
33
|
end
|
|
19
34
|
|
|
20
35
|
post '/mcp' do
|
|
21
|
-
content_type 'text/event-stream'
|
|
22
|
-
headers['Cache-Control'] = 'no-cache'
|
|
23
|
-
headers['X-Accel-Buffering'] = 'no'
|
|
24
|
-
headers['mcp-session-id'] = SecureRandom.uuid
|
|
25
36
|
request.body&.rewind
|
|
26
|
-
body = request.body.read.to_s
|
|
27
37
|
|
|
28
38
|
response_body =
|
|
29
39
|
begin
|
|
30
|
-
MCP_PROCESSOR.rpc_endpoint(body)
|
|
40
|
+
MCP_PROCESSOR.rpc_endpoint(request.body.read.to_s)
|
|
31
41
|
rescue McpProcessor::ParseError => e
|
|
32
42
|
status e.status
|
|
33
43
|
e.body
|
|
34
44
|
end
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
if response_body.nil?
|
|
47
|
+
status 202
|
|
48
|
+
headers 'Content-Length' => '0'
|
|
49
|
+
''
|
|
50
|
+
elsif McpHelper.transport == :json
|
|
51
|
+
content_type :json
|
|
52
|
+
response_body
|
|
53
|
+
else
|
|
54
|
+
content_type 'text/event-stream'
|
|
55
|
+
headers['Cache-Control'] = 'no-cache'
|
|
56
|
+
headers['X-Accel-Buffering'] = 'no'
|
|
57
|
+
headers['mcp-session-id'] = SecureRandom.uuid
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
stream true do |s|
|
|
60
|
+
s.callback { LOGGER.debug "stream closed: #{s}" }
|
|
61
|
+
s << ['event: message', "data: #{response_body}", '', ''].join($/)
|
|
62
|
+
s.close
|
|
63
|
+
end
|
|
43
64
|
end
|
|
44
65
|
end
|
|
45
66
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
1
3
|
class JsonRpcError < StandardError
|
|
2
4
|
attr_reader :code
|
|
3
5
|
|
|
@@ -15,6 +17,11 @@ end
|
|
|
15
17
|
|
|
16
18
|
class McpProcessor
|
|
17
19
|
PROTOCOL_VERSION = '2025-06-18'
|
|
20
|
+
DEFAULT_SERVER_INFO = {
|
|
21
|
+
name: 'mcp-server',
|
|
22
|
+
title: 'MCP Server',
|
|
23
|
+
version: '1.0.0'
|
|
24
|
+
}.freeze
|
|
18
25
|
|
|
19
26
|
include RpcErrorHelpers
|
|
20
27
|
ParseError = Class.new(StandardError) do
|
|
@@ -23,11 +30,13 @@ class McpProcessor
|
|
|
23
30
|
def initialize(body:, status:)
|
|
24
31
|
@body = body
|
|
25
32
|
@status = status
|
|
26
|
-
super(
|
|
33
|
+
super('MCP parse error')
|
|
27
34
|
end
|
|
28
35
|
end
|
|
29
36
|
|
|
30
|
-
def initialize(logger: LOGGER)
|
|
37
|
+
def initialize(registry: nil, server_info: DEFAULT_SERVER_INFO, logger: (defined?(LOGGER) ? LOGGER : nil))
|
|
38
|
+
@registry = registry
|
|
39
|
+
@server_info = server_info
|
|
31
40
|
@logger = logger
|
|
32
41
|
end
|
|
33
42
|
|
|
@@ -37,14 +46,22 @@ class McpProcessor
|
|
|
37
46
|
|
|
38
47
|
def rpc_endpoint(raw_body)
|
|
39
48
|
req = JSON.parse(raw_body.to_s)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
method = req['method']
|
|
50
|
+
params = req['params']
|
|
51
|
+
|
|
52
|
+
if req.key?('id')
|
|
53
|
+
rpc_response(id: req['id'], method: method, params: params)
|
|
54
|
+
else
|
|
55
|
+
notification_response(method: method, params: params)
|
|
56
|
+
end
|
|
57
|
+
rescue JSON::ParserError => e
|
|
58
|
+
@logger&.warn("MCP JSON parse failed: #{e.message}")
|
|
59
|
+
body = error_response(id: nil, code: -32_700, message: 'Parse error')
|
|
43
60
|
raise ParseError.new(body: body, status: 400)
|
|
44
61
|
end
|
|
45
62
|
|
|
46
63
|
def list_tools
|
|
47
|
-
{ tools:
|
|
64
|
+
{ tools: registry.list, nextCursor: 'no-more' }
|
|
48
65
|
end
|
|
49
66
|
|
|
50
67
|
def root_response
|
|
@@ -56,32 +73,47 @@ class McpProcessor
|
|
|
56
73
|
end
|
|
57
74
|
|
|
58
75
|
def rpc_response(id:, method:, params:)
|
|
59
|
-
json_rpc_response(id: id) {
|
|
76
|
+
json_rpc_response(id: id) { handle(method: method, params: params) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def notification_response(method:, params:)
|
|
80
|
+
handle_notification(method: method, params: params)
|
|
81
|
+
nil
|
|
82
|
+
rescue => e
|
|
83
|
+
@logger&.error("Unhandled MCP notification error: #{e.class}: #{e.message}")
|
|
84
|
+
nil
|
|
60
85
|
end
|
|
61
86
|
|
|
62
|
-
def handle(method:, params
|
|
87
|
+
def handle(method:, params:)
|
|
63
88
|
case method
|
|
64
|
-
when
|
|
65
|
-
# when
|
|
66
|
-
# when
|
|
67
|
-
when
|
|
68
|
-
when
|
|
69
|
-
when
|
|
70
|
-
when
|
|
89
|
+
when 'tools/list' then list_tools
|
|
90
|
+
# when 'resources/list' then {}
|
|
91
|
+
# when 'prompts/list' then {}
|
|
92
|
+
when 'tools/call' then call_tool(params || {})
|
|
93
|
+
when 'initialize' then initialize_response
|
|
94
|
+
when 'notifications/initialized' then @logger&.debug(params); {}
|
|
95
|
+
when 'logging/setLevel' then @logger&.debug(params); {}
|
|
71
96
|
else
|
|
72
|
-
rpc_error!(-
|
|
97
|
+
rpc_error!(-32_601, "Unknown method #{method}")
|
|
73
98
|
end
|
|
74
99
|
end
|
|
75
100
|
|
|
76
|
-
|
|
101
|
+
def handle_notification(method:, params:)
|
|
102
|
+
case method
|
|
103
|
+
when 'notifications/initialized', 'notifications/cancelled'
|
|
104
|
+
@logger&.debug("MCP notification accepted: #{method}")
|
|
105
|
+
else
|
|
106
|
+
@logger&.debug("MCP notification ignored: #{method}")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
77
109
|
|
|
78
|
-
def initialize_(
|
|
110
|
+
def initialize_(_body = nil)
|
|
111
|
+
initialize_response
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def initialize_response
|
|
79
115
|
{
|
|
80
|
-
serverInfo:
|
|
81
|
-
name: 'mcp-server',
|
|
82
|
-
title: 'MCP Server',
|
|
83
|
-
version: '1.0.0'
|
|
84
|
-
},
|
|
116
|
+
serverInfo: @server_info,
|
|
85
117
|
protocolVersion: PROTOCOL_VERSION,
|
|
86
118
|
capabilities: {
|
|
87
119
|
logging: {},
|
|
@@ -95,16 +127,16 @@ class McpProcessor
|
|
|
95
127
|
private
|
|
96
128
|
|
|
97
129
|
def json_rpc_response(id:)
|
|
98
|
-
body = { jsonrpc:
|
|
130
|
+
body = { jsonrpc: '2.0', id: id }
|
|
99
131
|
|
|
100
132
|
begin
|
|
101
|
-
result = yield
|
|
133
|
+
result = yield
|
|
102
134
|
body[:result] = result unless body[:error] || result.nil?
|
|
103
135
|
rescue JsonRpcError => e
|
|
104
136
|
body[:error] = { code: e.code, message: e.message }
|
|
105
137
|
rescue => e
|
|
106
138
|
@logger&.error("Unhandled RPC error: #{e.class}: #{e.message}\n#{e.backtrace&.first}")
|
|
107
|
-
body[:error] = { code: -
|
|
139
|
+
body[:error] = { code: -32_603, message: 'Internal error' }
|
|
108
140
|
end
|
|
109
141
|
|
|
110
142
|
body.delete(:result) if body[:error]
|
|
@@ -112,13 +144,31 @@ class McpProcessor
|
|
|
112
144
|
end
|
|
113
145
|
|
|
114
146
|
def call_tool(params)
|
|
115
|
-
name = params[
|
|
116
|
-
arguments = params[
|
|
117
|
-
tool =
|
|
147
|
+
name = params['name']
|
|
148
|
+
arguments = params['arguments'] || {}
|
|
149
|
+
tool = registry.fetch(name) || rpc_error!(-32_601, "Unknown tool #{name}")
|
|
118
150
|
response = tool.call_tool(arguments)
|
|
151
|
+
return response if mcp_tool_response?(response)
|
|
152
|
+
|
|
153
|
+
wrap_tool_response(response)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def registry
|
|
157
|
+
@registry || ToolRegistry.default
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def mcp_tool_response?(response)
|
|
161
|
+
return false unless response.is_a?(Hash)
|
|
162
|
+
|
|
163
|
+
[:content, :structuredContent, :isError, 'content', 'structuredContent', 'isError'].any? do |key|
|
|
164
|
+
response.key?(key)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def wrap_tool_response(response)
|
|
119
169
|
{
|
|
120
170
|
content: [
|
|
121
|
-
{
|
|
171
|
+
{ 'type' => 'text', 'text' => response.is_a?(String) ? response : JSON.dump(response) }
|
|
122
172
|
],
|
|
123
173
|
isError: false
|
|
124
174
|
}
|
|
@@ -3,12 +3,35 @@ module ToolRegistry
|
|
|
3
3
|
include RpcErrorHelpers
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
+
class Registry
|
|
7
|
+
attr_reader :registry
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@registry = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def define(name, &block)
|
|
14
|
+
definition = Definition.new(name)
|
|
15
|
+
definition.instance_eval(&block)
|
|
16
|
+
@registry[definition.name] = definition
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def list
|
|
20
|
+
registry.values.map(&:to_h)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch(name)
|
|
24
|
+
registry[name.to_s]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
6
28
|
class Definition
|
|
7
29
|
attr_reader :name
|
|
8
30
|
|
|
9
31
|
def initialize(name)
|
|
10
32
|
@name = name.to_s
|
|
11
33
|
@inputs = {}
|
|
34
|
+
@annotations = {}
|
|
12
35
|
end
|
|
13
36
|
|
|
14
37
|
def description(text = nil)
|
|
@@ -20,62 +43,89 @@ module ToolRegistry
|
|
|
20
43
|
@inputs.merge!(fields.transform_keys(&:to_s))
|
|
21
44
|
end
|
|
22
45
|
|
|
23
|
-
def input_schema
|
|
24
|
-
|
|
25
|
-
required = []
|
|
46
|
+
def input_schema(schema = nil)
|
|
47
|
+
return @input_schema || build_input_schema if schema.nil?
|
|
26
48
|
|
|
27
|
-
@
|
|
28
|
-
|
|
29
|
-
required << field if cfg.delete(:required)
|
|
30
|
-
properties[field] = cfg.transform_keys(&:to_s)
|
|
31
|
-
end
|
|
49
|
+
@input_schema = stringify_keys(schema)
|
|
50
|
+
end
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
def annotations(value = nil)
|
|
53
|
+
return @annotations if value.nil?
|
|
54
|
+
|
|
55
|
+
@annotations = stringify_keys(value)
|
|
34
56
|
end
|
|
35
57
|
|
|
36
58
|
def call(&block) = @executor = block
|
|
37
59
|
|
|
38
60
|
def to_h
|
|
39
|
-
{
|
|
61
|
+
payload = {
|
|
40
62
|
name: name,
|
|
41
63
|
description: description,
|
|
42
64
|
inputSchema: input_schema
|
|
43
65
|
}
|
|
66
|
+
payload[:annotations] = annotations unless annotations.empty?
|
|
67
|
+
payload
|
|
44
68
|
end
|
|
45
69
|
|
|
46
70
|
def call_tool(arguments)
|
|
47
|
-
raise JsonRpcError.new(code:
|
|
71
|
+
raise JsonRpcError.new(code: -32603, message: "Tool #{name} missing executor") unless @executor
|
|
48
72
|
|
|
49
73
|
ExecutionContext.new.instance_exec(symbolize_keys(arguments || {}), &@executor)
|
|
50
74
|
end
|
|
51
75
|
|
|
52
76
|
private
|
|
53
77
|
|
|
78
|
+
def build_input_schema
|
|
79
|
+
properties = {}
|
|
80
|
+
required = []
|
|
81
|
+
|
|
82
|
+
@inputs.each do |field, config|
|
|
83
|
+
cfg = stringify_keys(config)
|
|
84
|
+
required << field if cfg.delete('required')
|
|
85
|
+
properties[field] = cfg
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
{ type: 'object', properties: properties, required: required }
|
|
89
|
+
end
|
|
90
|
+
|
|
54
91
|
def symbolize_keys(hash)
|
|
55
92
|
hash.each_with_object({}) do |(key, value), memo|
|
|
56
93
|
memo[key.to_sym] = value
|
|
57
94
|
end
|
|
58
95
|
end
|
|
96
|
+
|
|
97
|
+
def stringify_keys(value)
|
|
98
|
+
case value
|
|
99
|
+
when Hash
|
|
100
|
+
value.each_with_object({}) { |(key, item), memo| memo[key.to_s] = stringify_keys(item) }
|
|
101
|
+
when Array
|
|
102
|
+
value.map { |item| stringify_keys(item) }
|
|
103
|
+
else
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
59
107
|
end
|
|
60
108
|
|
|
61
109
|
module_function
|
|
62
110
|
|
|
111
|
+
def default
|
|
112
|
+
@default ||= Registry.new
|
|
113
|
+
end
|
|
114
|
+
|
|
63
115
|
def define(name, &block)
|
|
64
|
-
|
|
65
|
-
definition.instance_eval(&block)
|
|
66
|
-
registry[definition.name] = definition
|
|
116
|
+
default.define(name, &block)
|
|
67
117
|
end
|
|
68
118
|
|
|
69
119
|
def registry
|
|
70
|
-
|
|
120
|
+
default.registry
|
|
71
121
|
end
|
|
72
122
|
|
|
73
123
|
def list
|
|
74
|
-
|
|
124
|
+
default.list
|
|
75
125
|
end
|
|
76
126
|
|
|
77
127
|
def fetch(name)
|
|
78
|
-
|
|
128
|
+
default.fetch(name)
|
|
79
129
|
end
|
|
80
130
|
end
|
|
81
131
|
|
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
stages:
|
|
2
|
+
- test
|
|
2
3
|
- build
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
image: docker:
|
|
5
|
+
.build_block: &build_block
|
|
6
|
+
# tags: [ build ]
|
|
7
|
+
image: docker:27.5.1-cli
|
|
7
8
|
services:
|
|
8
|
-
- docker:dind
|
|
9
|
+
- name: docker:27.5.1-dind
|
|
10
|
+
alias: docker
|
|
9
11
|
variables:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
before_script:
|
|
13
|
-
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
12
|
+
DOCKER_HOST: tcp://docker:2375
|
|
13
|
+
DOCKER_TLS_CERTDIR: ""
|
|
14
14
|
script:
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
15
|
+
- export CI_COMMIT_TAG="${CI_COMMIT_TAG:-0.0.0}"
|
|
16
|
+
- |
|
|
17
|
+
if [ -n "${CI_REGISTRY:-}" ] && [ -n "${CI_REGISTRY_USER:-}" ] && [ -n "${CI_REGISTRY_PASSWORD:-}" ]; then
|
|
18
|
+
echo "${CI_REGISTRY_PASSWORD}" | docker login "${CI_REGISTRY}" -u "${CI_REGISTRY_USER}" --password-stdin
|
|
19
|
+
fi
|
|
20
|
+
- docker buildx create --name "${CI_PROJECT_NAME}-wrapper-builder" --driver docker-container --use || docker buildx use "${CI_PROJECT_NAME}-wrapper-builder"
|
|
21
|
+
- docker buildx inspect --bootstrap
|
|
22
|
+
- |
|
|
23
|
+
docker buildx build --load \
|
|
24
|
+
-t build/${CI_PROJECT_NAME} \
|
|
25
|
+
-f docker/Dockerfile.build \
|
|
26
|
+
--cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/ci-wrapper:${CI_COMMIT_REF_SLUG}-cache \
|
|
27
|
+
--cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/ci-wrapper:${CI_COMMIT_REF_SLUG}-cache,mode=max \
|
|
28
|
+
.
|
|
29
|
+
- docker run --rm `env | grep -o '^CI_[^=]*' | sed 's/^/-e /'`
|
|
30
|
+
-e REGISTRY_HOST=$CI_REGISTRY/$CI_PROJECT_NAMESPACE
|
|
31
|
+
-v /var/run/docker.sock:/var/run/docker.sock
|
|
32
|
+
-v /root/.docker:/root/.docker
|
|
33
|
+
build/${CI_PROJECT_NAME} 2>&1
|
|
34
|
+
|
|
35
|
+
build push:
|
|
36
|
+
<<: *build_block
|
|
37
|
+
stage: build
|
|
38
|
+
needs: [ tests ]
|
|
39
|
+
|
|
40
|
+
tests:
|
|
41
|
+
<<: *build_block
|
|
42
|
+
stage: test
|
|
43
|
+
variables:
|
|
44
|
+
DOCKER_HOST: tcp://docker:2375
|
|
45
|
+
DOCKER_TLS_CERTDIR: ""
|
|
46
|
+
CI_SKIP_PUSH: true
|
|
47
|
+
# build-labels sets target for each service in docker-compose.yml
|
|
48
|
+
CI_BUILD_TARGET: tests
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
FROM ruby:3.4.4-slim-bookworm AS base
|
|
2
|
+
RUN apt-get update && apt-get install -y --no-install-recommends bash ca-certificates curl git tar \
|
|
3
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
4
|
+
RUN gem install build-labels:0.0.79
|
|
5
|
+
RUN BUILDX_VERSION=v0.20.1 COMPOSE_VERSION=v2.33.0 install-docker-static 27.5.1
|
|
6
|
+
|
|
7
|
+
WORKDIR /build
|
|
8
|
+
|
|
9
|
+
COPY .. .
|
|
10
|
+
|
|
11
|
+
CMD ["bash", "-euo", "pipefail", "-c", "\
|
|
12
|
+
cd /build/docker; \
|
|
13
|
+
if [ -n \"${CI_REGISTRY:-}\" ] && [ -n \"${CI_REGISTRY_USER:-}\" ] && [ -n \"${CI_REGISTRY_PASSWORD:-}\" ]; then \
|
|
14
|
+
echo \"$CI_REGISTRY_PASSWORD\" | docker login \"$CI_REGISTRY\" -u \"$CI_REGISTRY_USER\" --password-stdin; \
|
|
15
|
+
fi; \
|
|
16
|
+
build-labels -n -c docker-compose.yml changed gitlab set_version cache to_dockerfiles to_compose | tee bake.yml; \
|
|
17
|
+
export OTEL_RESOURCE_ATTRIBUTES=\"service.name=docker-builder,pipeline.id=${CI_PIPELINE_ID:-local},project.name=${service_name}\"; \
|
|
18
|
+
export REGISTRY_HOST=\"${REGISTRY_HOST:-${CI_REGISTRY_HOST:-${CI_REGISTRY_IMAGE:-}}}\"; \
|
|
19
|
+
: \"${REGISTRY_HOST:?REGISTRY_HOST, CI_REGISTRY_IMAGE, or CI_REGISTRY_HOST is required}\"; \
|
|
20
|
+
export BUILDX_BAKE_ENTITLEMENTS_FS=0; \
|
|
21
|
+
if grep -q \"services: {}\" bake.yml; then \
|
|
22
|
+
echo \"No changed services to build.\"; \
|
|
23
|
+
exit 0; \
|
|
24
|
+
fi; \
|
|
25
|
+
docker buildx create --name \"${service_name}-builder\" --driver docker-container --use || docker buildx use \"${service_name}-builder\"; \
|
|
26
|
+
docker buildx inspect --bootstrap; \
|
|
27
|
+
if [ -n \"${CI_SKIP_PUSH:-}\" ]; then \
|
|
28
|
+
docker buildx bake --load --allow=fs.read=../src -f bake.yml; \
|
|
29
|
+
else \
|
|
30
|
+
docker buildx bake --push --allow=fs.read=../src -f bake.yml; \
|
|
31
|
+
fi; \
|
|
32
|
+
if [ \"${CI_BUILD_TARGET:-}\" = \"tests\" ]; then \
|
|
33
|
+
docker compose -f bake.yml down --remove-orphans; \
|
|
34
|
+
docker compose -f bake.yml up --no-build --force-recreate --abort-on-container-failure; \
|
|
35
|
+
fi \
|
|
36
|
+
"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export CI_PROJECT_NAME=${service_name}
|
|
2
|
+
export CI_PIPELINE_ID=local
|
|
3
|
+
export CI_PIPELINE_IID=0
|
|
4
|
+
export CI_REGISTRY_HOST=localhost
|
|
5
|
+
export CI_SKIP_PUSH=true
|
|
6
|
+
|
|
7
|
+
# -v /root/.docker:/root/.docker \
|
|
8
|
+
docker run --rm --env-file <(env | grep ^CI_) \
|
|
9
|
+
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
10
|
+
$(docker build -q -t build/${CI_PROJECT_NAME} -f Dockerfile.build ..) 2>&1
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stack-service-base
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.99
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artyom B
|
|
@@ -433,7 +433,9 @@ files:
|
|
|
433
433
|
- lib/stack-service-base/project_template/gitlab-c/docker/docker-compose.yml
|
|
434
434
|
- lib/stack-service-base/project_template/gitlab-c/docker/local_build.sh
|
|
435
435
|
- lib/stack-service-base/project_template/gitlab/.gitlab-ci.yml
|
|
436
|
+
- lib/stack-service-base/project_template/gitlab/docker/Dockerfile.build
|
|
436
437
|
- lib/stack-service-base/project_template/gitlab/docker/docker-compose.yml
|
|
438
|
+
- lib/stack-service-base/project_template/gitlab/docker/local_build.sh
|
|
437
439
|
- lib/stack-service-base/project_template/home/.gitignore
|
|
438
440
|
- lib/stack-service-base/project_template/home/AGENTS.md
|
|
439
441
|
- lib/stack-service-base/project_template/home/docker/.env
|