github_snap_builder 0.1.0
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 +7 -0
- data/.gitignore +15 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +64 -0
- data/README.md +35 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/github_snap_builder +5 -0
- data/bin/github_snap_builder_config_validator +17 -0
- data/bin/setup +8 -0
- data/configure +20 -0
- data/github_snap_builder.gemspec +45 -0
- data/lib/github_snap_builder/builder_implementations/docker.rb +87 -0
- data/lib/github_snap_builder/config.rb +143 -0
- data/lib/github_snap_builder/server.rb +254 -0
- data/lib/github_snap_builder/snap_builder.rb +89 -0
- data/lib/github_snap_builder/version.rb +3 -0
- data/lib/github_snap_builder.rb +69 -0
- data/sample-config.yaml +35 -0
- data/tests/builder_implementations/docker_test.rb +85 -0
- data/tests/config_test.rb +164 -0
- data/tests/snap_builder_test.rb +114 -0
- data/tests/test_helpers.rb +16 -0
- metadata +212 -0
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'octokit'
|
3
|
+
require 'json'
|
4
|
+
require 'openssl' # Verifies the webhook signature
|
5
|
+
require 'jwt' # Authenticates a GitHub App
|
6
|
+
require 'time' # Gets ISO 8601 representation of a Time object
|
7
|
+
require 'logger' # Logs debug statements
|
8
|
+
require 'yaml'
|
9
|
+
require 'github_snap_builder/config'
|
10
|
+
require 'github_snap_builder/snap_builder'
|
11
|
+
|
12
|
+
module GithubSnapBuilder
|
13
|
+
if ENV.include? 'SNAP_BUILDER_CONFIG'
|
14
|
+
CONFIG = Config.new(File.read(ENV['SNAP_BUILDER_CONFIG']))
|
15
|
+
else
|
16
|
+
CONFIG = Config.new('{}')
|
17
|
+
end
|
18
|
+
|
19
|
+
class Application < Sinatra::Application
|
20
|
+
set :port, CONFIG.port
|
21
|
+
set :bind, CONFIG.bind
|
22
|
+
|
23
|
+
# Converts the newlines. Expects that the private key has been set as an
|
24
|
+
# environment variable in PEM format.
|
25
|
+
PRIVATE_KEY = OpenSSL::PKey::RSA.new(CONFIG.github_app_private_key.gsub('\n', "\n")) if CONFIG.valid?
|
26
|
+
|
27
|
+
# Your registered app must have a secret set. The secret is used to verify
|
28
|
+
# that webhooks are sent by GitHub.
|
29
|
+
WEBHOOK_SECRET = CONFIG.github_webhook_secret
|
30
|
+
|
31
|
+
# The GitHub App's identifier (type integer) set when registering an app.
|
32
|
+
APP_IDENTIFIER = CONFIG.github_app_id
|
33
|
+
|
34
|
+
# Turn on Sinatra's verbose logging during development
|
35
|
+
configure :development do
|
36
|
+
set :logging, Logger::DEBUG
|
37
|
+
end
|
38
|
+
|
39
|
+
# Executed before each request to the `/event_handler` route
|
40
|
+
before '/event_handler' do
|
41
|
+
get_payload_request(request)
|
42
|
+
verify_webhook_signature
|
43
|
+
authenticate_app
|
44
|
+
# Authenticate the app installation in order to run API operations
|
45
|
+
authenticate_installation(@payload)
|
46
|
+
end
|
47
|
+
|
48
|
+
post '/event_handler' do
|
49
|
+
if !CONFIG.valid?
|
50
|
+
logger.info "Invalid config, ignoring event..."
|
51
|
+
return 200
|
52
|
+
end
|
53
|
+
|
54
|
+
case request.env['HTTP_X_GITHUB_EVENT']
|
55
|
+
when 'pull_request'
|
56
|
+
repo = @payload['pull_request']['base']['repo']['full_name']
|
57
|
+
unless CONFIG.repos_include? repo
|
58
|
+
logger.info "Not configured for repo '#{repo}'. Ignoring event..."
|
59
|
+
return 200 # Ignored, but still successful
|
60
|
+
end
|
61
|
+
|
62
|
+
case @payload['action']
|
63
|
+
when "opened", "reopened", "synchronize"
|
64
|
+
handle_pull_request_updated_event(@payload)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
200 # success status
|
69
|
+
end
|
70
|
+
|
71
|
+
get '/logs/*' do
|
72
|
+
logfile = File.join logdir, params[:splat][0]
|
73
|
+
if File.file? logfile
|
74
|
+
send_file logfile
|
75
|
+
else
|
76
|
+
404
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
helpers do
|
82
|
+
|
83
|
+
def handle_pull_request_updated_event(payload)
|
84
|
+
pull_request = payload['pull_request']
|
85
|
+
repo = pull_request['base']['repo']['full_name']
|
86
|
+
head_url = pull_request['head']['repo']['html_url']
|
87
|
+
base_url = pull_request['base']['repo']['html_url']
|
88
|
+
commit_sha = pull_request['head']['sha']
|
89
|
+
pr_number = pull_request['number']
|
90
|
+
|
91
|
+
repo_config = CONFIG.repo(repo) || raise("Config missing repo definition for '#{repo}'")
|
92
|
+
channel = repo_config.channel
|
93
|
+
token = repo_config.token || raise("'#{repo}' config missing token")
|
94
|
+
|
95
|
+
relative_logfile_path = File.join repo, "#{commit_sha}.log"
|
96
|
+
logfile_path = File.join(logdir, relative_logfile_path)
|
97
|
+
FileUtils.mkpath File.dirname(logfile_path)
|
98
|
+
FileUtils.chmod_R 0755, File.dirname(logfile_path)
|
99
|
+
build_logger = Logger.new(logfile_path)
|
100
|
+
log_url = request.url.gsub 'event_handler', "logs/#{relative_logfile_path}"
|
101
|
+
status_reporter = GithubStatusReporter.new(@installation_client, repo, commit_sha, log_url)
|
102
|
+
|
103
|
+
begin
|
104
|
+
begin
|
105
|
+
status_reporter.pending("Currently building a snap...")
|
106
|
+
|
107
|
+
builder = SnapBuilder.new(build_logger, base_url, head_url, commit_sha, CONFIG.build_type)
|
108
|
+
logger.info "Building snap for '#{repo}'"
|
109
|
+
snap_path = builder.build
|
110
|
+
rescue Error => e
|
111
|
+
logger.error "Failed to build snap: #{e.message}"
|
112
|
+
status_reporter.error("Snap failed to build.")
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
begin
|
117
|
+
status_reporter.pending("Currently uploading/releasing snap...")
|
118
|
+
|
119
|
+
full_channel = "#{channel}/pr-#{pr_number}"
|
120
|
+
logger.info "Pushing and releasing snap into '#{full_channel}'"
|
121
|
+
builder.release(snap_path, token, full_channel)
|
122
|
+
rescue Error => e
|
123
|
+
logger.error "Failed to push/release snap: #{e.message}"
|
124
|
+
status_reporter.error("Snap failed to upload/release.")
|
125
|
+
return
|
126
|
+
end
|
127
|
+
|
128
|
+
logger.info 'Built and released snap, all done'
|
129
|
+
status_reporter.success("Snap built and released to '#{full_channel}'")
|
130
|
+
rescue => e
|
131
|
+
logger.error "Unknown error: #{e.message}"
|
132
|
+
status_reporter.error("Encountered an error.")
|
133
|
+
raise
|
134
|
+
ensure
|
135
|
+
if !snap_path.nil? and File.file? snap_path
|
136
|
+
File.delete snap_path
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Saves the raw payload and converts the payload to JSON format
|
142
|
+
def get_payload_request(request)
|
143
|
+
# request.body is an IO or StringIO object
|
144
|
+
# Rewind in case someone already read it
|
145
|
+
request.body.rewind
|
146
|
+
# The raw text of the body is required for webhook signature verification
|
147
|
+
@payload_raw = request.body.read
|
148
|
+
begin
|
149
|
+
@payload = JSON.parse @payload_raw
|
150
|
+
rescue => e
|
151
|
+
fail "Invalid JSON (#{e}): #{@payload_raw}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Instantiate an Octokit client authenticated as a GitHub App.
|
156
|
+
# GitHub App authentication requires that you construct a
|
157
|
+
# JWT (https://jwt.io/introduction/) signed with the app's private key,
|
158
|
+
# so GitHub can be sure that it came from the app an not altererd by
|
159
|
+
# a malicious third party.
|
160
|
+
def authenticate_app
|
161
|
+
payload = {
|
162
|
+
# The time that this JWT was issued, _i.e._ now.
|
163
|
+
iat: Time.now.to_i,
|
164
|
+
|
165
|
+
# JWT expiration time (10 minute maximum)
|
166
|
+
exp: Time.now.to_i + (10 * 60),
|
167
|
+
|
168
|
+
# Your GitHub App's identifier number
|
169
|
+
iss: APP_IDENTIFIER
|
170
|
+
}
|
171
|
+
|
172
|
+
# Cryptographically sign the JWT.
|
173
|
+
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
|
174
|
+
|
175
|
+
# Create the Octokit client, using the JWT as the auth token.
|
176
|
+
@app_client ||= Octokit::Client.new(bearer_token: jwt)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Instantiate an Octokit client, authenticated as an installation of a
|
180
|
+
# GitHub App, to run API operations.
|
181
|
+
def authenticate_installation(payload)
|
182
|
+
@installation_id = payload['installation']['id']
|
183
|
+
@installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
|
184
|
+
@installation_client = Octokit::Client.new(bearer_token: @installation_token)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Check X-Hub-Signature to confirm that this webhook was generated by
|
188
|
+
# GitHub, and not a malicious third party.
|
189
|
+
#
|
190
|
+
# GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
|
191
|
+
# create the hash signature sent in the `X-HUB-Signature` header of each
|
192
|
+
# webhook. This code computes the expected hash signature and compares it to
|
193
|
+
# the signature sent in the `X-HUB-Signature` header. If they don't match,
|
194
|
+
# this request is an attack, and you should reject it. GitHub uses the HMAC
|
195
|
+
# hexdigest to compute the signature. The `X-HUB-Signature` looks something
|
196
|
+
# like this: "sha1=123456".
|
197
|
+
# See https://developer.github.com/webhooks/securing/ for details.
|
198
|
+
def verify_webhook_signature
|
199
|
+
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
|
200
|
+
method, their_digest = their_signature_header.split('=')
|
201
|
+
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
|
202
|
+
halt 401 unless their_digest == our_digest
|
203
|
+
|
204
|
+
# The X-GITHUB-EVENT header provides the name of the event.
|
205
|
+
# The action value indicates the which action triggered the event.
|
206
|
+
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
|
207
|
+
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
|
208
|
+
end
|
209
|
+
|
210
|
+
def logdir
|
211
|
+
ENV.fetch("SNAP_BUILDER_LOG_DIR", "log")
|
212
|
+
end
|
213
|
+
|
214
|
+
def logfile_path(repo, commit_sha)
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class GithubStatusReporter
|
221
|
+
def initialize(client, repo, commit_sha, log_url)
|
222
|
+
@client = client
|
223
|
+
@repo = repo
|
224
|
+
@commit_sha = commit_sha
|
225
|
+
@log_url = log_url
|
226
|
+
end
|
227
|
+
|
228
|
+
def pending(message)
|
229
|
+
create_status 'pending', message
|
230
|
+
end
|
231
|
+
|
232
|
+
def success(message)
|
233
|
+
create_status 'success', message
|
234
|
+
end
|
235
|
+
|
236
|
+
def failure(message)
|
237
|
+
create_status 'failure', message
|
238
|
+
end
|
239
|
+
|
240
|
+
def error(message)
|
241
|
+
create_status 'error', message
|
242
|
+
end
|
243
|
+
|
244
|
+
private
|
245
|
+
|
246
|
+
def create_status(state, description)
|
247
|
+
@client.create_status(@repo, @commit_sha, state, {
|
248
|
+
context: "Snap Builder",
|
249
|
+
description: description,
|
250
|
+
target_url: @log_url
|
251
|
+
})
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'yaml'
|
5
|
+
require 'rugged'
|
6
|
+
require 'github_snap_builder'
|
7
|
+
|
8
|
+
Dir[File.join(__dir__, 'builder_implementations', '*.rb')].each {|file| require file }
|
9
|
+
|
10
|
+
module GithubSnapBuilder
|
11
|
+
class SnapBuilder
|
12
|
+
def initialize(logger, base_url, head_url, commit_sha, build_type)
|
13
|
+
@logger = logger
|
14
|
+
@base_url = base_url
|
15
|
+
@head_url = head_url
|
16
|
+
@commit_sha = commit_sha
|
17
|
+
@build_type = build_type
|
18
|
+
@base = 'core16'
|
19
|
+
end
|
20
|
+
|
21
|
+
def build
|
22
|
+
Dir.mktmpdir do |tempdir|
|
23
|
+
# First of all, clone the repository and get on the proper hash. Make
|
24
|
+
# sure to include the base repo so `git describe` has meaning.
|
25
|
+
repo = Rugged::Repository.clone_at(@base_url, tempdir)
|
26
|
+
remote = repo.remotes.create('fork', @head_url)
|
27
|
+
remote.fetch
|
28
|
+
repo.checkout(@commit_sha, {strategy: :force})
|
29
|
+
|
30
|
+
# Before we can actually build the snap, we must first determine the
|
31
|
+
# base to use. The default is "core".
|
32
|
+
snapcraft_yaml = snapcraft_yaml_location(tempdir)
|
33
|
+
@base = YAML.safe_load(File.read(snapcraft_yaml)).fetch("base", @base)
|
34
|
+
if @base == "core"
|
35
|
+
@base = "core16"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Factor out any snaps that existed before we build the new one
|
39
|
+
snaps_glob = File.join(tempdir, '*.snap')
|
40
|
+
existing_snaps = Dir.glob(snaps_glob)
|
41
|
+
|
42
|
+
# Now build the snap
|
43
|
+
build_implementation.build(tempdir)
|
44
|
+
|
45
|
+
# Grab the filename of the snap we just built
|
46
|
+
new_snaps = Dir.glob(snaps_glob) - existing_snaps
|
47
|
+
if new_snaps.empty?
|
48
|
+
raise BuildFailedError
|
49
|
+
elsif new_snaps.length > 1
|
50
|
+
raise TooManySnapsError, new_snaps
|
51
|
+
end
|
52
|
+
|
53
|
+
# The directory we're in right now will be removed shortly. Copy the
|
54
|
+
# snap somewhere that will live forever, and hand it to the Snap class
|
55
|
+
# (which will remove it when it's done with it).
|
56
|
+
snap_file = Tempfile.create [@commit_sha, '.snap']
|
57
|
+
FileUtils.cp new_snaps[0], snap_file
|
58
|
+
snap_file.path
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def release(snap_path, token, channel)
|
63
|
+
build_implementation.release(snap_path, token, channel)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.supported_build_types
|
67
|
+
Dir[File.join(__dir__, 'builder_implementations', '*.rb')].collect do |f|
|
68
|
+
File.basename(f, File.extname(f))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def build_implementation
|
75
|
+
GithubSnapBuilder.const_get("#{@build_type.capitalize}Builder").new(@logger, @base)
|
76
|
+
end
|
77
|
+
|
78
|
+
def snapcraft_yaml_location(project_dir)
|
79
|
+
["snapcraft.yaml", ".snapcraft.yaml", File.join("snap", "snapcraft.yaml")].each do |f|
|
80
|
+
path = File.join(project_dir, f)
|
81
|
+
if File.file? path
|
82
|
+
return path
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
raise MissingSnapcraftYaml
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'github_snap_builder/version'
|
2
|
+
|
3
|
+
module GithubSnapBuilder
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class MissingSnapcraftError < Error
|
7
|
+
def initialize
|
8
|
+
super("snapcraft must be installed and in the PATH")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class MissingSnapFileError < Error
|
13
|
+
def initialize(path)
|
14
|
+
super("unable to find snap with path '#{path}'")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class BuildFailedError < Error
|
19
|
+
def initialize
|
20
|
+
super("build failed")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class TooManySnapsError < Error
|
25
|
+
def initialize(paths)
|
26
|
+
super("expected to find a single snap, found #{paths.length}: #{paths}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class SnapPushError < Error
|
31
|
+
def initialize
|
32
|
+
super("failed to push/release snap")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class AuthenticationError < Error
|
37
|
+
def initialize(message)
|
38
|
+
super("failed to authenticate: #{message}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class MissingSnapcraftYaml < Error
|
43
|
+
def initialize
|
44
|
+
super("unable to find snapcraft.yaml")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class DockerError < Error; end
|
49
|
+
|
50
|
+
class DockerVersionError < DockerError
|
51
|
+
def initialize
|
52
|
+
super("docker is either not installed or is incompatible")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class DockerRunError < DockerError
|
57
|
+
def initialize(command)
|
58
|
+
super("command in docker returned non-zero: #{command}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class ConfigurationError < Error; end
|
63
|
+
|
64
|
+
class ConfigurationFieldError < ConfigurationError
|
65
|
+
def initialize(field)
|
66
|
+
super("configuration field is invalid: '#{field}'")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/sample-config.yaml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# REQUIRED. Your registered app must have a secret set. The secret is used to
|
2
|
+
# verify that webhooks are sent by github and not someone else on the internet.
|
3
|
+
# github_webhook_secret: ""
|
4
|
+
|
5
|
+
# REQUIRED. The ID of the github app
|
6
|
+
# github_app_id: 12345
|
7
|
+
|
8
|
+
# REQUIRED. The private key for the app, including line breaks.
|
9
|
+
# github_app_private_key: |
|
10
|
+
# -----BEGIN RSA PRIVATE KEY-----
|
11
|
+
# nice long key
|
12
|
+
# -----END RSA PRIVATE KEY-----
|
13
|
+
|
14
|
+
# REQUIRED. Specify the type of build. Options are:
|
15
|
+
# - docker: run in an ephemeral docker container
|
16
|
+
# build_type: docker
|
17
|
+
|
18
|
+
# The port on which to listen
|
19
|
+
# port: 3000
|
20
|
+
|
21
|
+
# The address on which to listen
|
22
|
+
# bind: "0.0.0.0"
|
23
|
+
|
24
|
+
# Configure destination channel and credentials for the snap in each repo
|
25
|
+
# repos:
|
26
|
+
# repo_owner/repo_name:
|
27
|
+
# # Snaps will be released to the hotfix channel <channel>/pr-<number>
|
28
|
+
# channel: edge
|
29
|
+
#
|
30
|
+
# # REQUIRED: Snapcraft login token. Generate this with:
|
31
|
+
# # $ snapcraft export-login \
|
32
|
+
# # --snaps=<snap-name> \
|
33
|
+
# # --channels=<channel> \
|
34
|
+
# # --acls=package_push,package_release -
|
35
|
+
# token: ""
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'github_snap_builder/builder_implementations/docker'
|
3
|
+
require_relative '../test_helpers'
|
4
|
+
|
5
|
+
module GithubSnapBuilder
|
6
|
+
class DockerBuilderTest < SnapBuilderBaseTest
|
7
|
+
def setup
|
8
|
+
@mock_image = mock('image')
|
9
|
+
@mock_container = mock('container')
|
10
|
+
@mock_logger = mock('logger')
|
11
|
+
@mock_logger.stubs(:info)
|
12
|
+
@mock_logger.stubs(:error)
|
13
|
+
Docker.stubs(:validate_version!)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_docker_missing
|
17
|
+
Docker.expects(:validate_version!).raises(Excon::Error::Socket)
|
18
|
+
|
19
|
+
assert_raises DockerVersionError do
|
20
|
+
DockerBuilder.new(@mock_logger, 'test-base')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_build_success
|
25
|
+
assert_build 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_build_failure
|
29
|
+
assert_raises DockerRunError do
|
30
|
+
assert_build 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_release
|
35
|
+
Docker::Image.expects(:create).with('fromImage' => 'kyrofa/github-snap-builder:test-base').returns(@mock_image)
|
36
|
+
|
37
|
+
# Expect container based on image to be fired up
|
38
|
+
@mock_image.expects(:id).returns('1234')
|
39
|
+
Docker::Container.expects(:create).with(
|
40
|
+
'Cmd' => ['sh', '-c', "snapcraft login --with /token && snapcraft push test.snap --release=test-channel"],
|
41
|
+
'Image' => '1234',
|
42
|
+
'Env' => ['SNAPCRAFT_MANAGED_HOST=yes'],
|
43
|
+
'WorkingDir' => '/snapcraft',
|
44
|
+
'HostConfig' => {
|
45
|
+
'Binds' => ["/foo:/snapcraft"],
|
46
|
+
'AutoRemove' => true,
|
47
|
+
}
|
48
|
+
).returns(@mock_container)
|
49
|
+
@mock_container.expects(:store_file).with('/token', 'test-token')
|
50
|
+
@mock_container.expects(:start)
|
51
|
+
@mock_container.expects(:attach)
|
52
|
+
@mock_container.expects(:wait).returns({'StatusCode' => 0})
|
53
|
+
@mock_container.expects(:delete).with(force: true)
|
54
|
+
|
55
|
+
builder = DockerBuilder.new(@mock_logger, 'test-base')
|
56
|
+
builder.release("/foo/test.snap", "test-token", "test-channel")
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def assert_build(status_code)
|
62
|
+
Docker::Image.expects(:create).with('fromImage' => 'kyrofa/github-snap-builder:test-base').returns(@mock_image)
|
63
|
+
|
64
|
+
# Expect container based on image to be fired up
|
65
|
+
@mock_image.expects(:id).returns('1234')
|
66
|
+
Docker::Container.expects(:create).with(
|
67
|
+
'Cmd' => ['sh', '-c', "apt update -qq && snapcraft"],
|
68
|
+
'Image' => '1234',
|
69
|
+
'Env' => ['SNAPCRAFT_MANAGED_HOST=yes'],
|
70
|
+
'WorkingDir' => '/snapcraft',
|
71
|
+
'HostConfig' => {
|
72
|
+
'Binds' => ["test-project-dir:/snapcraft"],
|
73
|
+
'AutoRemove' => true,
|
74
|
+
}
|
75
|
+
).returns(@mock_container)
|
76
|
+
@mock_container.expects(:start)
|
77
|
+
@mock_container.expects(:attach)
|
78
|
+
@mock_container.expects(:wait).returns({'StatusCode' => status_code})
|
79
|
+
@mock_container.expects(:delete).with(force: true)
|
80
|
+
|
81
|
+
builder = DockerBuilder.new(@mock_logger, 'test-base')
|
82
|
+
builder.build('test-project-dir')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'github_snap_builder/config'
|
2
|
+
require_relative 'test_helpers'
|
3
|
+
|
4
|
+
module GithubSnapBuilder
|
5
|
+
class ConfigTest < SnapBuilderBaseTest
|
6
|
+
def setup
|
7
|
+
@config_data = {
|
8
|
+
"github_webhook_secret" => "test-secret",
|
9
|
+
"github_app_id" => 123,
|
10
|
+
"github_app_private_key" => "test-key",
|
11
|
+
"port" => 1234,
|
12
|
+
"bind" => "1.2.3.4",
|
13
|
+
"build_type" => "docker",
|
14
|
+
"repos" => {
|
15
|
+
"test/repo" => {
|
16
|
+
"channel" => "test-channel",
|
17
|
+
"token" => "test-token",
|
18
|
+
}
|
19
|
+
},
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_valid
|
24
|
+
assert config.valid?
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_github_webhook_secret
|
28
|
+
assert_equal "test-secret", config.github_webhook_secret
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_github_app_id
|
32
|
+
assert_equal 123, config.github_app_id
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_github_app_private_key
|
36
|
+
assert_equal "test-key", config.github_app_private_key
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_port
|
40
|
+
assert_equal 1234, config.port
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_bind
|
44
|
+
assert_equal "1.2.3.4", config.bind
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_build_type
|
48
|
+
assert_equal "docker", config.build_type
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_repos
|
52
|
+
repos = config.repos
|
53
|
+
assert_equal 1, repos.length
|
54
|
+
repo = repos[0]
|
55
|
+
|
56
|
+
assert_equal "test/repo", repo.name
|
57
|
+
assert_equal "test-channel", repo.channel
|
58
|
+
assert_equal "test-token", repo.token
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_invalid_github_webhook_secret
|
62
|
+
@config_data["github_webhook_secret"] = 1
|
63
|
+
assert !config.valid?
|
64
|
+
|
65
|
+
@config_data["github_webhook_secret"] = ''
|
66
|
+
assert !config.valid?
|
67
|
+
|
68
|
+
@config_data.delete "github_webhook_secret"
|
69
|
+
assert !config.valid?
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_invalid_github_app_id
|
73
|
+
@config_data["github_app_id"] = 'string'
|
74
|
+
assert !config.valid?
|
75
|
+
|
76
|
+
@config_data["github_app_id"] = 0
|
77
|
+
assert !config.valid?
|
78
|
+
|
79
|
+
@config_data.delete "github_app_id"
|
80
|
+
assert !config.valid?
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_invalid_github_app_private_key
|
84
|
+
@config_data["github_app_private_key"] = 1
|
85
|
+
assert !config.valid?
|
86
|
+
|
87
|
+
@config_data["github_app_private_key"] = ''
|
88
|
+
assert !config.valid?
|
89
|
+
|
90
|
+
@config_data.delete "github_app_private_key"
|
91
|
+
assert !config.valid?
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_invalid_port
|
95
|
+
@config_data["port"] = 'string'
|
96
|
+
assert !config.valid?
|
97
|
+
|
98
|
+
@config_data["port"] = 0
|
99
|
+
assert !config.valid?
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_invalid_bind
|
103
|
+
@config_data["bind"] = 1
|
104
|
+
assert !config.valid?
|
105
|
+
|
106
|
+
@config_data["bind"] = ''
|
107
|
+
assert !config.valid?
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_invalid_build_type
|
111
|
+
@config_data["build_type"] = 1
|
112
|
+
assert !config.valid?
|
113
|
+
|
114
|
+
@config_data["build_type"] = ''
|
115
|
+
assert !config.valid?
|
116
|
+
|
117
|
+
@config_data["build_type"] = 'invalid'
|
118
|
+
assert !config.valid?
|
119
|
+
|
120
|
+
@config_data.delete "build_type"
|
121
|
+
assert !config.valid?
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_invalid_channel
|
125
|
+
@config_data["repos"]["test/repo"]["channel"] = 1
|
126
|
+
assert !config.valid?
|
127
|
+
|
128
|
+
@config_data["repos"]["test/repo"]["channel"] = ''
|
129
|
+
assert !config.valid?
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_invalid_token
|
133
|
+
@config_data["repos"]["test/repo"]["token"] = 1
|
134
|
+
assert !config.valid?
|
135
|
+
|
136
|
+
@config_data["repos"]["test/repo"]["token"] = ''
|
137
|
+
assert !config.valid?
|
138
|
+
|
139
|
+
@config_data["repos"]["test/repo"].delete "token"
|
140
|
+
assert !config.valid?
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_port_is_optional
|
144
|
+
@config_data.delete "port"
|
145
|
+
assert config.valid?
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_bind_is_optional
|
149
|
+
@config_data.delete "bind"
|
150
|
+
assert config.valid?
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_channel_is_optional
|
154
|
+
@config_data["repos"]["test/repo"].delete "channel"
|
155
|
+
assert config.valid?
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def config
|
161
|
+
Config.new(@config_data.to_yaml)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|