fastlane-plugin-appshot 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a15470692555262a4564076e0e13781454d5ac94f1e0a4127113f4ba163a2689
4
+ data.tar.gz: 96ee3e2203453bec0644b375c40223166232e35dc4241b7fa172a4b9d37090e3
5
+ SHA512:
6
+ metadata.gz: f10805d5c8d3fb8ff640853f268251492740731cf72c9363f8e91c274b341995189d4c9953ae730b82912858e4f2f075bc09d2de2cdc29cdd52c2bc8699b97a5
7
+ data.tar.gz: 7e4fffaadfc59f6d3c9ebb2bf446b4ae3e62b7c68af7145b30e6e1dd5cc89e4409de5b7c9a7e143337dbe81a5f9ae205f23cb3a5f9410c51f5a0e086b70d3508
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to fastlane-plugin-appshot are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [0.1.0] - 2026-10-08
6
+
7
+ ### Added
8
+
9
+ - The `appshot` action. For every locale it copies fastlane snapshot's capture of each screen into an [appshot](https://github.com/jems19s/appshot-studio) studio (a capture named `<simulator>-<name>.png` goes to the slot whose screenshot is `<name>.png`), renders the framed, captioned screenshots with appshot, and copies them into `<output_directory>/<locale>/`, the folder deliver uploads.
10
+ - Options `studio`, `app`, `raw_screenshots`, `device`, `locales`, `output_directory`, `appshot_path` and `chrome`, each also settable through an `APPSHOT_*` environment variable.
11
+ - Safeguards: the action refuses to read raw captures from the folder deliver uploads, warns about other PNGs deliver would upload alongside the framed ones, asks for `device:` when captures from several simulators match, and explains how to install appshot when it is missing.
12
+ - Requires Ruby 3.1 or later, like current fastlane.
13
+
14
+ [0.1.0]: https://github.com/jems19s/fastlane-plugin-appshot/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Maksim Zhelezniakov <48087000+jems19s@users.noreply.github.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # fastlane-plugin-appshot
2
+
3
+ [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-appshot)
4
+
5
+ A [_fastlane_](https://github.com/fastlane/fastlane) action that turns fastlane snapshot's raw captures into finished App Store screenshots with [appshot](https://github.com/jems19s/appshot-studio) — real device bezels, localized captions, generated backgrounds — and puts them where `deliver` uploads from.
6
+
7
+ ```ruby
8
+ lane :screenshots do
9
+ capture_screenshots(output_directory: "fastlane/raw_screenshots")
10
+ appshot(raw_screenshots: "fastlane/raw_screenshots")
11
+ upload_to_app_store(skip_binary_upload: true, skip_metadata: true)
12
+ end
13
+ ```
14
+
15
+ For every locale the action:
16
+
17
+ 1. copies snapshot's capture of each screen into the appshot studio, so every language gets its own app screens;
18
+ 2. runs `appshot render`;
19
+ 3. copies the framed PNGs into `fastlane/screenshots/<locale>/`, the folder `deliver` uploads.
20
+
21
+ ## Setup
22
+
23
+ ```bash
24
+ brew install jems19s/tap/appshot
25
+ fastlane add_plugin appshot
26
+ ```
27
+
28
+ Then, once, create the appshot studio next to your `fastlane` folder — the device, captions, colors and layout of your screenshots. Copy one locale's captures into a folder under plain names (`map.png`, `place.png`, …) and run:
29
+
30
+ ```bash
31
+ mkdir appshot && cd appshot
32
+ appshot init --name myapp --device "iPhone 17 Pro Max" --screenshots ../plain-captures \
33
+ --locale en-US --locale de-DE \
34
+ --caption 'The best cafés,\n*right around you*' --caption 'Know exactly\n*what to order*'
35
+ ```
36
+
37
+ Translate the captions in `apps/myapp/captions/<locale>.json`, style it in `apps/myapp/config.json`, and commit the studio (add `devices/` and `output/` to its `.gitignore`). See the [appshot README](https://github.com/jems19s/appshot-studio#readme) for everything you can change, or let a coding agent do it with appshot's [skill](https://github.com/jems19s/appshot-studio#with-a-coding-agent).
38
+
39
+ ### How captures are matched
40
+
41
+ fastlane snapshot saves `<simulator name>-<name>.png` in one folder per locale. A capture goes to the slot whose `screenshot` is `<name>.png`, so name your `snapshot("…")` calls after the studio's screenshots:
42
+
43
+ ```swift
44
+ snapshot("map") // fastlane/raw_screenshots/de-DE/iPhone 17 Pro Max-map.png → apps/myapp/assets/de-DE/map.png
45
+ ```
46
+
47
+ If snapshot runs on several simulators, say which one appshot frames with `device: "iPhone 17 Pro Max"`. A locale with no captures keeps the screenshots already in the studio.
48
+
49
+ ### Keep raw captures out of deliver's folder
50
+
51
+ snapshot writes to `fastlane/screenshots` by default — the folder `deliver` uploads. Point it somewhere else (`output_directory("./fastlane/raw_screenshots")` in the Snapfile, or the `capture_screenshots` option above), or `deliver` uploads raw and framed images side by side. The action refuses to read and write the same folder, and warns about any other PNGs it finds where it copies.
52
+
53
+ ## Options
54
+
55
+ | Key | Default | Description |
56
+ | --- | --- | --- |
57
+ | `studio` | `appshot` | appshot studio: the folder holding `apps/`, `templates/` and `devices/` |
58
+ | `app` | the only app | app under `apps/` |
59
+ | `raw_screenshots` | | snapshot's `output_directory`. Leave it out to render the screenshots already in the studio |
60
+ | `device` | | simulator whose captures to use, when `raw_screenshots` holds several |
61
+ | `locales` | all the app's locales | locales to import and render |
62
+ | `output_directory` | `fastlane/screenshots` | framed PNGs go to `<output_directory>/<locale>/` |
63
+ | `appshot_path` | `appshot` | the appshot command |
64
+ | `chrome` | found automatically | Chrome or Chromium to render with |
65
+
66
+ Every option can also come from an environment variable: `APPSHOT_STUDIO`, `APPSHOT_APP`, `APPSHOT_RAW_SCREENSHOTS`, `APPSHOT_DEVICE`, `APPSHOT_LOCALES`, `APPSHOT_OUTPUT_DIRECTORY`, `APPSHOT_PATH`, `APPSHOT_CHROME`.
67
+
68
+ The action returns a hash of locale → paths of the copied screenshots.
69
+
70
+ ## Development
71
+
72
+ ```bash
73
+ bundle install
74
+ bundle exec rake # tests and rubocop
75
+ ```
76
+
77
+ The tests use a stand-in for the `appshot` binary, so they run without appshot or Chrome.
78
+
79
+ ## License
80
+
81
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,143 @@
1
+ require "fastlane/action"
2
+ require_relative "../helper/appshot_helper"
3
+
4
+ module Fastlane
5
+ module Actions
6
+ class AppshotAction < Action
7
+ def self.run(params)
8
+ studio = File.expand_path(params[:studio])
9
+ appshot_path = params[:appshot_path]
10
+ UI.message("appshot #{Helper::AppshotHelper.appshot_version(appshot_path)}")
11
+
12
+ app = Helper::AppshotHelper.app_name(studio, params[:app])
13
+ config = Helper::AppshotHelper.load_config(studio, app)
14
+ locales = Helper::AppshotHelper.selected_locales(config, params[:locales])
15
+ slots = config.fetch("slots")
16
+ output_directory = File.expand_path(params[:output_directory])
17
+
18
+ if params[:raw_screenshots]
19
+ raw_screenshots = File.expand_path(params[:raw_screenshots])
20
+ if raw_screenshots == output_directory
21
+ UI.user_error!("raw_screenshots and output_directory are both #{output_directory}: deliver would upload " \
22
+ "the raw captures next to the framed ones. Have snapshot write somewhere else, e.g. " \
23
+ "capture_screenshots(output_directory: \"fastlane/raw_screenshots\").")
24
+ end
25
+ imported_paths = Helper::AppshotHelper.import_captures(
26
+ raw_screenshots: raw_screenshots,
27
+ assets_folder: File.join(studio, "apps", app, "assets"),
28
+ locales: locales,
29
+ slots: slots,
30
+ device: params[:device]
31
+ )
32
+ UI.success("Imported #{imported_paths.count} captures into apps/#{app}/assets")
33
+ end
34
+
35
+ render_command = [appshot_path, "render", "--root", studio, "--app", app]
36
+ render_command += locales.flat_map { |locale| ["--locale", locale] }
37
+ render_command += ["--chrome", params[:chrome]] if params[:chrome]
38
+ Actions.sh(*render_command)
39
+
40
+ copied_paths_by_locale = Helper::AppshotHelper.copy_renders(
41
+ rendered_folder: File.join(studio, "output", app),
42
+ output_directory: output_directory,
43
+ locales: locales,
44
+ slots: slots
45
+ )
46
+ UI.success("Copied #{copied_paths_by_locale.values.sum(&:count)} screenshots into #{output_directory}")
47
+ copied_paths_by_locale
48
+ end
49
+
50
+ def self.description
51
+ "Frame fastlane snapshot captures with appshot and hand them to deliver"
52
+ end
53
+
54
+ def self.authors
55
+ ["Maksim Zhelezniakov"]
56
+ end
57
+
58
+ def self.return_value
59
+ "A hash of locale => paths of the framed screenshots copied into output_directory"
60
+ end
61
+
62
+ def self.details
63
+ [
64
+ "Takes fastlane snapshot's raw captures into an appshot studio, renders the framed, captioned App Store",
65
+ "screenshots for every locale with appshot (https://github.com/jems19s/appshot-studio), and copies them",
66
+ "into the folder fastlane deliver uploads from. Each capture named \"<simulator>-<name>.png\" goes to the",
67
+ "slot whose screenshot is \"<name>.png\"."
68
+ ].join(" ")
69
+ end
70
+
71
+ def self.available_options
72
+ [
73
+ FastlaneCore::ConfigItem.new(key: :studio,
74
+ env_name: "APPSHOT_STUDIO",
75
+ description: "appshot studio: the folder holding apps/, templates/ and devices/",
76
+ default_value: "appshot",
77
+ type: String,
78
+ verify_block: proc do |value|
79
+ UI.user_error!("No appshot studio at #{value}") unless File.directory?(value)
80
+ end),
81
+ FastlaneCore::ConfigItem.new(key: :app,
82
+ env_name: "APPSHOT_APP",
83
+ description: "App under apps/ (default: the only one there)",
84
+ optional: true,
85
+ type: String),
86
+ FastlaneCore::ConfigItem.new(key: :raw_screenshots,
87
+ env_name: "APPSHOT_RAW_SCREENSHOTS",
88
+ description: "fastlane snapshot's output_directory, one folder per locale. " \
89
+ "Leave it out to render the screenshots already in the studio",
90
+ optional: true,
91
+ type: String),
92
+ FastlaneCore::ConfigItem.new(key: :device,
93
+ env_name: "APPSHOT_DEVICE",
94
+ description: "Simulator whose captures to use, e.g. \"iPhone 17 Pro Max\". " \
95
+ "Needed only when raw_screenshots holds several devices",
96
+ optional: true,
97
+ type: String),
98
+ FastlaneCore::ConfigItem.new(key: :locales,
99
+ env_name: "APPSHOT_LOCALES",
100
+ description: "Locales to import and render (default: all the app's locales)",
101
+ optional: true,
102
+ type: Array),
103
+ FastlaneCore::ConfigItem.new(key: :output_directory,
104
+ env_name: "APPSHOT_OUTPUT_DIRECTORY",
105
+ description: "Where deliver picks up screenshots; framed PNGs go to " \
106
+ "<output_directory>/<locale>/",
107
+ default_value: "fastlane/screenshots",
108
+ type: String),
109
+ FastlaneCore::ConfigItem.new(key: :appshot_path,
110
+ env_name: "APPSHOT_PATH",
111
+ description: "The appshot command",
112
+ default_value: "appshot",
113
+ type: String),
114
+ FastlaneCore::ConfigItem.new(key: :chrome,
115
+ env_name: "APPSHOT_CHROME",
116
+ description: "Chrome or Chromium to render with (default: found automatically)",
117
+ optional: true,
118
+ type: String)
119
+ ]
120
+ end
121
+
122
+ def self.example_code
123
+ [
124
+ <<~LANE
125
+ lane :screenshots do
126
+ capture_screenshots(output_directory: "fastlane/raw_screenshots")
127
+ appshot(raw_screenshots: "fastlane/raw_screenshots")
128
+ upload_to_app_store(skip_binary_upload: true, skip_metadata: true)
129
+ end
130
+ LANE
131
+ ]
132
+ end
133
+
134
+ def self.category
135
+ :screenshots
136
+ end
137
+
138
+ def self.is_supported?(platform)
139
+ %i[ios mac].include?(platform)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,119 @@
1
+ require "fastlane_core/ui/ui"
2
+ require "fileutils"
3
+ require "json"
4
+ require "open3"
5
+
6
+ module Fastlane
7
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
8
+
9
+ module Helper
10
+ class AppshotHelper
11
+ INSTALL_HINT = "Install it with `brew install jems19s/tap/appshot`, or point appshot_path at the binary.".freeze
12
+
13
+ def self.appshot_version(appshot_path)
14
+ version_output, status = Open3.capture2e(appshot_path, "--version")
15
+ unless status.success?
16
+ UI.user_error!("`#{appshot_path} --version` failed: #{version_output.strip}. #{INSTALL_HINT}")
17
+ end
18
+ version_output.strip
19
+ rescue Errno::ENOENT
20
+ UI.user_error!("appshot not found at '#{appshot_path}'. #{INSTALL_HINT}")
21
+ end
22
+
23
+ def self.app_name(studio, requested_app)
24
+ return requested_app if requested_app
25
+
26
+ apps_folder = File.join(studio, "apps")
27
+ app_names = Dir.exist?(apps_folder) ? Dir.children(apps_folder) : []
28
+ app_names = app_names.select { |entry| File.exist?(File.join(apps_folder, entry, "config.json")) }.sort
29
+ return app_names.first if app_names.count == 1
30
+
31
+ UI.user_error!("No appshot app in #{apps_folder} yet — create one with `appshot init`") if app_names.empty?
32
+ UI.user_error!("Several apps in #{apps_folder}: #{app_names.join(', ')} — pick one with app:")
33
+ end
34
+
35
+ def self.load_config(studio, app)
36
+ config_path = File.join(studio, "apps", app, "config.json")
37
+ unless File.exist?(config_path)
38
+ UI.user_error!("No appshot app at #{config_path} — create one with `appshot init`")
39
+ end
40
+ JSON.parse(File.read(config_path))
41
+ end
42
+
43
+ def self.selected_locales(config, requested_locales)
44
+ configured_locales = config.fetch("locales")
45
+ return configured_locales if requested_locales.nil? || requested_locales.empty?
46
+
47
+ unknown_locales = requested_locales - configured_locales
48
+ unless unknown_locales.empty?
49
+ UI.user_error!("#{unknown_locales.join(', ')} not in the app's locales (#{configured_locales.join(', ')})")
50
+ end
51
+ requested_locales
52
+ end
53
+
54
+ def self.import_captures(raw_screenshots:, assets_folder:, locales:, slots:, device:)
55
+ imported_paths = []
56
+ locales.each do |locale|
57
+ capture_folder = File.join(raw_screenshots, locale)
58
+ unless File.directory?(capture_folder)
59
+ UI.important("No #{capture_folder} — #{locale} keeps the screenshots already in the studio")
60
+ next
61
+ end
62
+ capture_files = Dir.children(capture_folder).select { |file| file.end_with?(".png") }
63
+ slots.each do |slot|
64
+ screenshot = slot.fetch("screenshot")
65
+ capture = capture_for(screenshot, capture_files, capture_folder, device)
66
+ next if capture.nil?
67
+
68
+ locale_assets_folder = File.join(assets_folder, locale)
69
+ FileUtils.mkdir_p(locale_assets_folder)
70
+ FileUtils.cp(File.join(capture_folder, capture), File.join(locale_assets_folder, screenshot))
71
+ imported_paths << File.join(locale_assets_folder, screenshot)
72
+ end
73
+ end
74
+ imported_paths
75
+ end
76
+
77
+ # fastlane snapshot saves "<simulator name>-<name>.png"; simulator names can contain hyphens themselves,
78
+ # so captures are matched by their "-<slot screenshot>" ending rather than split at the first hyphen.
79
+ def self.capture_for(screenshot, capture_files, capture_folder, device)
80
+ if device
81
+ capture = "#{device}-#{screenshot}"
82
+ return capture if capture_files.include?(capture)
83
+
84
+ UI.important("No #{capture} in #{capture_folder}")
85
+ return nil
86
+ end
87
+
88
+ matching_captures = capture_files.select { |file| file.end_with?("-#{screenshot}") }.sort
89
+ if matching_captures.count > 1
90
+ UI.user_error!("Several captures in #{capture_folder} end in -#{screenshot} " \
91
+ "(#{matching_captures.join(', ')}) — set device: to the simulator name")
92
+ end
93
+ UI.important("No capture ending in -#{screenshot} in #{capture_folder}") if matching_captures.empty?
94
+ matching_captures.first
95
+ end
96
+
97
+ def self.copy_renders(rendered_folder:, output_directory:, locales:, slots:)
98
+ rendered_files = slots.map { |slot| "#{slot.fetch('name')}.png" }
99
+ locales.to_h do |locale|
100
+ destination_folder = File.join(output_directory, locale)
101
+ FileUtils.mkdir_p(destination_folder)
102
+ other_files = Dir.children(destination_folder).select { |file|
103
+ file.downcase.end_with?(".png")
104
+ } - rendered_files
105
+ unless other_files.empty?
106
+ UI.important("deliver uploads every PNG in #{destination_folder}, " \
107
+ "so these go up too: #{other_files.sort.join(', ')}")
108
+ end
109
+ copied_paths = rendered_files.map do |file|
110
+ destination = File.join(destination_folder, file)
111
+ FileUtils.cp(File.join(rendered_folder, locale, file), destination)
112
+ destination
113
+ end
114
+ [locale, copied_paths]
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module Appshot
3
+ VERSION = "0.1.0".freeze
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ require "fastlane/plugin/appshot/version"
2
+
3
+ module Fastlane
4
+ module Appshot
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ def self.all_classes
7
+ Dir[File.expand_path("**/{actions,helper}/*.rb", File.dirname(__FILE__))]
8
+ end
9
+ end
10
+ end
11
+
12
+ # By default we want to import all available actions and helpers
13
+ # A plugin can contain any number of actions and plugins
14
+ Fastlane::Appshot.all_classes.each do |current|
15
+ require current
16
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-appshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Maksim Zhelezniakov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ email: 48087000+jems19s@users.noreply.github.com
13
+ executables: []
14
+ extensions: []
15
+ extra_rdoc_files: []
16
+ files:
17
+ - CHANGELOG.md
18
+ - LICENSE
19
+ - README.md
20
+ - lib/fastlane/plugin/appshot.rb
21
+ - lib/fastlane/plugin/appshot/actions/appshot_action.rb
22
+ - lib/fastlane/plugin/appshot/helper/appshot_helper.rb
23
+ - lib/fastlane/plugin/appshot/version.rb
24
+ homepage: https://github.com/jems19s/fastlane-plugin-appshot
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ rubygems_mfa_required: 'true'
29
+ source_code_uri: https://github.com/jems19s/fastlane-plugin-appshot
30
+ bug_tracker_uri: https://github.com/jems19s/fastlane-plugin-appshot/issues
31
+ changelog_uri: https://github.com/jems19s/fastlane-plugin-appshot/blob/main/CHANGELOG.md
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 4.0.11
47
+ specification_version: 4
48
+ summary: Frame fastlane snapshot captures with appshot and hand them to deliver
49
+ test_files: []