Skip to content

Commit 074adbd

Browse files
authored
Merge branch 'master' into fdf-133550-forms-service-separation
2 parents 9ad72b6 + 755dbeb commit 074adbd

File tree

18 files changed

+862
-312
lines changed

18 files changed

+862
-312
lines changed

.github/workflows/code_checks.yml

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,114 @@ jobs:
5959
bundler-cache: true
6060

6161
- name: Run bundle-audit (checks gems for CVE issues)
62+
id: bundle_audit
63+
continue-on-error: true
6264
run: bundle exec bundle-audit check --update
6365

66+
- name: Configure AWS Credentials
67+
if: steps.bundle_audit.outcome == 'failure'
68+
uses: aws-actions/configure-aws-credentials@v6
69+
with:
70+
role-to-assume: ${{ vars.AWS_ASSUME_ROLE }}
71+
aws-region: us-gov-west-1
72+
73+
- uses: department-of-veterans-affairs/action-inject-ssm-secrets@d8e6de3bde4dd728c9d732baef58b3c854b8c4bb
74+
if: steps.bundle_audit.outcome == 'failure'
75+
with:
76+
ssm_parameter: /devops/VA_VSP_BOT_GITHUB_TOKEN
77+
env_variable_name: VA_VSP_BOT_GITHUB_TOKEN
78+
79+
- name: Checkout VSP actions
80+
if: steps.bundle_audit.outcome == 'failure'
81+
uses: actions/checkout@v6
82+
with:
83+
repository: department-of-veterans-affairs/vsp-github-actions
84+
ref: refs/heads/main
85+
token: ${{ env.VA_VSP_BOT_GITHUB_TOKEN }}
86+
persist-credentials: false
87+
path: ./.github/actions/vsp-github-actions
88+
89+
- uses: department-of-veterans-affairs/action-inject-ssm-secrets@d8e6de3bde4dd728c9d732baef58b3c854b8c4bb
90+
if: steps.bundle_audit.outcome == 'failure'
91+
with:
92+
ssm_parameter: /devops/github_actions_slack_socket_token
93+
env_variable_name: SLACK_APP_TOKEN
94+
95+
- uses: department-of-veterans-affairs/action-inject-ssm-secrets@d8e6de3bde4dd728c9d732baef58b3c854b8c4bb
96+
if: steps.bundle_audit.outcome == 'failure'
97+
with:
98+
ssm_parameter: /devops/github_actions_slack_bot_user_token
99+
env_variable_name: SLACK_BOT_TOKEN
100+
101+
- name: Slack notify - bundle-audit CVE failure
102+
if: steps.bundle_audit.outcome == 'failure'
103+
uses: ./.github/actions/vsp-github-actions/slack-socket
104+
with:
105+
slack_app_token: ${{ env.SLACK_APP_TOKEN }}
106+
slack_bot_token: ${{ env.SLACK_BOT_TOKEN }}
107+
message: "Critical: Vets-API bundle-audit detected CVE vulnerabilities"
108+
blocks: |
109+
[
110+
{ "type": "divider" },
111+
{
112+
"type": "header",
113+
"text": {
114+
"type": "plain_text",
115+
"text": ":rotating_light: CRITICAL: CVE Vulnerability Detected",
116+
"emoji": true
117+
}
118+
},
119+
{
120+
"type": "section",
121+
"text": {
122+
"type": "mrkdwn",
123+
"text": "*bundle-audit found gems with known CVE vulnerabilities in vets-api*"
124+
}
125+
},
126+
{
127+
"type": "section",
128+
"fields": [
129+
{
130+
"type": "mrkdwn",
131+
"text": ":x: *Status*: Build Failed"
132+
},
133+
{
134+
"type": "mrkdwn",
135+
"text": ":git: *Branch*: `${{ github.ref_name }}`"
136+
}
137+
]
138+
},
139+
{
140+
"type": "section",
141+
"text": {
142+
"type": "mrkdwn",
143+
"text": ":bust_in_silhouette: *Author:* ${{ github.actor }}"
144+
}
145+
},
146+
{
147+
"type": "actions",
148+
"elements": [
149+
{
150+
"type": "button",
151+
"text": {
152+
"type": "plain_text",
153+
"text": ":mag_right: View Run",
154+
"emoji": true
155+
},
156+
"url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
157+
"style": "danger"
158+
}
159+
]
160+
},
161+
{ "type": "divider" }
162+
]
163+
channel_id: "C0APA867M0V"
164+
165+
- name: Fail job if bundle-audit found CVEs
166+
if: steps.bundle_audit.outcome == 'failure'
167+
run: exit 1
168+
169+
64170
- name: Run Rubocop
65171
run: bundle exec rubocop --parallel --format github
66172

@@ -569,7 +675,7 @@ jobs:
569675
},
570676
{ "type": "divider" }
571677
]
572-
channel_id: "C039HRTHXDH"
678+
channel_id: "C0APA867M0V"
573679

574680
- name: Slack notify - warning low coverage alert
575681
if: needs.tests.result == 'success' && github.event_name != 'pull_request' && fromJSON(steps.coverage.outputs.percent) >= 90 && fromJSON(steps.coverage.outputs.percent) < 93
@@ -639,7 +745,7 @@ jobs:
639745
},
640746
{ "type": "divider" }
641747
]
642-
channel_id: "C039HRTHXDH"
748+
channel_id: "C0APA867M0V"
643749

644750
- name: Comment Coverage on PR
645751
if: needs.tests.result == 'success' && github.event_name == 'pull_request' && fromJSON(steps.coverage.outputs.percent) < 90

.github/workflows/deploy_delay_notifications.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ jobs:
151151
},
152152
{ "type": "divider" }
153153
]
154-
channel_id: "C039HRTHXDH"
154+
channel_id: "C0APA867M0V"
155155

156156
- name: Notify for other failure
157157
if: ${{ env.dev_summary == '' && env.staging_summary == '' }}
@@ -172,4 +172,4 @@ jobs:
172172
},
173173
{ "type": "divider" }
174174
]
175-
channel_id: "C039HRTHXDH"
175+
channel_id: "C0APA867M0V"

.github/workflows/deploy_missing_notifications.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,4 @@ jobs:
175175
slack_bot_token: ${{ env.SLACK_BOT_TOKEN }}
176176
message: "Vets API Deployment Delay:"
177177
blocks: "[{\"type\": \"divider\"}, {\"type\": \"section\", \"text\": { \"type\": \"mrkdwn\", \"text\": \":scared_and_sweating_smiley: GitHub Action Runner Workflow failed! :scared_and_sweating_smiley:\n <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.workflow }} Run #${{ github.run_number }}>\n\n*Status Summary:*\n${{ needs.check-api-status.outputs.status_summary }}\"}}, {\"type\": \"divider\"}]"
178-
channel_id: "C039HRTHXDH"
178+
channel_id: "C0APA867M0V"

