Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions release-scripts/upload-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Comment on lines +185 to +189

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should also include retries, for example--retry 2 --retry-connrefused?

--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 \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't we follow same pattern of trigger_repository_event and output to a file? Then we could have more information about a failure?

"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
Comment on lines +200 to +208

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we have a different return value for the "WARNING" logs? Because right now it just logs (and might get lost / nobody sees it), but continues the flow normally in all cases.

}

trigger_build_agentic_integration() {
echo "Triggering build-and-release workflow at agentic-integration-wrappers..."
echo "Version: $VERSION_TAG"
Expand Down Expand Up @@ -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"
Expand Down
158 changes: 158 additions & 0 deletions release-scripts/upload-artifacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

func TestDistributionTriggersRequestEndpointExplorerRefresh(t *testing.T) {

@danskmt danskmt Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe you have to plug these tests to run in the CI, e.g. in config.yml, under jobs, under test-go:

- run:
      name: Running release script tests
      command: make test-release-scripts

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
}