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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a29686255562ea2b36806d5d50db270dda9c65d6b9d819f0f9c241a3a4f85049
4
- data.tar.gz: 2e33c64c8f927a7ac034c39d2b2ea1b5118bedb3315d44d2265f80d463fafdad
3
+ metadata.gz: 3db078c7ea6acaca31ec7b32b7991fd213dcaacd03fcab42733f8f3d299b6456
4
+ data.tar.gz: 621b4b6c3b116f0f72555740ca83f9f62029e0a58b3d19655fdf77db45f27a5b
5
5
  SHA512:
6
- metadata.gz: 0f8d15d2b3e8c33e8e321464df2068351da9e6fdb5b0439405635ed7876f4fb014d9525284a87c6237ee13c4731cc177ebfd0f5328f9d44fcc8a32b36f811ff9
7
- data.tar.gz: ea9223839e6ccabe433121647be6a8c93ce62d6d807c482aad9c3a1fdf48b18a226a8d7eb1449b71c7ab93e41c4c390f7379d092e6403de8136d0249e3ce4647
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('**/*').each do |file|
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
- "database-backend" => {
8
- status: "running",
9
- uptime: 72 * 3600, # seconds
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: "string", description: "Term to search for", required: true }
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:"doc-1",title:"...",url:"..."}] }
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: "string", description: "Resource ID to fetch", required: true }
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: "doc-1", title: "...", text: "full text...", url: "https://example.com/doc", metadata: { source: "vector_store" } }
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: "string", description: "Service name to inspect", required: true }
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: "string", description: "Service name to restart", required: true },
54
- force: { type: "boolean", default: false, description: "Force restart if graceful fails" }
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] = "running"
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
- LOGGER.debug "request body: #{body}"
37
- LOGGER.debug "response body: #{response_body}"
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
- stream true do |s|
40
- s.callback { LOGGER.debug "stream closed: #{s}" }
41
- s << "event: message\ndata: #{response_body}\n\n"
42
- s.close
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("MCP parse error")
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
- rpc_response(id: req["id"], method: req["method"], params: req["params"])
41
- rescue JSON::ParserError
42
- body = error_response(id: nil, code: -32700, message: "Parse error")
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: ToolRegistry.list, nextCursor: 'no-more' }
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) { |body| handle(method: method, params: params, body: body) }
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:, body: )
87
+ def handle(method:, params:)
63
88
  case method
64
- when "tools/list" then list_tools
65
- # when "resources/list" then {}
66
- # when "prompts/list" then {}
67
- when "tools/call" then call_tool(params || {})
68
- when "initialize" then initialize_(body)
69
- when "notifications/initialized" then LOGGER.debug params; {}
70
- when "logging/setLevel" then LOGGER.debug params; {}
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!(-32601, "Unknown method #{method}")
97
+ rpc_error!(-32_601, "Unknown method #{method}")
73
98
  end
74
99
  end
75
100
 
76
- # https://gist.github.com/ruvnet/7b6843c457822cbcf42fc4aa635eadbb
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_(body)
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: "2.0", id: id }
130
+ body = { jsonrpc: '2.0', id: id }
99
131
 
100
132
  begin
101
- result = yield(body)
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: -32603, message: "Internal error" }
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["name"]
116
- arguments = params["arguments"] || {}
117
- tool = ToolRegistry.fetch(name) || rpc_error!(-32601, "Unknown tool #{name}")
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
- { "type": "text", "text": response.is_a?(String) ? response : response.to_json }
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
- properties = {}
25
- required = []
46
+ def input_schema(schema = nil)
47
+ return @input_schema || build_input_schema if schema.nil?
26
48
 
27
- @inputs.each do |field, config|
28
- cfg = config.transform_keys { |key| key.is_a?(Symbol) ? key : key.to_sym }
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
- { type: 'object', properties: properties, required: required }
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: 500, message: "Tool #{name} missing executor") unless @executor
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
- definition = Definition.new(name)
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
- @registry ||= {}
120
+ default.registry
71
121
  end
72
122
 
73
123
  def list
74
- registry.values.map(&:to_h)
124
+ default.list
75
125
  end
76
126
 
77
127
  def fetch(name)
78
- registry[name.to_s]
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
- build-job:
5
- stage: build
6
- image: docker:latest
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
- DOCKER_TLS_CERTDIR: "/certs"
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
- - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -t $CI_REGISTRY_IMAGE:latest -f docker/Dockerfile ./src
16
- - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
17
- - docker push $CI_REGISTRY_IMAGE:latest
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
+ "]
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  ${service_name}:
3
- image: ${REGISTRY_HOST}/${service_name}
3
+ image: ${REGISTRY_HOST}/${service_name}/${CI_COMMIT_REF_SLUG:-local}
4
4
  build:
5
5
  context: ../src
6
6
  additional_contexts:
@@ -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
@@ -1,3 +1,4 @@
1
+ # Long ....
1
2
  stages:
2
3
  - deploy
3
4
 
@@ -1,3 +1,3 @@
1
1
  module StackServiceBase
2
- VERSION = '0.0.97'
2
+ VERSION = '0.0.99'
3
3
  end
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.97
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