Skip to content

Commit 09dfd4c

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 09dfd4c

2 files changed

Lines changed: 169 additions & 1 deletion

File tree

app/models/shipit/deploy_spec.rb

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ def pretty_generate?
3535

3636
self.pretty_generate = false
3737

38+
SAFE_DEPLOY_COMMAND_PREFIXES = %w[
39+
production-platform-next
40+
kubernetes-deploy
41+
kubernetes-restart
42+
].freeze
43+
3844
def initialize(config)
3945
@config = config
4046
end
@@ -76,7 +82,18 @@ def directory
7682

7783
def dependencies_steps
7884
around_steps('dependencies') do
79-
config('dependencies', 'override') { discover_dependencies_steps || [] }
85+
config('dependencies', 'override') do
86+
if skip_dependencies_for_production_platform?
87+
Rails.logger.warn(
88+
"Skipping dependency installation: stack uses production_platform " \
89+
"and has no deploy steps requiring local dependencies. " \
90+
"To override, set `dependencies.override` in your shipit.yml."
91+
)
92+
[]
93+
else
94+
discover_dependencies_steps || []
95+
end
96+
end
8097
end
8198
end
8299
alias dependencies_steps! dependencies_steps
@@ -264,6 +281,43 @@ def links
264281

265282
private
266283

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

test/models/deploy_spec_test.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,120 @@ class DeploySpecTest < ActiveSupport::TestCase
5252
assert_equal ['before', 'bundle install', 'after'], @spec.dependencies_steps
5353
end
5454

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

0 commit comments

Comments
 (0)