Gemfile.lock

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ GEM
242242
minitest (>= 5.1, < 6)
243243
securerandom (>= 0.3)
244244
tzinfo (~> 2.0, >= 2.0.5)
245-
addressable (2.8.8)
245+
addressable (2.8.9)
246246
public_suffix (>= 2.0.2, < 8.0)
247247
adler32 (0.0.2)
248248
afm (1.0.0)
@@ -441,7 +441,7 @@ GEM
441441
dry-logic (~> 1.6)
442442
dry-types (~> 1.8)
443443
zeitwerk (~> 2.6)
444-
dry-struct (1.8.0)
444+
dry-struct (1.8.1)
445445
dry-core (~> 1.1)
446446
dry-types (~> 1.8, >= 1.8.2)
447447
ice_nine (~> 0.11)
@@ -473,7 +473,7 @@ GEM
473473
factory_bot_rails (6.5.1)
474474
factory_bot (~> 6.5)
475475
railties (>= 6.1.0)
476-
faker (3.6.0)
476+
faker (3.6.1)
477477
i18n (>= 1.8.11, < 2)
478478
faraday (2.14.1)
479479
faraday-net_http (>= 2.0, < 3.5)
@@ -497,12 +497,12 @@ GEM
497497
faraday_curl (0.0.2)
498498
faraday (>= 0.9.0)
499499
fastimage (2.4.1)
500-
ffi (1.17.3)
501-
ffi (1.17.3-aarch64-linux-gnu)
502-
ffi (1.17.3-java)
503-
ffi (1.17.3-x64-mingw-ucrt)
504-
ffi (1.17.3-x86-mingw32)
505-
ffi (1.17.3-x86_64-linux-gnu)
500+
ffi (1.17.4)
501+
ffi (1.17.4-aarch64-linux-gnu)
502+
ffi (1.17.4-java)
503+
ffi (1.17.4-x64-mingw-ucrt)
504+
ffi (1.17.4-x86-mingw32)
505+
ffi (1.17.4-x86_64-linux-gnu)
506506
ffi-compiler (1.3.2)
507507
ffi (>= 1.15.5)
508508
rake
@@ -583,26 +583,26 @@ GEM
583583
base64 (~> 0.2)
584584
faraday (>= 1.0, < 3.a)
585585
google-logging-utils (0.2.0)
586-
google-protobuf (4.33.5)
586+
google-protobuf (4.34.0)
587587
bigdecimal
588-
rake (>= 13)
589-
google-protobuf (4.33.5-aarch64-linux-gnu)
588+
rake (~> 13.3)
589+
google-protobuf (4.34.0-aarch64-linux-gnu)
590590
bigdecimal
591-
rake (>= 13)
592-
google-protobuf (4.33.5-java)
591+
rake (~> 13.3)
592+
google-protobuf (4.34.0-java)
593593
bigdecimal
594594
ffi (~> 1)
595595
ffi-compiler (~> 1)
596-
rake (>= 13)
597-
google-protobuf (4.33.5-x64-mingw-ucrt)
596+
rake (~> 13.3)
597+
google-protobuf (4.34.0-x64-mingw-ucrt)
598598
bigdecimal
599-
rake (>= 13)
600-
google-protobuf (4.33.5-x86-mingw32)
599+
rake (~> 13.3)
600+
google-protobuf (4.34.0-x86-mingw32)
601601
bigdecimal
602-
rake (>= 13)
603-
google-protobuf (4.33.5-x86_64-linux-gnu)
602+
rake (~> 13.3)
603+
google-protobuf (4.34.0-x86_64-linux-gnu)
604604
bigdecimal
605-
rake (>= 13)
605+
rake (~> 13.3)
606606
googleauth (1.16.2)
607607
faraday (>= 1.0, < 3.a)
608608
google-cloud-env (~> 2.2)
@@ -1248,7 +1248,7 @@ GEM
12481248
activemodel (>= 6.0.0)
12491249
bindex (>= 0.4.0)
12501250
railties (>= 6.0.0)
1251-
webmock (3.26.1)
1251+
webmock (3.26.2)
12521252
addressable (>= 2.8.0)
12531253
crack (>= 0.3.2)
12541254
hashdiff (>= 0.4.0, < 2.0.0)

app/sidekiq/simple_forms_api/form_remediation/upload_retry_job.rb

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ module FormRemediation
88
# +Aws::S3::Errors::ServiceError+ occurs during the initial upload attempt.
99
#
1010
# @example Enqueue a retry (typically called from Uploader, not directly)
11-
# UploadRetryJob.perform_async(file, directory, config)
12-
#
11+
# UploadRetryJob.perform_async(temp_file_path, directory, config_class_name)
12+
# we now pass the temp file path instead of the original file object because the retry job needs to be able
13+
# access the file even if the original file object
14+
# has been cleaned up by the caller after the initial upload attempt
1315
# @see SimpleFormsApi::FormRemediation::Uploader
1416
class UploadRetryJob
1517
include Sidekiq::Job
@@ -22,11 +24,20 @@ class FileNoLongerExistsError < StandardError; end
2224

2325
STATSD_KEY_PREFIX = 'api.simple_forms_api.upload_retry_job'
2426

25-
sidekiq_retries_exhausted do |_msg, ex|
27+
sidekiq_retries_exhausted do |msg, ex|
28+
temp_path = msg['args']&.first.to_s
29+
s3_dir = msg['args']&.[](1).to_s
2630
StatsD.increment("#{STATSD_KEY_PREFIX}.retries_exhausted")
31+
2732
Rails.logger.error(
28-
'SimpleFormsApi::FormRemediation::UploadRetryJob retries exhausted',
29-
{ exception: "#{ex.class} - #{ex.message}", backtrace: ex.backtrace&.join("\n").to_s }
33+
'SimpleFormsApi::FormRemediation::UploadRetryJob retries exhausted - ' \
34+
'manual remediation required. Temp file preserved for recovery.',
35+
{
36+
exception: "#{ex.class} - #{ex.message}",
37+
backtrace: ex.backtrace&.first(10)&.join("\n").to_s,
38+
temp_path:,
39+
s3_directory: s3_dir
40+
}
3041
)
3142
end
3243

