feat: support GitHub token for update checks
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
type githubReleaseClient struct {
|
||||
httpClient *http.Client
|
||||
downloadHTTPClient *http.Client
|
||||
updateGitHubToken string
|
||||
}
|
||||
|
||||
type githubReleaseClientError struct {
|
||||
@@ -43,6 +45,8 @@ func NewGitHubReleaseClient(proxyURL string, allowDirectOnProxyError bool) servi
|
||||
}
|
||||
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
||||
}
|
||||
apiClient := cloneHTTPClient(sharedClient)
|
||||
apiClient.CheckRedirect = githubAPICheckRedirect(apiClient.CheckRedirect)
|
||||
|
||||
// 下载客户端需要更长的超时时间
|
||||
downloadClient, err := httpclient.GetClient(httpclient.Options{
|
||||
@@ -56,13 +60,50 @@ func NewGitHubReleaseClient(proxyURL string, allowDirectOnProxyError bool) servi
|
||||
}
|
||||
downloadClient = &http.Client{Timeout: 10 * time.Minute}
|
||||
}
|
||||
downloadClient = cloneHTTPClient(downloadClient)
|
||||
|
||||
return &githubReleaseClient{
|
||||
httpClient: sharedClient,
|
||||
httpClient: apiClient,
|
||||
downloadHTTPClient: downloadClient,
|
||||
updateGitHubToken: os.Getenv("UPDATE_GITHUB_TOKEN"),
|
||||
}
|
||||
}
|
||||
|
||||
func cloneHTTPClient(client *http.Client) *http.Client {
|
||||
cloned := *client
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func isGitHubAPIURL(url *url.URL) bool {
|
||||
return url != nil && strings.EqualFold(url.Scheme, "https") && url.User == nil &&
|
||||
strings.EqualFold(url.Host, "api.github.com")
|
||||
}
|
||||
|
||||
func githubAPICheckRedirect(previous func(*http.Request, []*http.Request) error) func(*http.Request, []*http.Request) error {
|
||||
return func(req *http.Request, via []*http.Request) error {
|
||||
if !isGitHubAPIURL(req.URL) {
|
||||
req.Header.Del("Authorization")
|
||||
}
|
||||
if previous != nil {
|
||||
return previous(req, via)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *githubReleaseClient) newAPIRequest(ctx context.Context, url string) (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
req.Header.Set("User-Agent", "Sub2API-Updater")
|
||||
if c.updateGitHubToken != "" && isGitHubAPIURL(req.URL) {
|
||||
req.Header.Set("Authorization", "Bearer "+c.updateGitHubToken)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *githubReleaseClientError) FetchLatestRelease(ctx context.Context, repo string) (*service.GitHubRelease, error) {
|
||||
return nil, c.err
|
||||
}
|
||||
@@ -82,12 +123,10 @@ func (c *githubReleaseClientError) FetchChecksumFile(ctx context.Context, url st
|
||||
func (c *githubReleaseClient) FetchLatestRelease(ctx context.Context, repo string) (*service.GitHubRelease, error) {
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
req, err := c.newAPIRequest(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
req.Header.Set("User-Agent", "Sub2API-Updater")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -116,12 +155,10 @@ func (c *githubReleaseClient) FetchRecentReleases(ctx context.Context, repo stri
|
||||
}
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=%d", repo, perPage)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
req, err := c.newAPIRequest(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
req.Header.Set("User-Agent", "Sub2API-Updater")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -47,6 +48,97 @@ func newTestGitHubReleaseClient() *githubReleaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubReleaseClientAPIRequestAuthorization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantAuth string
|
||||
}{
|
||||
{name: "exact HTTPS authority", url: "https://api.github.com/repos/test/repo", wantAuth: "Bearer update-secret"},
|
||||
{name: "HTTP", url: "http://api.github.com/repos/test/repo"},
|
||||
{name: "subdomain", url: "https://sub.api.github.com/repos/test/repo"},
|
||||
{name: "userinfo", url: "https://user@api.github.com/repos/test/repo"},
|
||||
{name: "explicit default port", url: "https://api.github.com:443/repos/test/repo"},
|
||||
{name: "custom port", url: "https://api.github.com:8443/repos/test/repo"},
|
||||
{name: "different host", url: "https://github.com/test/repo"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := newTestGitHubReleaseClient()
|
||||
client.updateGitHubToken = "update-secret"
|
||||
req, err := client.newAPIRequest(context.Background(), tt.url)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantAuth, req.Header.Get("Authorization"))
|
||||
})
|
||||
}
|
||||
|
||||
client := newTestGitHubReleaseClient()
|
||||
req, err := client.newAPIRequest(context.Background(), "https://api.github.com/repos/test/repo")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, req.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestGitHubReleaseClientRedirectAuthorization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantAuth string
|
||||
}{
|
||||
{name: "same HTTPS authority", url: "https://api.github.com/redirected", wantAuth: "Bearer update-secret"},
|
||||
{name: "HTTP", url: "http://api.github.com/redirected"},
|
||||
{name: "subdomain", url: "https://sub.api.github.com/redirected"},
|
||||
{name: "userinfo", url: "https://user@api.github.com/redirected"},
|
||||
{name: "custom port", url: "https://api.github.com:8443/redirected"},
|
||||
{name: "different host", url: "https://example.com/redirected"},
|
||||
}
|
||||
|
||||
checkRedirect := githubAPICheckRedirect(nil)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, tt.url, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer update-secret")
|
||||
|
||||
require.NoError(t, checkRedirect(req, nil))
|
||||
require.Equal(t, tt.wantAuth, req.Header.Get("Authorization"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubReleaseClientDoesNotAuthorizeDownloads(t *testing.T) {
|
||||
client := newTestGitHubReleaseClient()
|
||||
client.updateGitHubToken = "update-secret"
|
||||
|
||||
var headers []http.Header
|
||||
transport := githubReleaseRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
headers = append(headers, req.Header.Clone())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("checksum")),
|
||||
Request: req,
|
||||
}, nil
|
||||
})
|
||||
client.httpClient.Transport = transport
|
||||
client.downloadHTTPClient.Transport = transport
|
||||
|
||||
dest := filepath.Join(t.TempDir(), "asset")
|
||||
require.NoError(t, client.DownloadFile(context.Background(), "https://objects.githubusercontent.com/asset", dest, 100))
|
||||
_, err := client.FetchChecksumFile(context.Background(), "https://github.com/test/repo/releases/download/v1/checksums.txt")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, headers, 2)
|
||||
for _, header := range headers {
|
||||
require.Empty(t, header.Get("Authorization"))
|
||||
}
|
||||
}
|
||||
|
||||
type githubReleaseRoundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f githubReleaseRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func (s *GitHubReleaseServiceSuite) SetupTest() {
|
||||
s.tempDir = s.T().TempDir()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ SERVER_PORT=8080
|
||||
# Server mode: release or debug
|
||||
SERVER_MODE=release
|
||||
|
||||
# Optional token used only for GitHub Release API update checks. Release asset
|
||||
# downloads remain anonymous. GITHUB_TOKEN and GH_TOKEN are not used.
|
||||
UPDATE_GITHUB_TOKEN=
|
||||
|
||||
# Return Server-Timing for authenticated requests made by the Admin web UI
|
||||
ENABLE_SERVER_TIMING=false
|
||||
|
||||
|
||||
@@ -238,6 +238,7 @@ docker compose down -v
|
||||
| `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email |
|
||||
| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password |
|
||||
| `TZ` | No | `Asia/Shanghai` | Timezone |
|
||||
| `UPDATE_GITHUB_TOKEN` | No | *(empty)* | Token for `api.github.com` release checks only; asset downloads remain anonymous. |
|
||||
| `GEMINI_OAUTH_CLIENT_ID` | No | *(builtin)* | Google OAuth client ID (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
|
||||
| `GEMINI_OAUTH_CLIENT_SECRET` | No | *(builtin)* | Google OAuth client secret (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
|
||||
| `GEMINI_OAUTH_SCOPES` | No | *(default)* | OAuth scopes (Gemini OAuth) |
|
||||
|
||||
@@ -53,6 +53,7 @@ services:
|
||||
- SERVER_MODE=${SERVER_MODE:-release}
|
||||
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
|
||||
- RUN_MODE=${RUN_MODE:-standard}
|
||||
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
|
||||
|
||||
# =======================================================================
|
||||
# Database Configuration (PostgreSQL)
|
||||
|
||||
@@ -39,6 +39,7 @@ services:
|
||||
- SERVER_MODE=${SERVER_MODE:-release}
|
||||
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
|
||||
- RUN_MODE=${RUN_MODE:-standard}
|
||||
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
|
||||
|
||||
# =======================================================================
|
||||
# Database Configuration (PostgreSQL) - Required
|
||||
|
||||
@@ -49,6 +49,7 @@ services:
|
||||
- SERVER_MODE=${SERVER_MODE:-release}
|
||||
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
|
||||
- RUN_MODE=${RUN_MODE:-standard}
|
||||
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
|
||||
|
||||
# =======================================================================
|
||||
# Database Configuration (PostgreSQL)
|
||||
|
||||
+51
-3
@@ -480,10 +480,58 @@ check_dependencies() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Authenticate only GitHub REST API requests. Release asset downloads must stay anonymous.
|
||||
github_api_curl() {
|
||||
local arg
|
||||
local expect_value=false
|
||||
local url
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "github_api_curl requires exactly one GitHub API URL" >&2
|
||||
return 2
|
||||
fi
|
||||
url="${!#}"
|
||||
|
||||
# Keep authenticated invocations constrained to the options used below. In
|
||||
# particular, curl config, --url, and --next could add another destination.
|
||||
for arg in "${@:1:$#-1}"; do
|
||||
if [ "$expect_value" = true ]; then
|
||||
expect_value=false
|
||||
continue
|
||||
fi
|
||||
case "$arg" in
|
||||
-s|--silent)
|
||||
;;
|
||||
--connect-timeout|--max-time|-o|--output|-w|--write-out)
|
||||
expect_value=true
|
||||
;;
|
||||
*)
|
||||
echo "Unsafe github_api_curl argument: $arg" >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$expect_value" = true ] || [[ "$url" != https://api.github.com/* ]]; then
|
||||
echo "github_api_curl requires exactly one GitHub API URL" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
if [ -n "${UPDATE_GITHUB_TOKEN:-}" ]; then
|
||||
if [[ "$UPDATE_GITHUB_TOKEN" == *$'\n'* || "$UPDATE_GITHUB_TOKEN" == *$'\r'* || "$UPDATE_GITHUB_TOKEN" == *'"'* || "$UPDATE_GITHUB_TOKEN" == *'\'* ]]; then
|
||||
echo "UPDATE_GITHUB_TOKEN contains unsupported characters" >&2
|
||||
return 2
|
||||
fi
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$UPDATE_GITHUB_TOKEN" | UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff --config - "$@"
|
||||
else
|
||||
UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get latest release version
|
||||
get_latest_version() {
|
||||
print_info "$(msg 'fetching_version')"
|
||||
LATEST_VERSION=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
LATEST_VERSION=$(github_api_curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
print_error "$(msg 'failed_get_version')"
|
||||
@@ -499,7 +547,7 @@ list_versions() {
|
||||
print_info "$(msg 'fetching_versions')"
|
||||
|
||||
local versions
|
||||
versions=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' | head -20)
|
||||
versions=$(github_api_curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' | head -20)
|
||||
|
||||
if [ -z "$versions" ]; then
|
||||
print_error "$(msg 'failed_get_version')"
|
||||
@@ -536,7 +584,7 @@ validate_version() {
|
||||
|
||||
# Check if the release exists
|
||||
local http_code
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${version}" 2>/dev/null)
|
||||
http_code=$(github_api_curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${version}" 2>/dev/null)
|
||||
|
||||
# Check for network errors (empty or non-numeric response)
|
||||
if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
|
||||
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
cat > "$TEMP_DIR/curl" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$@" > "$CURL_ARGS_LOG"
|
||||
env > "${CURL_ARGS_LOG}.env"
|
||||
cat > "${CURL_ARGS_LOG}.stdin"
|
||||
EOF
|
||||
chmod +x "$TEMP_DIR/curl"
|
||||
|
||||
mkdir "$TEMP_DIR/home"
|
||||
cat > "$TEMP_DIR/home/.curlrc" <<'EOF'
|
||||
url = "https://example.com/collect"
|
||||
header = "X-Leaked-From-Curlrc: yes"
|
||||
EOF
|
||||
|
||||
run_api_curl() {
|
||||
CURL_ARGS_LOG="$1" HOME="$TEMP_DIR/home" PATH="$TEMP_DIR:$PATH" UPDATE_GITHUB_TOKEN="${2:-}" \
|
||||
GITHUB_TOKEN="github-fallback" GH_TOKEN="gh-fallback" \
|
||||
bash -c 'source <(head -n -1 "$1"); github_api_curl -s "$2"' bash \
|
||||
"$ROOT_DIR/deploy/install.sh" "https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest"
|
||||
}
|
||||
|
||||
run_api_curl "$TEMP_DIR/authenticated" "update-secret"
|
||||
test "$(head -n 1 "$TEMP_DIR/authenticated")" = '-q'
|
||||
grep -Fxq -- '--config' "$TEMP_DIR/authenticated"
|
||||
grep -Fxq -- '-' "$TEMP_DIR/authenticated"
|
||||
grep -Fxq -- '--globoff' "$TEMP_DIR/authenticated"
|
||||
grep -Fxq 'header = "Authorization: Bearer update-secret"' "$TEMP_DIR/authenticated.stdin"
|
||||
if grep -Fq 'update-secret' "$TEMP_DIR/authenticated"; then
|
||||
echo "installer exposed the update token in curl argv" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Eq 'update-secret|github-fallback|gh-fallback' "$TEMP_DIR/authenticated.env"; then
|
||||
echo "installer exposed a token in curl environment" >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(grep -Fxc 'https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest' "$TEMP_DIR/authenticated")" -eq 1
|
||||
if grep -Fq 'example.com/collect' "$TEMP_DIR/authenticated" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/authenticated" ||
|
||||
grep -Fq 'example.com/collect' "$TEMP_DIR/authenticated.stdin" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/authenticated.stdin"; then
|
||||
echo "installer allowed hostile curl config into authenticated invocation" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_api_curl "$TEMP_DIR/anonymous"
|
||||
test "$(head -n 1 "$TEMP_DIR/anonymous")" = '-q'
|
||||
if grep -Eq 'github-fallback|gh-fallback' "$TEMP_DIR/anonymous.env"; then
|
||||
echo "installer exposed a fallback token in anonymous curl environment" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Fq 'Authorization:' "$TEMP_DIR/anonymous"; then
|
||||
echo "installer unexpectedly used a fallback token" >&2
|
||||
exit 1
|
||||
fi
|
||||
test ! -s "$TEMP_DIR/anonymous.stdin"
|
||||
test "$(grep -Fxc 'https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest' "$TEMP_DIR/anonymous")" -eq 1
|
||||
if grep -Fq 'example.com/collect' "$TEMP_DIR/anonymous" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/anonymous"; then
|
||||
echo "installer allowed hostile curl config into anonymous invocation" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
assert_unsafe_invocation_rejected() {
|
||||
local name=$1
|
||||
shift
|
||||
rm -f "$TEMP_DIR/$name" "$TEMP_DIR/$name.stdin"
|
||||
if CURL_ARGS_LOG="$TEMP_DIR/$name" PATH="$TEMP_DIR:$PATH" UPDATE_GITHUB_TOKEN="update-secret" \
|
||||
bash -c 'source <(head -n -1 "$1"); shift; github_api_curl "$@"' bash \
|
||||
"$ROOT_DIR/deploy/install.sh" "$@" 2>/dev/null; then
|
||||
echo "installer accepted unsafe curl invocation: $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -e "$TEMP_DIR/$name" ]; then
|
||||
echo "installer invoked curl for unsafe request: $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_unsafe_invocation_rejected non-api -s \
|
||||
"https://github.com/Wei-Shaw/sub2api/releases/download/v1/asset"
|
||||
assert_unsafe_invocation_rejected mixed-host -s \
|
||||
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest" \
|
||||
"https://example.com/collect"
|
||||
assert_unsafe_invocation_rejected multiple-api -s \
|
||||
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest" \
|
||||
"https://api.github.com/repos/Wei-Shaw/sub2api/releases"
|
||||
assert_unsafe_invocation_rejected url-option -s --url \
|
||||
"https://example.com/collect" \
|
||||
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest"
|
||||
|
||||
# Every installer release API request must use the scoped helper.
|
||||
test "$(grep -c 'github_api_curl .*https://api.github.com/' "$ROOT_DIR/deploy/install.sh")" -eq 3
|
||||
|
||||
# Asset and checksum downloads must continue to call curl directly.
|
||||
grep -Fq 'curl -sL "$download_url"' "$ROOT_DIR/deploy/install.sh"
|
||||
grep -Fq 'curl -sL "$checksum_url"' "$ROOT_DIR/deploy/install.sh"
|
||||
|
||||
echo "install GitHub token checks passed"
|
||||
Reference in New Issue
Block a user