Skip to content

Commit fcdac79

Browse files
DudeRandom21claude
andcommitted
Auto-skip dependency installation for production_platform repos
When a repo has production_platform configured and all explicitly configured deploy/rollback/task steps match a known-safe command allowlist (production-platform-next, kubernetes-deploy, kubernetes-restart), skip dependency installation automatically. This unblocks Ruby version upgrades for repos that deploy via production-platform-next, where bundle install fails due to gem incompatibilities with the new Ruby version on the shipit worker, even though those deps are never actually needed for the deploy. Refs Shopify/continuous-deployment#2454 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent becb899 commit fcdac79

4 files changed

Lines changed: 208 additions & 1 deletion

File tree

app/models/shipit/deploy_spec.rb

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,18 @@ def directory
7676

7777
def dependencies_steps
7878
around_steps('dependencies') do
79-
config('dependencies', 'override') { discover_dependencies_steps || [] }
79+
config('dependencies', 'override') do
80+
if skip_dependencies_for_production_platform?
81+
Rails.logger.warn(
82+
"Skipping dependency installation: stack uses production_platform " \
83+
"and has no deploy steps requiring local dependencies. " \
84+
"To override, set `dependencies.override` in your shipit.yml."
85+
)
86+
[]
87+
else
88+
discover_dependencies_steps || []
89+
end
90+
end
8091
end
8192
end
8293
alias dependencies_steps! dependencies_steps
@@ -264,6 +275,44 @@ def links
264275

265276
private
266277

278+
def production_platform?
279+
config('production_platform').present?
280+
end
281+
282+
def skip_dependencies_for_production_platform?
283+
return false unless production_platform?
284+
return false if Shipit.safe_deploy_command_prefixes.empty?
285+
286+
# Only check explicitly configured steps. If deploy/rollback rely on auto-discovery
287+
# (no override), we conservatively assume dependencies may be needed.
288+
# Similarly, discovered task definitions (e.g., kubernetes-restart) are inherently
289+
# safe commands and don't need to be checked here.
290+
all_steps = Array(config('deploy', 'override')) +
291+
Array(config('deploy', 'pre')) +
292+
Array(config('deploy', 'post')) +
293+
Array(config('rollback', 'override')) +
294+
Array(config('rollback', 'pre')) +
295+
Array(config('rollback', 'post')) +
296+
all_task_steps
297+
298+
all_steps = all_steps.compact
299+
return false if all_steps.empty?
300+
301+
all_steps.all? { |step| safe_deploy_command?(step) }
302+
end
303+
304+
def all_task_steps
305+
task_configs = config('tasks') || {}
306+
task_configs.values.flat_map { |td| Array(td['steps']) }
307+
end
308+
309+
def safe_deploy_command?(step)
310+
step = step.to_s.strip
311+
return true if step.empty?
312+
313+
Shipit.safe_deploy_command_prefixes.any? { |prefix| step == prefix || step.start_with?("#{prefix} ") }
314+
end
315+
267316
def around_steps(section)
268317
steps = yield
269318
return unless steps

lib/shipit.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ module Shipit
6969
attr_writer(
7070
:internal_hook_receivers,
7171
:preferred_org_emails,
72+
:safe_deploy_command_prefixes,
7273
:task_execution_strategy,
7374
:task_logger,
7475
:use_git_askpass
@@ -293,6 +294,10 @@ def committer_email
293294
secrets.committer_email.presence || "#{app_name.underscore.dasherize}@#{host}"
294295
end
295296

297+
def safe_deploy_command_prefixes
298+
@safe_deploy_command_prefixes ||= []
299+
end
300+
296301
def internal_hook_receivers
297302
@internal_hook_receivers ||= []
298303
end

test/models/deploy_spec_test.rb

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ class DeploySpecTest < ActiveSupport::TestCase
99
@stack = shipit_stacks(:shipit)
1010
@spec = DeploySpec::FileSystem.new(@app_dir, @stack)
1111
@spec.stubs(:load_config).returns({})
12+
@original_safe_deploy_command_prefixes = Shipit.safe_deploy_command_prefixes
13+
Shipit.safe_deploy_command_prefixes = %w[
14+
production-platform-next
15+
kubernetes-deploy
16+
kubernetes-restart
17+
]
18+
end
19+
20+
teardown do
21+
Shipit.safe_deploy_command_prefixes = @original_safe_deploy_command_prefixes
1222
end
1323

1424
test '#supports_fetch_deployed_revision? returns false by default' do
@@ -52,6 +62,140 @@ class DeploySpecTest < ActiveSupport::TestCase
5262
assert_equal ['before', 'bundle install', 'after'], @spec.dependencies_steps
5363
end
5464

65+
test '#dependencies_steps returns empty when production_platform is configured and all steps are safe' do
66+
@spec.stubs(:load_config).returns(
67+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
68+
'deploy' => { 'override' => ['production-platform-next deploy my-app production-unrestricted'] }
69+
)
70+
assert_equal [], @spec.dependencies_steps
71+
end
72+
73+
test '#dependencies_steps still discovers deps when production_platform has unsafe deploy steps' do
74+
@spec.stubs(:load_config).returns(
75+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
76+
'deploy' => { 'override' => ['bundle exec rake deploy'] }
77+
)
78+
@spec.expects(:bundler?).returns(true).at_least_once
79+
@spec.expects(:bundle_install).returns(['bundle install'])
80+
assert_equal ['bundle install'], @spec.dependencies_steps
81+
end
82+
83+
test '#dependencies_steps still discovers deps when production_platform is absent' do
84+
@spec.stubs(:load_config).returns({})
85+
@spec.expects(:bundler?).returns(true).at_least_once
86+
@spec.expects(:bundle_install).returns(['bundle install'])
87+
assert_equal ['bundle install'], @spec.dependencies_steps
88+
end
89+
90+
test '#dependencies_steps respects explicit override even with production_platform' do
91+
@spec.stubs(:load_config).returns(
92+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
93+
'dependencies' => { 'override' => ['custom-install'] }
94+
)
95+
assert_equal ['custom-install'], @spec.dependencies_steps
96+
end
97+
98+
test '#dependencies_steps preserves pre/post steps when skipping for production_platform' do
99+
@spec.stubs(:load_config).returns(
100+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
101+
'deploy' => { 'override' => ['production-platform-next deploy my-app production-unrestricted'] },
102+
'dependencies' => { 'pre' => ['echo before'], 'post' => ['echo after'] }
103+
)
104+
assert_equal ['echo before', 'echo after'], @spec.dependencies_steps
105+
end
106+
107+
test '#dependencies_steps skips deps when production_platform tasks only use safe commands' do
108+
@spec.stubs(:load_config).returns(
109+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
110+
'tasks' => {
111+
'restart' => { 'steps' => ['production-platform-next run-once my-app production-unrestricted restart'] }
112+
}
113+
)
114+
assert_equal [], @spec.dependencies_steps
115+
end
116+
117+
test '#dependencies_steps does not skip when production_platform task has unsafe steps' do
118+
@spec.stubs(:load_config).returns(
119+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
120+
'tasks' => {
121+
'migrate' => { 'steps' => ['bundle exec rake db:migrate'] }
122+
}
123+
)
124+
@spec.expects(:bundler?).returns(true).at_least_once
125+
@spec.expects(:bundle_install).returns(['bundle install'])
126+
assert_equal ['bundle install'], @spec.dependencies_steps
127+
end
128+
129+
test '#dependencies_steps skips deps when production_platform uses kubernetes-deploy' do
130+
@spec.stubs(:load_config).returns(
131+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
132+
'deploy' => { 'override' => ['kubernetes-deploy --max-watch-seconds 900 my-namespace my-context'] }
133+
)
134+
assert_equal [], @spec.dependencies_steps
135+
end
136+
137+
test '#dependencies_steps falls through to discovery when deploy step has unknown command' do
138+
@spec.stubs(:load_config).returns(
139+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
140+
'deploy' => { 'override' => ['some-unknown-deploy-tool --flag'] }
141+
)
142+
@spec.expects(:discover_dependencies_steps).returns(nil).once
143+
assert_equal [], @spec.dependencies_steps
144+
end
145+
146+
test '#dependencies_steps does not skip when deploy is safe but rollback is unsafe' do
147+
@spec.stubs(:load_config).returns(
148+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
149+
'deploy' => { 'override' => ['production-platform-next deploy my-app production-unrestricted'] },
150+
'rollback' => { 'override' => ['bundle exec rake rollback'] }
151+
)
152+
@spec.expects(:bundler?).returns(true).at_least_once
153+
@spec.expects(:bundle_install).returns(['bundle install'])
154+
assert_equal ['bundle install'], @spec.dependencies_steps
155+
end
156+
157+
test '#dependencies_steps does not skip when deploy.pre has unsafe steps' do
158+
@spec.stubs(:load_config).returns(
159+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
160+
'deploy' => {
161+
'pre' => ['bundle exec rake before_deploy'],
162+
'override' => ['production-platform-next deploy my-app production-unrestricted']
163+
}
164+
)
165+
@spec.expects(:bundler?).returns(true).at_least_once
166+
@spec.expects(:bundle_install).returns(['bundle install'])
167+
assert_equal ['bundle install'], @spec.dependencies_steps
168+
end
169+
170+
test '#dependencies_steps does not skip when production_platform has no deploy override configured' do
171+
@spec.stubs(:load_config).returns(
172+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] }
173+
)
174+
@spec.expects(:bundler?).returns(true).at_least_once
175+
@spec.expects(:bundle_install).returns(['bundle install'])
176+
assert_equal ['bundle install'], @spec.dependencies_steps
177+
end
178+
179+
test '#dependencies_steps does not skip when safe_deploy_command_prefixes is empty' do
180+
Shipit.safe_deploy_command_prefixes = []
181+
@spec.stubs(:load_config).returns(
182+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
183+
'deploy' => { 'override' => ['production-platform-next deploy my-app production-unrestricted'] }
184+
)
185+
@spec.expects(:bundler?).returns(true).at_least_once
186+
@spec.expects(:bundle_install).returns(['bundle install'])
187+
assert_equal ['bundle install'], @spec.dependencies_steps
188+
end
189+
190+
test '#dependencies_steps skips deps when custom safe_deploy_command_prefixes match' do
191+
Shipit.safe_deploy_command_prefixes = %w[my-custom-deployer]
192+
@spec.stubs(:load_config).returns(
193+
'production_platform' => { 'application' => 'my-app', 'runtime_ids' => ['production-unrestricted'] },
194+
'deploy' => { 'override' => ['my-custom-deployer deploy my-app'] }
195+
)
196+
assert_equal [], @spec.dependencies_steps
197+
end
198+
55199
test '#fetch_deployed_revision_steps! is unknown by default' do
56200
assert_raises DeploySpec::Error do
57201
@spec.fetch_deployed_revision_steps!

test/unit/shipit_test.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ class ShipitTest < ActiveSupport::TestCase
3030
assert_equal(['shopify/developers'], Shipit.github_teams.map(&:handle))
3131
end
3232

33+
test ".safe_deploy_command_prefixes defaults to empty array" do
34+
assert_equal [], Shipit.safe_deploy_command_prefixes
35+
end
36+
37+
test ".safe_deploy_command_prefixes can be set" do
38+
Shipit.safe_deploy_command_prefixes = %w[foo bar]
39+
assert_equal %w[foo bar], Shipit.safe_deploy_command_prefixes
40+
end
41+
3342
test ".presence_check_timeout defaults to 30" do
3443
assert_equal 30, Shipit.presence_check_timeout
3544
end

0 commit comments

Comments
 (0)