@@ -40,9 +51,10 @@ class FileNoLongerExistsError < StandardError; end
4051
# (where arguments are JSON-serialized into strings/hashes). Each argument is
4152
# normalized to the expected type before use.
4253
#
43-
# @param file [String, Hash, CarrierWave::SanitizedFile] file path, serialized hash, or file object
54+
# @param file [String, Hash, CarrierWave::SanitizedFile] temp file path,
55+
# serialised hash, or file object
4456
# @param directory [String] the S3 target directory
45-
# @param config [String, Configuration::Base] config class name or config instance
57+
# @param config [String, Configuration::Base] config class name or instance
4658
def perform(file, directory, config)
4759
@file = file.respond_to?(:filename) ? file : CarrierWave::SanitizedFile.new(file)
4860
@directory = directory
@@ -52,31 +64,38 @@ def perform(file, directory, config)
5264

5365
uploader = @config.uploader_class.new(directory:, config: @config)
5466

55-
begin
56-
StatsD.increment("#{STATSD_KEY_PREFIX}.total")
67+
StatsD.increment("#{STATSD_KEY_PREFIX}.total")
5768

58-
uploader.store!(@file)
69+
begin
70+
uploader.store_for_retry!(@file)
5971
rescue Aws::S3::Errors::ServiceError
6072
raise if service_available?(@config.s3_settings.region)
6173

6274
retry_later
75+
return
6376
end
77+
78+
# Upload succeeded — remove the temp copy inside this job.
79+
cleanup_temp_file!
80+
81+
StatsD.increment("#{STATSD_KEY_PREFIX}.success")
6482
end
6583

6684
private
6785

6886
attr_accessor :file, :directory, :config
6987

70-
# Raises if the file no longer exists on disk. Callers typically delete
71-
# the source file after the initial upload attempt, so retries may arrive
72-
# with a stale path. Failing fast avoids burning Sidekiq retries.
88+
# Raises {FileNoLongerExistsError} if the temp file is missing.
89+
# This kills the Sidekiq job immediately rather than burning retries on
90+
# a file that will never reappear.
7391
def verify_file_exists!(sanitized_file)
7492
path = sanitized_file.respond_to?(:path) ? sanitized_file.path : sanitized_file.to_s
7593
return if path.blank? || File.exist?(path)
7694

7795
StatsD.increment("#{STATSD_KEY_PREFIX}.file_missing")
7896
raise FileNoLongerExistsError,
79-
"Retry file no longer exists at #{path}. Source was likely cleaned up after the initial upload failure."
97+
"Retry file no longer exists at #{path}. " \
98+
'The temp copy may have been removed by a previous successful attempt or manual cleanup.'
8099
end
81100

82101
# Checks whether the S3 service is reachable in the given region.
@@ -91,13 +110,38 @@ def service_available?(region)
91110
end
92111

93112
# Re-enqueues this job with a delay when S3 is completely unavailable.
94-
# This avoids burning a Sidekiq retry on a known-down service.
113+
# Uses the current +file.path+ (which already points to the temp copy)
114+
# so the same file is available when the delayed job runs.
95115
#
96-
# @param delay [Time] when to run the retry (default: 30 minutes from now)
116+
# @param delay [ActiveSupport::Duration] when to run the retry
97117
def retry_later(delay: 30.minutes.from_now)
98-
Rails.logger.info("S3 service unavailable. Retrying upload later for #{file.filename}.")
118+
Rails.logger.info(
119+
'SimpleFormsApi::FormRemediation::UploadRetryJob - S3 service unavailable, scheduling retry',
120+
{ temp_path: file.path, s3_directory: directory, retry_at: delay }
121+
)
99122
self.class.perform_in(delay, file.path, directory, config.class.name)
100123
end
124+
125+
# Deletes the temp copy that {Uploader#store!} created for this job.
126+
# Logs a warning instead of raising if the file is already gone — a
127+
# concurrent process may have cleaned it up, which is not an error.
128+
def cleanup_temp_file!
129+
path = file.respond_to?(:path) ? file.path : file.to_s
130+
return if path.blank?
131+
132+
if File.exist?(path)
133+
FileUtils.rm_f(path)
134+
Rails.logger.info(
135+
'SimpleFormsApi::FormRemediation::UploadRetryJob - temp file removed after successful upload',
136+
{ temp_path: path }
137+
)
138+
else
139+
Rails.logger.warn(
140+
'SimpleFormsApi::FormRemediation::UploadRetryJob - temp file already gone at cleanup; skipping',
141+
{ temp_path: path }
142+
)
143+
end
144+
end
101145
end
102146
end
103147
end

0 commit comments

Comments
 (0)