Skip to content

Add graceful shutdown support with signal context handling#35

Merged
yiftach-armis merged 5 commits intomainfrom
fix/graceful-shutdown
Jan 13, 2026
Merged

Add graceful shutdown support with signal context handling#35
yiftach-armis merged 5 commits intomainfrom
fix/graceful-shutdown

Conversation

@yiftach-armis
Copy link
Collaborator

Description

Adds signal-based graceful shutdown support to allow users to cleanly interrupt long-running scans with Ctrl+C. When SIGINT or SIGTERM is received, the context is cancelled, allowing all in-flight operations to terminate and cleanup tasks to run.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Changes

  • Created NewSignalContext() utility to initialize contexts with SIGINT/SIGTERM handling
  • Updated scan_repo and scan_image commands to use signal-aware contexts
  • Refactored image scanner temp file cleanup to use defer pattern for guaranteed cleanup
  • Added unit tests for signal context utility

Testing

  • Unit tests pass locally
  • No linting errors
  • All packages build successfully

Verification

  • Temp files are now cleaned up even when context is cancelled
  • User sees "Scan cancelled" message when interrupting with Ctrl+C
  • Context cancellation propagates through scanner and HTTP client layers

@github-actions
Copy link

github-actions bot commented Jan 12, 2026

Test Coverage Report

total: (statements) 77.1%

Coverage by function
github.com/ArmisSecurity/armis-cli/cmd/armis-cli/main.go:16:			main					0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:36:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:44:			NewClient				91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:88:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:93:			StartIngest				78.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:160:			GetIngestStatus				84.2%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:193:			WaitForIngest				0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:235:			FetchNormalizedResults			78.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:282:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:307:			GetScanResult				66.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:335:			WaitForScan				0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:356:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:14:			NewSignalContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:21:			handleScanError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:41:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:49:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:53:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:65:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:72:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:82:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:89:			getToken				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:96:			getTenantID				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:103:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:110:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:120:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:134:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:21:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_image.go:97:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_repo.go:75:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:30:		NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:56:		Do					87.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:31:			write					66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:62:			Write					90.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:93:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:98:			FormatWithOptions			96.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:136:		getSeverityIcon				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:153:		getSeverityColor			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:182:		init					50.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:189:		disableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:201:		sortFindingsBySeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:230:		loadSnippetFromFile			75.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:312:		formatCodeSnippet			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:349:		highlightColumns			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:385:		detectLanguage				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:683:		scanDuration				26.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:715:		renderSummaryDashboard			61.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:804:		renderFindings				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:819:		renderFinding				62.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:876:		renderGroupedFindings			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:897:		groupFindings				96.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:952:		severityRank				75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:966:		isGitRepo				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:973:		getGitBlame				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1009:		parseGitBlame				85.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1045:		maskEmail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1068:		getTopLevelDomain			75.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:14:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:21:			FormatWithOptions			66.7%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:29:			formatWithDebug				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:43:			Format					83.3%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:67:			convertToJUnitCases			91.7%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:99:			countFailures				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:110:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:25:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:41:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:57:		ExitIfNeeded				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:64:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:87:			convertToSarifResults			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:124:		severityToSarifLevel			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:138:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:25:		IsCI					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:47:		NewReader				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:62:		NewWriter				50.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:96:		NewSpinner				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:104:		NewSpinnerWithTimeout			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:120:		NewSpinnerWithContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:128:		SetWriter				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:137:		Start					93.8%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:225:		Stop					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:260:		UpdateMessage				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:267:		Update					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:274:		GetElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:281:		formatDuration				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:40:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:54:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:60:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:92:		ScanTarball				93.1%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:140:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:173:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:187:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:196:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:203:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:230:		convertNormalizedFindings		93.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:324:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:343:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:362:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:375:		mapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:390:		formatElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/validate.go:11:		validateImageName			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:18:		LoadIgnorePatterns			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:52:		loadIgnoreFile				89.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:86:		Match					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:98:		shouldSkipDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:38:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:52:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:58:		Scan					84.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:145:		tarGzDirectory				71.4%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:224:		calculateDirSize			81.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:263:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:294:		isTestFile				88.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:337:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:364:		convertNormalizedFindings		90.2%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:462:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:481:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:500:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:513:		mapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:528:		formatElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:9:	CreateNormalizedFinding			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:14:	CreateNormalizedFindingWithLabels	0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:19:	CreateNormalizedFindingFull		0.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:43:			MaskSecretInLine			81.2%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:79:			maskValue				60.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:94:			MaskSecretInLines			100.0%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:12:			SanitizePath				90.9%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:41:			SafeJoinPath				84.2%
github.com/ArmisSecurity/armis-cli/test/sample-repo/src/main.go:6:		main					0.0%
total:										(statements)				77.1%

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds graceful shutdown support to allow users to interrupt long-running scans with Ctrl+C (SIGINT) or SIGTERM signals. The implementation ensures proper cleanup of resources and provides clear user feedback when scans are cancelled.

Changes:

  • Introduced signal-aware context handling utility (NewSignalContext())
  • Updated scan commands to use signal contexts with proper cancellation handling
  • Refactored image scanner cleanup to use defer pattern for guaranteed resource cleanup

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/cmd/context.go New utility function to create contexts that cancel on SIGINT/SIGTERM
internal/cmd/context_test.go Unit tests for signal context creation and cancellation behavior
internal/cmd/scan_repo.go Updated to use signal context with cancellation message handling
internal/cmd/scan_image.go Updated to use signal context with cancellation message handling for both tarball and image scan paths
internal/scan/image/image.go Refactored temp file cleanup using defer to guarantee cleanup on context cancellation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Initialize contexts using signal.NotifyContext to gracefully handle SIGINT and SIGTERM. Pass cancellable context through scanner and HTTP client. Refactor image scanner temp file cleanup to use defer pattern to ensure cleanup runs even when context is cancelled.
Add handleScanError helper that displays "Scan cancelled" when context
is cancelled, providing clear feedback when users interrupt scans.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Use errors.Is to verify the error itself contains context.Canceled
rather than just checking the context state. This prevents incorrectly
printing "Scan cancelled" when an unrelated error occurs while the
context happens to be cancelled.

Also adds comprehensive test coverage for handleScanError.
Check error return values from w.Close() and io.Copy() in the
captureStderr helper function.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Handle os.Pipe() error in test helper instead of ignoring
- Document unused ctx parameter in handleScanError for API consistency
@yiftach-armis yiftach-armis merged commit e4be2c9 into main Jan 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants