appship 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/LICENSE +21 -0
- data/README.md +94 -0
- data/exe/appship +6 -0
- data/lib/appship/badger.rb +175 -0
- data/lib/appship/builder.rb +295 -0
- data/lib/appship/cache.rb +207 -0
- data/lib/appship/cli.rb +800 -0
- data/lib/appship/config.rb +75 -0
- data/lib/appship/errors.rb +8 -0
- data/lib/appship/ipa.rb +51 -0
- data/lib/appship/runner.rb +55 -0
- data/lib/appship/uploader.rb +490 -0
- data/lib/appship/version.rb +5 -0
- data/lib/appship.rb +25 -0
- data/test/test_cache.rb +146 -0
- data/test/test_cli.rb +22 -0
- data/test/test_config.rb +22 -0
- data/test/test_fir_uploader.rb +93 -0
- data/test/test_multipart_body.rb +33 -0
- metadata +61 -0
data/lib/appship.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "optparse"
|
|
8
|
+
require "pathname"
|
|
9
|
+
require "shellwords"
|
|
10
|
+
require "securerandom"
|
|
11
|
+
require "tempfile"
|
|
12
|
+
require "tmpdir"
|
|
13
|
+
require "uri"
|
|
14
|
+
require "yaml"
|
|
15
|
+
|
|
16
|
+
require_relative "appship/version"
|
|
17
|
+
require_relative "appship/errors"
|
|
18
|
+
require_relative "appship/config"
|
|
19
|
+
require_relative "appship/cache"
|
|
20
|
+
require_relative "appship/runner"
|
|
21
|
+
require_relative "appship/badger"
|
|
22
|
+
require_relative "appship/ipa"
|
|
23
|
+
require_relative "appship/uploader"
|
|
24
|
+
require_relative "appship/builder"
|
|
25
|
+
require_relative "appship/cli"
|
data/test/test_cache.rb
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require_relative "../lib/appship"
|
|
6
|
+
|
|
7
|
+
class CacheTest < Minitest::Test
|
|
8
|
+
class InteractiveInput
|
|
9
|
+
def initialize(values)
|
|
10
|
+
@values = values
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tty?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def gets
|
|
18
|
+
"#{@values.shift}\n"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_resolves_json_dictionary_array
|
|
23
|
+
Dir.mktmpdir do |directory|
|
|
24
|
+
path = File.join(directory, ".env")
|
|
25
|
+
File.write(path, <<~JSON)
|
|
26
|
+
[
|
|
27
|
+
{"project_name":"Demo","app_name":"DemoApp","pgyer_api_key":"secret"}
|
|
28
|
+
]
|
|
29
|
+
JSON
|
|
30
|
+
|
|
31
|
+
profile = Appship::Cache.new(path).resolve(project_name: "Demo", project_dir: directory)
|
|
32
|
+
assert_equal "DemoApp", profile["app_name"]
|
|
33
|
+
assert_equal "secret", profile["pgyer_api_key"]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_resolves_yaml_dictionary_array
|
|
38
|
+
Dir.mktmpdir do |directory|
|
|
39
|
+
path = File.join(directory, ".env")
|
|
40
|
+
File.write(path, <<~YAML)
|
|
41
|
+
- project_name: Demo
|
|
42
|
+
app_name: DemoApp
|
|
43
|
+
pgyer_api_key: secret
|
|
44
|
+
YAML
|
|
45
|
+
|
|
46
|
+
profile = Appship::Cache.new(path).resolve(project_name: "Demo", project_dir: directory)
|
|
47
|
+
assert_equal "Demo", profile["project_name"]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_creates_missing_cache_file
|
|
52
|
+
Dir.mktmpdir do |directory|
|
|
53
|
+
path = File.join(directory, "nested", ".config")
|
|
54
|
+
error = assert_raises(Appship::ConfigurationError) do
|
|
55
|
+
Appship::Cache.load(path, interactive: false)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
assert_includes error.message, "请填写 project_name、app_name、pgyer_api_key;pgyer_password 可为空"
|
|
59
|
+
assert File.file?(path)
|
|
60
|
+
assert_equal 0o600, File.stat(path).mode & 0o777
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_guides_first_time_setup
|
|
65
|
+
Dir.mktmpdir do |directory|
|
|
66
|
+
path = File.join(directory, ".config")
|
|
67
|
+
FileUtils.mkdir_p(File.join(directory, "Demo.xcodeproj"))
|
|
68
|
+
original_stdin = $stdin
|
|
69
|
+
$stdin = InteractiveInput.new(%w[DemoApp secret qiahao])
|
|
70
|
+
profile = Appship::Cache.load(path, project_dir: directory, interactive: true).resolve(project_name: "Demo")
|
|
71
|
+
assert_equal "Demo", profile["project_name"]
|
|
72
|
+
assert_equal "DemoApp", profile["app_name"]
|
|
73
|
+
assert_equal "qiahao", profile["pgyer_password"]
|
|
74
|
+
assert_includes File.read(path), "pgyer_api_key"
|
|
75
|
+
ensure
|
|
76
|
+
$stdin = original_stdin
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_guides_fir_api_key_when_cache_entry_is_missing_it
|
|
81
|
+
Dir.mktmpdir do |directory|
|
|
82
|
+
path = File.join(directory, ".config")
|
|
83
|
+
File.write(path, JSON.generate([{ "project_name" => "Demo", "app_name" => "DemoApp" }]))
|
|
84
|
+
original_stdin = $stdin
|
|
85
|
+
$stdin = InteractiveInput.new(["fir-secret"])
|
|
86
|
+
|
|
87
|
+
profile = Appship::Cache.load(path, provider: "fir").resolve(project_name: "Demo", provider: "fir")
|
|
88
|
+
|
|
89
|
+
assert_equal "fir-secret", profile["fir_api_key"]
|
|
90
|
+
assert_includes File.read(path), "fir_api_key"
|
|
91
|
+
ensure
|
|
92
|
+
$stdin = original_stdin
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_guides_fir_credentials_on_first_use
|
|
97
|
+
Dir.mktmpdir do |directory|
|
|
98
|
+
path = File.join(directory, ".config")
|
|
99
|
+
FileUtils.mkdir_p(File.join(directory, "Demo.xcodeproj"))
|
|
100
|
+
original_stdin = $stdin
|
|
101
|
+
$stdin = InteractiveInput.new(%w[DemoApp fir-secret install-secret])
|
|
102
|
+
|
|
103
|
+
profile = Appship::Cache.load(path, project_dir: directory, provider: "fir").resolve(project_name: "Demo", provider: "fir")
|
|
104
|
+
|
|
105
|
+
assert_equal "fir-secret", profile["fir_api_key"]
|
|
106
|
+
assert_equal "install-secret", profile["fir_password"]
|
|
107
|
+
ensure
|
|
108
|
+
$stdin = original_stdin
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_guides_pgyer_api_key_when_cache_entry_is_missing_it
|
|
113
|
+
Dir.mktmpdir do |directory|
|
|
114
|
+
path = File.join(directory, ".config")
|
|
115
|
+
File.write(path, JSON.generate([{ "project_name" => "Demo", "app_name" => "DemoApp" }]))
|
|
116
|
+
original_stdin = $stdin
|
|
117
|
+
$stdin = InteractiveInput.new(["pgyer-secret"])
|
|
118
|
+
|
|
119
|
+
profile = Appship::Cache.load(path).resolve(project_name: "Demo")
|
|
120
|
+
|
|
121
|
+
assert_equal "pgyer-secret", profile["pgyer_api_key"]
|
|
122
|
+
assert_includes File.read(path), "pgyer_api_key"
|
|
123
|
+
ensure
|
|
124
|
+
$stdin = original_stdin
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_credential_overrides_are_persisted
|
|
129
|
+
Dir.mktmpdir do |directory|
|
|
130
|
+
path = File.join(directory, ".config")
|
|
131
|
+
File.write(path, JSON.generate([{ "project_name" => "Demo", "app_name" => "DemoApp", "fir_api_key" => "old-key", "fir_password" => "old-password" }]))
|
|
132
|
+
|
|
133
|
+
cache = Appship::Cache.load(path, provider: "fir", interactive: false)
|
|
134
|
+
cache.resolve(
|
|
135
|
+
project_name: "Demo",
|
|
136
|
+
provider: "fir",
|
|
137
|
+
interactive: false,
|
|
138
|
+
credential_overrides: { "fir_api_key" => "new-key", "fir_password" => "new-password" }
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
saved = JSON.parse(File.read(path)).first
|
|
142
|
+
assert_equal "new-key", saved["fir_api_key"]
|
|
143
|
+
assert_equal "new-password", saved["fir_password"]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/test/test_cli.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "../lib/appship"
|
|
5
|
+
|
|
6
|
+
class CLITest < Minitest::Test
|
|
7
|
+
def test_fir_short_options_are_normalized
|
|
8
|
+
args = ["-key", "new-key", "-password", "new-password"]
|
|
9
|
+
|
|
10
|
+
Appship::CLI.new.send(:normalize_provider_short_options!, args, "fir")
|
|
11
|
+
|
|
12
|
+
assert_equal ["--fir-api-key", "new-key", "--fir-password", "new-password"], args
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_pgyer_short_options_are_normalized
|
|
16
|
+
args = ["-key=new-key", "-password=new-password"]
|
|
17
|
+
|
|
18
|
+
Appship::CLI.new.send(:normalize_provider_short_options!, args, "pgyer")
|
|
19
|
+
|
|
20
|
+
assert_equal ["--api-key=new-key", "--password=new-password"], args
|
|
21
|
+
end
|
|
22
|
+
end
|
data/test/test_config.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "../lib/appship"
|
|
5
|
+
|
|
6
|
+
class ConfigTest < Minitest::Test
|
|
7
|
+
def test_reads_nested_values
|
|
8
|
+
Dir.mktmpdir do |directory|
|
|
9
|
+
path = File.join(directory, ".appship.yml")
|
|
10
|
+
File.write(path, <<~YAML)
|
|
11
|
+
workspace: Demo.xcworkspace
|
|
12
|
+
upload:
|
|
13
|
+
api_key_env: DEMO_API_KEY
|
|
14
|
+
YAML
|
|
15
|
+
|
|
16
|
+
config = Appship::Config.new(path)
|
|
17
|
+
assert_equal "Demo.xcworkspace", config.get("workspace")
|
|
18
|
+
assert_equal "DEMO_API_KEY", config.get("upload", "api_key_env")
|
|
19
|
+
assert_equal "fallback", config.get("missing", default: "fallback")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require_relative "../lib/appship"
|
|
6
|
+
|
|
7
|
+
class FirUploaderTest < Minitest::Test
|
|
8
|
+
def test_requests_credentials_and_uploads_binary_with_fir_fields
|
|
9
|
+
uploader_class = Class.new(Appship::FirUploader) do
|
|
10
|
+
attr_reader :credential_request, :binary_upload, :password_update
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def post_json!(url, payload)
|
|
15
|
+
@credential_request = [url, payload]
|
|
16
|
+
{
|
|
17
|
+
"id" => "app-id",
|
|
18
|
+
"short" => "demo",
|
|
19
|
+
"cert" => {
|
|
20
|
+
"binary" => {
|
|
21
|
+
"key" => "binary-key",
|
|
22
|
+
"token" => "binary-token",
|
|
23
|
+
"upload_url" => "https://upload.example.test/binary"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def multipart_upload!(url, fields, file)
|
|
30
|
+
@binary_upload = [url, fields, file]
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update_access_password!(app_id, password)
|
|
35
|
+
@password_update = [app_id, password]
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Tempfile.create(["demo", ".ipa"]) do |file|
|
|
41
|
+
file.write("ipa-data")
|
|
42
|
+
file.flush
|
|
43
|
+
uploader = uploader_class.new(
|
|
44
|
+
fir_api_token: "fir-secret",
|
|
45
|
+
bundle_id: "com.example.demo",
|
|
46
|
+
app_name: "Demo",
|
|
47
|
+
app_version: "1.2.3",
|
|
48
|
+
build_number: "42",
|
|
49
|
+
release_type: "Inhouse",
|
|
50
|
+
fir_password: "install-secret",
|
|
51
|
+
description: "first build"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
result = uploader.upload!(file.path)
|
|
55
|
+
|
|
56
|
+
assert_equal "https://api.appmeta.cn/apps", uploader.credential_request.first
|
|
57
|
+
assert_equal({
|
|
58
|
+
"type" => "ios",
|
|
59
|
+
"bundle_id" => "com.example.demo",
|
|
60
|
+
"api_token" => "fir-secret"
|
|
61
|
+
}, uploader.credential_request.last)
|
|
62
|
+
assert_equal "https://upload.example.test/binary", uploader.binary_upload.first
|
|
63
|
+
assert_equal "binary-key", uploader.binary_upload[1]["key"]
|
|
64
|
+
assert_equal "binary-token", uploader.binary_upload[1]["token"]
|
|
65
|
+
assert_equal "Demo", uploader.binary_upload[1]["x:name"]
|
|
66
|
+
assert_equal "1.2.3", uploader.binary_upload[1]["x:version"]
|
|
67
|
+
assert_equal "42", uploader.binary_upload[1]["x:build"]
|
|
68
|
+
assert_equal "Inhouse", uploader.binary_upload[1]["x:release_type"]
|
|
69
|
+
assert_equal "first build", uploader.binary_upload[1]["x:changelog"]
|
|
70
|
+
assert_equal ["app-id", "install-secret"], uploader.password_update
|
|
71
|
+
assert_equal "https://fir.im/demo", result["url"]
|
|
72
|
+
assert_equal "fir", result["provider"]
|
|
73
|
+
assert result["data"]["password_protected"]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_uploader_selects_fir_provider
|
|
78
|
+
uploader = Appship::Uploader.new(provider: "fir", fir_api_token: "secret")
|
|
79
|
+
|
|
80
|
+
assert_instance_of Appship::FirUploader, uploader.instance_variable_get(:@delegate)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_requires_bundle_id_when_ipa_metadata_is_unavailable
|
|
84
|
+
Tempfile.create(["invalid", ".ipa"]) do |file|
|
|
85
|
+
file.write("not-an-ipa")
|
|
86
|
+
file.flush
|
|
87
|
+
uploader = Appship::FirUploader.new(fir_api_token: "secret")
|
|
88
|
+
|
|
89
|
+
error = assert_raises(Appship::ConfigurationError) { uploader.upload!(file.path) }
|
|
90
|
+
assert_includes error.message, "--bundle-id"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require_relative "../lib/appship"
|
|
6
|
+
|
|
7
|
+
class MultipartBodyTest < Minitest::Test
|
|
8
|
+
def test_streams_file_and_form_fields
|
|
9
|
+
Tempfile.create("pgyer-upload-test") do |file|
|
|
10
|
+
file.write("ipa-data")
|
|
11
|
+
file.flush
|
|
12
|
+
progress = []
|
|
13
|
+
body = Appship::MultipartBody.new(
|
|
14
|
+
{ "key" => "abc" },
|
|
15
|
+
file.path,
|
|
16
|
+
"boundary",
|
|
17
|
+
progress: ->(current, total) { progress << [current, total] }
|
|
18
|
+
)
|
|
19
|
+
chunks = +""
|
|
20
|
+
while (chunk = body.read(3))
|
|
21
|
+
chunks << chunk
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
assert_includes chunks, 'name="key"'
|
|
25
|
+
assert_includes chunks, "abc"
|
|
26
|
+
assert_includes chunks, 'filename="pgyer-upload-test'
|
|
27
|
+
assert_includes chunks, "ipa-data"
|
|
28
|
+
assert_includes chunks, "--boundary--"
|
|
29
|
+
assert_equal body.length, chunks.bytesize
|
|
30
|
+
assert_equal [8, 8], progress.last
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: appship
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- appship contributors
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Build any Xcode workspace or project, optionally add an app icon badge,
|
|
13
|
+
package an IPA and upload it to supported app distribution platforms.
|
|
14
|
+
email:
|
|
15
|
+
- ''
|
|
16
|
+
executables:
|
|
17
|
+
- appship
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- exe/appship
|
|
24
|
+
- lib/appship.rb
|
|
25
|
+
- lib/appship/badger.rb
|
|
26
|
+
- lib/appship/builder.rb
|
|
27
|
+
- lib/appship/cache.rb
|
|
28
|
+
- lib/appship/cli.rb
|
|
29
|
+
- lib/appship/config.rb
|
|
30
|
+
- lib/appship/errors.rb
|
|
31
|
+
- lib/appship/ipa.rb
|
|
32
|
+
- lib/appship/runner.rb
|
|
33
|
+
- lib/appship/uploader.rb
|
|
34
|
+
- lib/appship/version.rb
|
|
35
|
+
- test/test_cache.rb
|
|
36
|
+
- test/test_cli.rb
|
|
37
|
+
- test/test_config.rb
|
|
38
|
+
- test/test_fir_uploader.rb
|
|
39
|
+
- test/test_multipart_body.rb
|
|
40
|
+
homepage: https://github.com/your-org/appship
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata: {}
|
|
44
|
+
rdoc_options: []
|
|
45
|
+
require_paths:
|
|
46
|
+
- lib
|
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '3.1'
|
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0'
|
|
57
|
+
requirements: []
|
|
58
|
+
rubygems_version: 4.0.16
|
|
59
|
+
specification_version: 4
|
|
60
|
+
summary: A standalone iOS build, IPA packaging, badge and app distribution CLI
|
|
61
|
+
test_files: []
|