You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2026/02/12 14:53:12 UTC

(incubator-devlake) branch main updated: fix(github): made token expiration fields nullable and updated related logic (#8707)

This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 32eafc80b fix(github): made token expiration fields nullable and updated related logic (#8707)
32eafc80b is described below

commit 32eafc80b8cfcaedeaef5273e777e5d37763e938
Author: Yuvraj Singh Chauhan <24...@iiitu.ac.in>
AuthorDate: Thu Feb 12 20:23:07 2026 +0530

    fix(github): made token expiration fields nullable and updated related logic (#8707)
---
 backend/plugins/github/models/connection.go        |  8 ++--
 ...dify_connection_token_expires_at_to_nullable.go | 52 ++++++++++++++++++++++
 .../github/models/migrationscripts/register.go     |  1 +
 backend/plugins/github/token/round_tripper_test.go |  3 +-
 backend/plugins/github/token/token_provider.go     | 12 +++--
 .../plugins/github/token/token_provider_test.go    | 18 +++++---
 6 files changed, 80 insertions(+), 14 deletions(-)

diff --git a/backend/plugins/github/models/connection.go b/backend/plugins/github/models/connection.go
index 4dba2f756..d03353c18 100644
--- a/backend/plugins/github/models/connection.go
+++ b/backend/plugins/github/models/connection.go
@@ -56,13 +56,13 @@ type GithubConn struct {
 	helper.MultiAuth      `mapstructure:",squash"`
 	GithubAccessToken     `mapstructure:",squash" authMethod:"AccessToken"`
 	GithubAppKey          `mapstructure:",squash" authMethod:"AppKey"`
-	RefreshToken          string    `mapstructure:"refreshToken" json:"refreshToken" gorm:"type:text;serializer:encdec"`
-	TokenExpiresAt        time.Time `mapstructure:"tokenExpiresAt" json:"tokenExpiresAt"`
-	RefreshTokenExpiresAt time.Time `mapstructure:"refreshTokenExpiresAt" json:"refreshTokenExpiresAt"`
+	RefreshToken          string     `mapstructure:"refreshToken" json:"refreshToken" gorm:"type:text;serializer:encdec"`
+	TokenExpiresAt        *time.Time `mapstructure:"tokenExpiresAt" json:"tokenExpiresAt"`
+	RefreshTokenExpiresAt *time.Time `mapstructure:"refreshTokenExpiresAt" json:"refreshTokenExpiresAt"`
 }
 
 // UpdateToken updates the token and refresh token information
-func (conn *GithubConn) UpdateToken(newToken, newRefreshToken string, expiry, refreshExpiry time.Time) {
+func (conn *GithubConn) UpdateToken(newToken, newRefreshToken string, expiry, refreshExpiry *time.Time) {
 	conn.Token = newToken
 	conn.RefreshToken = newRefreshToken
 	conn.TokenExpiresAt = expiry
diff --git a/backend/plugins/github/models/migrationscripts/20260211_modify_connection_token_expires_at_to_nullable.go b/backend/plugins/github/models/migrationscripts/20260211_modify_connection_token_expires_at_to_nullable.go
new file mode 100644
index 000000000..ff7b127fb
--- /dev/null
+++ b/backend/plugins/github/models/migrationscripts/20260211_modify_connection_token_expires_at_to_nullable.go
@@ -0,0 +1,52 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"time"
+
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+type githubConnection20260211 struct {
+	TokenExpiresAt        *time.Time
+	RefreshTokenExpiresAt *time.Time
+}
+
+func (githubConnection20260211) TableName() string {
+	return "_tool_github_connections"
+}
+
+type modifyTokenExpiresAtToNullable struct{}
+
+func (*modifyTokenExpiresAtToNullable) Up(basicRes context.BasicRes) errors.Error {
+	return migrationhelper.AutoMigrateTables(
+		basicRes,
+		&githubConnection20260211{},
+	)
+}
+
+func (*modifyTokenExpiresAtToNullable) Version() uint64 {
+	return 20260211000001
+}
+
+func (*modifyTokenExpiresAtToNullable) Name() string {
+	return "modify token_expires_at and refresh_token_expires_at to nullable"
+}
diff --git a/backend/plugins/github/models/migrationscripts/register.go b/backend/plugins/github/models/migrationscripts/register.go
index 74f9d712b..009497289 100644
--- a/backend/plugins/github/models/migrationscripts/register.go
+++ b/backend/plugins/github/models/migrationscripts/register.go
@@ -56,5 +56,6 @@ func All() []plugin.MigrationScript {
 		new(changeIssueComponentType),
 		new(addIndexToGithubJobs),
 		new(addRefreshTokenFields),
+		new(modifyTokenExpiresAtToNullable),
 	}
 }
diff --git a/backend/plugins/github/token/round_tripper_test.go b/backend/plugins/github/token/round_tripper_test.go
index 6767d8ccf..e766adb88 100644
--- a/backend/plugins/github/token/round_tripper_test.go
+++ b/backend/plugins/github/token/round_tripper_test.go
@@ -36,6 +36,7 @@ func TestRoundTripper401Refresh(t *testing.T) {
 	mockRT := new(MockRoundTripper)
 	client := &http.Client{Transport: mockRT}
 
+	expiry := time.Now().Add(10 * time.Minute) // Not expired
 	conn := &models.GithubConnection{
 		GithubConn: models.GithubConn{
 			RefreshToken: "refresh_token",
@@ -44,7 +45,7 @@ func TestRoundTripper401Refresh(t *testing.T) {
 					Token: "old_token",
 				},
 			},
-			TokenExpiresAt: time.Now().Add(10 * time.Minute), // Not expired
+			TokenExpiresAt: &expiry,
 			GithubAppKey: models.GithubAppKey{
 				AppKey: api.AppKey{
 					AppId:     "123",
diff --git a/backend/plugins/github/token/token_provider.go b/backend/plugins/github/token/token_provider.go
index ba9941cd4..ed3fa2256 100644
--- a/backend/plugins/github/token/token_provider.go
+++ b/backend/plugins/github/token/token_provider.go
@@ -84,7 +84,10 @@ func (tp *TokenProvider) needsRefresh() bool {
 		}
 	}
 
-	return time.Now().Add(buffer).After(tp.conn.TokenExpiresAt)
+	if tp.conn.TokenExpiresAt == nil {
+		return false
+	}
+	return time.Now().Add(buffer).After(*tp.conn.TokenExpiresAt)
 }
 
 func (tp *TokenProvider) refreshToken() errors.Error {
@@ -145,11 +148,14 @@ func (tp *TokenProvider) refreshToken() errors.Error {
 		return errors.Default.New(fmt.Sprintf("empty access token returned; response body: %s", bodyStr))
 	}
 
+	tokenExpiredAt := time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
+	refreshTokenExpiredAt := time.Now().Add(time.Duration(result.RefreshTokenExpiresIn) * time.Second)
+
 	tp.conn.UpdateToken(
 		result.AccessToken,
 		result.RefreshToken,
-		time.Now().Add(time.Duration(result.ExpiresIn)*time.Second),
-		time.Now().Add(time.Duration(result.RefreshTokenExpiresIn)*time.Second),
+		&tokenExpiredAt,
+		&refreshTokenExpiredAt,
 	)
 
 	if tp.dal != nil {
diff --git a/backend/plugins/github/token/token_provider_test.go b/backend/plugins/github/token/token_provider_test.go
index 1c296376a..3319f5591 100644
--- a/backend/plugins/github/token/token_provider_test.go
+++ b/backend/plugins/github/token/token_provider_test.go
@@ -55,15 +55,18 @@ func TestNeedsRefresh(t *testing.T) {
 	}
 
 	// Not expired, outside buffer
-	tp.conn.TokenExpiresAt = time.Now().Add(10 * time.Minute)
+	expiry1 := time.Now().Add(10 * time.Minute)
+	tp.conn.TokenExpiresAt = &expiry1
 	assert.False(t, tp.needsRefresh())
 
 	// Inside buffer
-	tp.conn.TokenExpiresAt = time.Now().Add(1 * time.Minute)
+	expiry2 := time.Now().Add(1 * time.Minute)
+	tp.conn.TokenExpiresAt = &expiry2
 	assert.True(t, tp.needsRefresh())
 
 	// Expired
-	tp.conn.TokenExpiresAt = time.Now().Add(-1 * time.Minute)
+	expiry3 := time.Now().Add(-1 * time.Minute)
+	tp.conn.TokenExpiresAt = &expiry3
 	assert.True(t, tp.needsRefresh())
 
 	// No refresh token
@@ -75,10 +78,11 @@ func TestTokenProviderConcurrency(t *testing.T) {
 	mockRT := new(MockRoundTripper)
 	client := &http.Client{Transport: mockRT}
 
+	expired := time.Now().Add(-1 * time.Minute) // Expired
 	conn := &models.GithubConnection{
 		GithubConn: models.GithubConn{
 			RefreshToken:   "refresh_token",
-			TokenExpiresAt: time.Now().Add(-1 * time.Minute), // Expired
+			TokenExpiresAt: &expired,
 			GithubAppKey: models.GithubAppKey{
 				AppKey: api.AppKey{
 					AppId:     "123",
@@ -129,11 +133,13 @@ func TestConfigurableBuffer(t *testing.T) {
 	}
 
 	// 9 minutes remaining (inside 10m buffer)
-	tp.conn.TokenExpiresAt = time.Now().Add(9 * time.Minute)
+	expiry9 := time.Now().Add(9 * time.Minute)
+	tp.conn.TokenExpiresAt = &expiry9
 	assert.True(t, tp.needsRefresh())
 
 	// 11 minutes remaining (outside 10m buffer)
-	tp.conn.TokenExpiresAt = time.Now().Add(11 * time.Minute)
+	expiry11 := time.Now().Add(11 * time.Minute)
+	tp.conn.TokenExpiresAt = &expiry11
 	assert.False(t, tp.needsRefresh())
 }