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.
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"
@@ -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
@@ -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: []