diff --git a/release-scripts/upload-artifacts.sh b/release-scripts/upload-artifacts.sh index 163817311f..6dc842b6b6 100755 --- a/release-scripts/upload-artifacts.sh +++ b/release-scripts/upload-artifacts.sh @@ -170,6 +170,44 @@ trigger_repository_event() { fi } +# Request a channel-specific Endpoint Explorer refresh without making the CLI +# release depend on the dispatch or the resulting workflow. +trigger_endpoint_explorer_refresh() { + channel=$1 + + if [ -z "${HAMMERHEAD_GITHUB_PAT:-}" ]; then + echo "WARNING: HAMMERHEAD_GITHUB_PAT is unavailable; Endpoint Explorer $channel refresh was not requested." + return 0 + fi + + echo "Triggering Endpoint Explorer $channel refresh..." + curl_status=0 + response=$(curl \ + --location \ + --request POST \ + --connect-timeout 2 \ + --max-time 5 \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: Bearer $HAMMERHEAD_GITHUB_PAT" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + --data "{\"ref\":\"main\",\"inputs\":{\"channel\":\"$channel\"}}" \ + --write-out "%{http_code}" \ + --silent \ + --show-error \ + --output /dev/null \ + "https://api.github.com/repos/snyk/endpoint-binary-explorer/actions/workflows/refresh-cli-data.yml/dispatches") || curl_status=$? + + if [ "$curl_status" -ne 0 ]; then + echo "WARNING: Endpoint Explorer $channel refresh dispatch failed (curl exit $curl_status); continuing the CLI release." + elif [ "$response" != "204" ]; then + echo "WARNING: Endpoint Explorer $channel refresh was not requested (HTTP $response); continuing the CLI release." + else + echo "Endpoint Explorer $channel refresh requested." + fi + + return 0 +} + trigger_build_agentic_integration() { echo "Triggering build-and-release workflow at agentic-integration-wrappers..." echo "Version: $VERSION_TAG" @@ -289,6 +327,12 @@ for arg in "${@}"; do # Trigger builds across distribution channel repositories elif [ "${arg}" == "trigger-distribution-channels" ]; then DISTRIBUTION_FAILURE=0 + + # Refresh the Endpoint Explorer independently of the required distribution + # triggers. Release candidates are not represented by an Explorer channel. + if [ "$RELEASE_CHANNEL" == "stable" ] || [ "$RELEASE_CHANNEL" == "preview" ]; then + trigger_endpoint_explorer_refresh "$RELEASE_CHANNEL" + fi # 1. Trigger snyk-images trigger_repository_event "snyk-images" "build_and_push_images" diff --git a/release-scripts/upload-artifacts_test.go b/release-scripts/upload-artifacts_test.go new file mode 100644 index 0000000000..df6d78d5ac --- /dev/null +++ b/release-scripts/upload-artifacts_test.go @@ -0,0 +1,158 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestDistributionTriggersRequestEndpointExplorerRefresh(t *testing.T) { + for _, channel := range []string{"stable", "preview"} { + t.Run(channel, func(t *testing.T) { + result := runDistributionTriggers(t, channel, "204", "0") + + if result.err != nil { + t.Fatalf("expected successful distribution triggers, got %v\n%s", result.err, result.output) + } + if !strings.Contains(result.output, "Endpoint Explorer "+channel+" refresh requested") { + t.Fatalf("expected accepted Explorer dispatch message, got:\n%s", result.output) + } + if !strings.Contains(result.curlArguments, "endpoint-binary-explorer/actions/workflows/refresh-cli-data.yml/dispatches") { + t.Fatalf("expected the Explorer workflow dispatch endpoint, got:\n%s", result.curlArguments) + } + if !strings.Contains(result.curlArguments, `"channel":"`+channel+`"`) { + t.Fatalf("expected %s workflow input, got:\n%s", channel, result.curlArguments) + } + if !strings.Contains(result.curlArguments, "--connect-timeout\n2") || !strings.Contains(result.curlArguments, "--max-time\n5") { + t.Fatalf("expected bounded Explorer dispatch timeouts, got:\n%s", result.curlArguments) + } + }) + } +} + +func TestDistributionTriggersIgnoreEndpointExplorerHTTPFailure(t *testing.T) { + result := runDistributionTriggers(t, "preview", "503", "0") + + if result.err != nil { + t.Fatalf("expected Explorer failure not to fail distribution triggers, got %v\n%s", result.err, result.output) + } + if !strings.Contains(result.output, "WARNING: Endpoint Explorer preview refresh was not requested") { + t.Fatalf("expected a warning, got:\n%s", result.output) + } +} + +func TestDistributionTriggersIgnoreEndpointExplorerTransportFailure(t *testing.T) { + result := runDistributionTriggers(t, "stable", "000", "28") + + if result.err != nil { + t.Fatalf("expected Explorer failure not to fail distribution triggers, got %v\n%s", result.err, result.output) + } + if !strings.Contains(result.output, "WARNING: Endpoint Explorer stable refresh dispatch failed") { + t.Fatalf("expected a warning, got:\n%s", result.output) + } +} + +func TestDistributionTriggersSkipEndpointExplorerForReleaseCandidate(t *testing.T) { + result := runDistributionTriggers(t, "rc", "204", "0") + + if result.err != nil { + t.Fatalf("expected successful distribution triggers, got %v\n%s", result.err, result.output) + } + if strings.Contains(result.curlArguments, "endpoint-binary-explorer") { + t.Fatalf("expected no Explorer refresh for release candidates, got:\n%s", result.curlArguments) + } +} + +type distributionTriggerResult struct { + output string + curlArguments string + err error +} + +func runDistributionTriggers(t *testing.T, channel string, explorerHTTPStatus string, explorerExitStatus string) distributionTriggerResult { + t.Helper() + + rootDir := t.TempDir() + releaseScriptsDir := filepath.Join(rootDir, "release-scripts") + binaryReleasesDir := filepath.Join(rootDir, "binary-releases") + binDir := filepath.Join(rootDir, "bin") + for _, directory := range []string{releaseScriptsDir, binaryReleasesDir, binDir} { + if err := os.MkdirAll(directory, 0755); err != nil { + t.Fatal(err) + } + } + + copyTestFile(t, repoPath(t, "release-scripts", "upload-artifacts.sh"), filepath.Join(releaseScriptsDir, "upload-artifacts.sh"), 0755) + writeTestFile(t, filepath.Join(releaseScriptsDir, "determine-release-channel.sh"), fmt.Sprintf("#!/usr/bin/env bash\necho %s\n", channel), 0755) + writeTestFile(t, filepath.Join(binaryReleasesDir, "version"), "1.2.3\n", 0644) + writeTestFile(t, filepath.Join(binaryReleasesDir, "ls-protocol-version-test"), "1\n", 0644) + + curlLog := filepath.Join(rootDir, "curl-arguments") + mockCurl := `#!/usr/bin/env bash +printf '%s\n' '--- request ---' "$@" >> "$MOCK_CURL_LOG" +if [[ "$*" == *"endpoint-binary-explorer/actions/workflows"* ]]; then + printf '%s' "$MOCK_EXPLORER_HTTP_STATUS" + exit "$MOCK_EXPLORER_EXIT_STATUS" +fi +printf '204' +` + writeTestFile(t, filepath.Join(binDir, "curl"), mockCurl, 0755) + + command := exec.Command("bash", "./release-scripts/upload-artifacts.sh", "trigger-distribution-channels") + command.Dir = rootDir + command.Env = environmentWithOverrides(os.Environ(), map[string]string{ + "HAMMERHEAD_GITHUB_PAT": "test-token", + "MOCK_CURL_LOG": curlLog, + "MOCK_EXPLORER_EXIT_STATUS": explorerExitStatus, + "MOCK_EXPLORER_HTTP_STATUS": explorerHTTPStatus, + "PATH": binDir + string(os.PathListSeparator) + os.Getenv("PATH"), + "TMPDIR": rootDir, + }) + output, err := command.CombinedOutput() + curlArguments, readErr := os.ReadFile(curlLog) + if readErr != nil { + t.Fatal(readErr) + } + + return distributionTriggerResult{ + output: string(output), + curlArguments: string(curlArguments), + err: err, + } +} + +func copyTestFile(t *testing.T, source string, destination string, mode os.FileMode) { + t.Helper() + contents, err := os.ReadFile(source) + if err != nil { + t.Fatal(err) + } + writeTestFile(t, destination, string(contents), mode) +} + +func writeTestFile(t *testing.T, path string, contents string, mode os.FileMode) { + t.Helper() + if err := os.WriteFile(path, []byte(contents), mode); err != nil { + t.Fatal(err) + } +} + +func environmentWithOverrides(current []string, overrides map[string]string) []string { + result := make([]string, 0, len(current)+len(overrides)) + for _, entry := range current { + key, _, found := strings.Cut(entry, "=") + if !found { + continue + } + if _, overridden := overrides[key]; !overridden { + result = append(result, entry) + } + } + for key, value := range overrides { + result = append(result, key+"="+value) + } + return result +}