Allow for a personal token to be specified for self-updates (#5568)

* Allow for a personal token to be specified for self-updates

This change will allow for setting the $GITHUB_ACCESS_TOKEN environment variable with a Github personal access token, allowing e.g. for higher rate limits

* Refactor github request and add test

---------

Co-authored-by: Paulo Saraiva <pauloman@cern.ch>
This commit is contained in:
Paulo Saraiva
2025-11-17 21:39:39 +01:00
committed by GitHub
co-authored by Paulo Saraiva
parent 3826167474
commit c1a89d5150
4 changed files with 71 additions and 9 deletions
+8
View File
@@ -0,0 +1,8 @@
Enhancement: Allow Github personal access token to be specified for `self-update`
`restic self-update` previously only used unauthenticated GitHub API requests when checking for the latest release. This caused some users sharing IP addresses to hit the GitHub rate limit, resulting in a 403 Forbidden error and preventing updates.
Restic still uses unauthenticated requests by default, but it now optionally supports authenticated GitHub API requests during `self-update`. Users can set the `$GITHUB_ACCESS_TOKEN` environment variable to use a [personal access token](https://github.com/settings/tokens) for this effect, avoiding update failures due to rate limiting.
https://github.com/restic/restic/issues/3738
https://github.com/restic/restic/pull/5568
+6 -1
View File
@@ -25,7 +25,12 @@ you can download and run without having to do additional installation work.
Please see the :ref:`official_binaries` section below for various downloads.
Official binaries can be updated in place by using the ``restic self-update``
command.
command.
The environment variable ``$GITHUB_ACCESS_TOKEN`` can be set to use a personal
access token when updating. This increases the rate limit through authenticated GitHub API
requests, and prevents update failures when many
unauthenticated requests have already been made from the same IP.
Alpine Linux
============
+20 -8
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
@@ -45,6 +46,23 @@ type githubError struct {
Message string
}
func newGitHubRequest(ctx context.Context, url, acceptHeader string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
// Set the Accept header to pin the API version
req.Header.Set("Accept", acceptHeader)
// Add Authorization header if token is available
if token := os.Getenv("GITHUB_ACCESS_TOKEN"); token != "" {
req.Header.Set("Authorization", "token "+token)
}
return req, nil
}
// GitHubLatestRelease uses the GitHub API to get information about the latest
// release of a repository.
func GitHubLatestRelease(ctx context.Context, owner, repo string) (Release, error) {
@@ -52,14 +70,11 @@ func GitHubLatestRelease(ctx context.Context, owner, repo string) (Release, erro
defer cancel()
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req, err := newGitHubRequest(ctx, url, "application/vnd.github.v3+json")
if err != nil {
return Release{}, err
}
// pin API version 3
req.Header.Set("Accept", "application/vnd.github.v3+json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return Release{}, err
@@ -111,14 +126,11 @@ func GitHubLatestRelease(ctx context.Context, owner, repo string) (Release, erro
}
func getGithubData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req, err := newGitHubRequest(ctx, url, "application/octet-stream")
if err != nil {
return nil, err
}
// request binary data
req.Header.Set("Accept", "application/octet-stream")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
+37
View File
@@ -0,0 +1,37 @@
package selfupdate
import (
"context"
"net/http"
"testing"
rtest "github.com/restic/restic/internal/test"
)
func TestNewGitHubRequest(t *testing.T) {
ctx := context.Background()
url := "https://api.github.com/repos/restic/restic/releases/latest"
acceptHeader := "application/vnd.github.v3+json"
t.Run("With GITHUB_ACCESS_TOKEN", func(t *testing.T) {
expectedToken := "testtoken123"
t.Setenv("GITHUB_ACCESS_TOKEN", expectedToken)
req, err := newGitHubRequest(ctx, url, acceptHeader)
rtest.OK(t, err)
rtest.Assert(t, req.Method == http.MethodGet, "expected method %s, got %s", http.MethodGet, req.Method)
rtest.Assert(t, req.URL.String() == url, "expected URL %s, got %s", url, req.URL.String())
rtest.Assert(t, req.Header.Get("Accept") == acceptHeader, "expected Accept header %s, got %s", acceptHeader, req.Header.Get("Accept"))
rtest.Assert(t, req.Header.Get("Authorization") == "token "+expectedToken, "expected Authorization header 'token %s', got %s", expectedToken, req.Header.Get("Authorization"))
})
t.Run("Without GITHUB_ACCESS_TOKEN", func(t *testing.T) {
t.Setenv("GITHUB_ACCESS_TOKEN", "")
req, err := newGitHubRequest(ctx, url, acceptHeader)
rtest.OK(t, err)
rtest.Assert(t, req.Header.Get("Authorization") == "", "expected no Authorization header, got %s", req.Header.Get("Authorization"))
})
}