Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6cb2b83
feat(credentials): forward GitHub CLI authentication
atty303 Oct 17, 2026
deeab48
fix(credentials): forward gh auth for tokenless account settings
atty303 Oct 17, 2026
87883d1
fix(credentials): keep nested gh auth lookups local
atty303 Oct 18, 2026
ffc1c37
fix(credentials): preserve host gh configuration during forwarding
atty303 Oct 18, 2026
4681196
docs(credentials): remove redundant and inaccurate comments
atty303 Oct 18, 2026
0fedae9
fix(credentials): install gh relay wrapper on the standard PATH
atty303 Oct 18, 2026
de8c8ce
fix(credentials): preserve relay entry point in gh self references
atty303 Oct 18, 2026
15fb668
chore(credentials): remove redundant relay test comments
atty303 Oct 18, 2026
e21936e
refactor(credentials): use standard logging and command dispatch for gh
atty303 Oct 18, 2026
f2c6c3e
test(credentials): integrate gh relay coverage into the ssh suite
atty303 Oct 18, 2026
7506004
test(credentials): fix relay CI compatibility and fixture findings
atty303 Oct 18, 2026
8ed9256
fix(credentials): install the gh wrapper from non-root services
atty303 Oct 18, 2026
78b8d88
fix(credentials): use the gh backup when PATH omits its wrapper
atty303 Oct 18, 2026
b598c3c
fix(credentials): harden opt-in GitHub CLI forwarding
skevetter Oct 19, 2026
988659c
refactor(e2e): split credential race fixture checks
skevetter Oct 20, 2026
d3e66a6
fix(credentials): handle SSH relay handoff
skevetter Oct 20, 2026
b2f2579
fix(credentials): validate authentication recovery input
skevetter Oct 20, 2026
6c40780
fix(credentials): serialize aliases and observe service exits
skevetter Oct 20, 2026
ef8b6cf
fix(ci): use runner scratch for Go cross-builds
skevetter Oct 20, 2026
70dc59b
fix(credentials): tolerate guest profiles and stale links
skevetter Oct 21, 2026
6887c98
fix(credentials): preserve forwarding service boundaries
skevetter Oct 21, 2026
c5107c7
fix(credentials): preserve edits during publication checkpoints
skevetter Oct 21, 2026
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
67 changes: 67 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,45 @@ jobs:
if: runner.os == 'Linux'
run: go test -race -count=1 -timeout 90s ./e2e/framework/registryfixture

- name: test Linux GitHub API oracle
if: runner.os == 'Linux'
run: go test -race -count=1 -timeout 90s ./e2e/framework/ghfixture

github-credential-host-tests:
name: GitHub Credential Host Tests (${{ matrix.runner }})
needs: [changes]
if: needs.changes.outputs.go == 'true'
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- name: record native host and available CLI versions
run: |
go env GOOS GOARCH
gh --version
- name: execute native credential discovery and process-bound regressions
env:
DEVSY_TEST_REAL_GH: "1"
DEVSY_TEST_NATIVE_KEYRING: ${{ runner.os == 'macOS' && '1' || '0' }}
run: >-
go test -race -count=1 -v -timeout 5m ./pkg/ghcredentials
-run '^(TestNative.*|TestHostLookupEnvironmentAndGuestEnvironmentAreIndependent|TestRegisteredGuestDeclinesAmbiguousKeyringResult)$'
- name: test strict fixture report validation
run: go test -race -count=1 -v ./e2e/framework/ghfixture
- name: test native credential service boundaries
run: >-
go test -race -count=1 -v -timeout 2m ./pkg/tunnel ./pkg/credentials
-run '^(TestCredentials.*Host.*|TestOptionalGitHubService.*|TestServicesStartup.*)$'

windows-transport-tests:
name: Windows Transport Tests
needs: [changes]
Expand Down Expand Up @@ -272,6 +311,8 @@ jobs:
args: build --id devsy-${{ steps.os.outputs.runner_os }} --snapshot
env:
DEVSY_CLI_VERSION: v0.0.0
# Ubuntu 26.04 quotas /tmp: https://github.com/actions/runner-images/issues/14777
GOTMPDIR: ${{ runner.temp }}

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
Expand Down Expand Up @@ -302,6 +343,23 @@ jobs:
name: devsy-ci-microsandbox-linux
path: ${{ runner.temp }}/devsy-ci-microsandbox

- name: build Linux GitHub credential fixtures
if: runner.os == 'Linux'
shell: bash
env:
CGO_ENABLED: 0
run: |
for arch in amd64 arm64; do
GOOS=linux GOARCH="$arch" go build -trimpath -o "${RUNNER_TEMP}/ghfixture-linux-${arch}" ./e2e/cmd/ghfixture
done

- name: upload Linux GitHub credential fixtures
if: runner.os == 'Linux'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ghfixture-linux
path: ${{ runner.temp }}/ghfixture-linux-*

- name: build e2e test binary
if: matrix.runner == 'ubuntu-latest' || matrix.runner == 'windows-latest'
shell: bash
Expand Down Expand Up @@ -1016,6 +1074,13 @@ jobs:
name: e2e-test-${{ steps.os.outputs.runner_os }}
path: ./e2e/

- name: download GitHub credential fixture
if: matrix.label == 'ssh' && runner.os == 'Linux'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ghfixture-linux
path: ./e2e/bin/

- name: setup executable
if: matrix.requires-secret == false || needs.can-read-secret.outputs.secret-set == 'true'
shell: bash
Expand Down Expand Up @@ -1293,6 +1358,7 @@ jobs:
- lint
- agent-skill
- unit-tests
- github-credential-host-tests
- windows-transport-tests
- provider-update-windows
- licenses
Expand Down Expand Up @@ -1333,6 +1399,7 @@ jobs:
check_result "lint" "${{ needs.lint.result }}" false || status=1
check_result "agent-skill" "${{ needs.agent-skill.result }}" false || status=1
check_result "unit-tests" "${{ needs.unit-tests.result }}" true || status=1
check_result "github-credential-host-tests" "${{ needs.github-credential-host-tests.result }}" true || status=1
check_result "windows-transport-tests" "${{ needs.windows-transport-tests.result }}" true || status=1
check_result "provider-update-windows" "${{ needs.provider-update-windows.result }}" true || status=1
check_result "licenses" "${{ needs.licenses.result }}" true || status=1
Expand Down
1 change: 1 addition & 0 deletions cmd/internal/agentcontainer/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func NewContainerCmd(flags *flags.GlobalFlags) *cobra.Command {
containerCmd.AddCommand(newBrowserAsyncCmd(b))
}
containerCmd.AddCommand(NewCredentialsServerCmd(flags))
containerCmd.AddCommand(NewCredentialsStatusCmd())
containerCmd.AddCommand(NewSetupDevsyPlatformAccessCmd(flags))
containerCmd.AddCommand(NewSSHServerCmd(flags))
return containerCmd
Expand Down
49 changes: 49 additions & 0 deletions cmd/internal/agentcontainer/credentials_port.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package agentcontainer

import (
"context"
"errors"
"net"
"time"

"github.com/devsy-org/devsy/pkg/credentials"
)

func (cmd *CredentialsServerCmd) claimPort(ctx context.Context, port int) (net.Listener, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if !cmd.ConfigureGitHubHelper {
return claimPort(port)
}
return claimPortAfterDrain(ctx, port)
}

func claimPortAfterDrain(ctx context.Context, port int) (net.Listener, error) {
claimCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
for {
if err := ctx.Err(); err != nil {
return nil, err
}
ln, err := claimPort(port)
if !errors.Is(err, errPortOwnedByAnotherSession) {
return ln, err
}
_, probeErr := credentials.ProbeReadiness(
claimCtx,
port,
credentials.Readiness{GitHubAllowed: true},
)
if probeErr == nil {
return nil, err
}
select {
case <-claimCtx.Done():
return nil, errors.Join(err, ctx.Err())
case <-ticker.C:
}
}
}
107 changes: 107 additions & 0 deletions cmd/internal/agentcontainer/credentials_port_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package agentcontainer

import (
"context"
"encoding/json"
"net"
"net/http"
"testing"
"time"

"github.com/devsy-org/devsy/pkg/credentials"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGitHubCredentialsClaimWaitsForDrainingListener(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
port := ln.Addr().(*net.TCPAddr).Port
server := &http.Server{ReadHeaderTimeout: time.Second}
server.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
_ = ln.Close()
})
joined := make(chan struct{})
go func() { defer close(joined); _ = server.Serve(ln) }()
t.Cleanup(func() { _ = server.Close(); <-joined })
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
cmd := &CredentialsServerCmd{ConfigureGitHubHelper: true}
claimed, err := cmd.claimPort(ctx, port)
require.NoError(t, err, "a draining prior session must not suppress the next service")
require.NoError(t, claimed.Close())
}

func TestGitHubCredentialsClaimPreservesLiveOwner(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
port := ln.Addr().(*net.TCPAddr).Port
status := credentials.Readiness{
Version: credentials.ReadinessVersion, Owner: "other-user", HostOwner: "other-host",
Instance: "live-instance", GitHubAllowed: true, GitHubState: "installed",
}
server := &http.Server{
ReadHeaderTimeout: time.Second,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/ready", r.URL.Path)
_ = json.NewEncoder(w).Encode(status)
}),
}
joined := make(chan struct{})
go func() { defer close(joined); _ = server.Serve(ln) }()
t.Cleanup(func() { _ = server.Close(); <-joined })
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
cmd := &CredentialsServerCmd{User: "vscode", HostOwner: "new-host", ConfigureGitHubHelper: true}
claimed, err := cmd.claimPort(ctx, port)
require.Nil(t, claimed)
require.ErrorIs(t, err, errPortOwnedByAnotherSession)
_, err = credentials.ProbeReadiness(ctx, port, status)
require.NoError(t, err, "a live owner must remain untouched")
}

func TestGitHubCredentialsClaimCancellationPreservesUnresponsiveListener(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
port := ln.Addr().(*net.TCPAddr).Port
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
cmd := &CredentialsServerCmd{ConfigureGitHubHelper: true}
done := make(chan error, 1)
go func() {
claimed, claimErr := cmd.claimPort(ctx, port)
if claimed != nil {
_ = claimed.Close()
}
done <- claimErr
}()
require.NoError(t, ln.(*net.TCPListener).SetDeadline(time.Now().Add(3*time.Second)))
conn, err := ln.Accept()
require.NoError(t, err)
t.Cleanup(func() { _ = conn.Close() })
cancel()
err = <-done
require.ErrorIs(t, err, context.Canceled)
require.ErrorIs(t, err, errPortOwnedByAnotherSession)
_, err = claimPort(port)
require.ErrorIs(t, err, errPortOwnedByAnotherSession)
}

func TestGitHubCredentialsClaimBoundsUnavailableListener(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := &CredentialsServerCmd{ConfigureGitHubHelper: true}
claimed, err := cmd.claimPort(ctx, ln.Addr().(*net.TCPAddr).Port)
require.Nil(t, claimed)
require.ErrorIs(t, err, errPortOwnedByAnotherSession)
require.NoError(
t,
ctx.Err(),
"an unavailable listener must not consume the caller's entire deadline",
)
}
40 changes: 34 additions & 6 deletions cmd/internal/agentcontainer/credentials_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package agentcontainer

import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
Expand All @@ -20,6 +21,7 @@ import (
"github.com/devsy-org/devsy/pkg/dockercredentials"
cliflags "github.com/devsy-org/devsy/pkg/flags"
"github.com/devsy-org/devsy/pkg/flags/names"
"github.com/devsy-org/devsy/pkg/ghcredentials"
"github.com/devsy-org/devsy/pkg/gitcredentials"
"github.com/devsy-org/devsy/pkg/gitsshsigning"
"github.com/devsy-org/devsy/pkg/log"
Expand All @@ -31,9 +33,11 @@ import (
type CredentialsServerCmd struct {
*flags.GlobalFlags

User string
User string
HostOwner string

ConfigureGitHelper bool
ConfigureGitHubHelper bool
ConfigureDockerHelper bool

ForwardPorts bool
Expand All @@ -60,6 +64,13 @@ func NewCredentialsServerCmd(flags *flags.GlobalFlags) *cobra.Command {
}
cliflags.Add(
credentialsServerCmd,
cliflags.String(&cmd.HostOwner, "host-owner", "manual", "Nonsecret host connection owner"),
cliflags.Bool(
&cmd.ConfigureGitHubHelper,
"configure-github-helper",
false,
"If true will configure GitHub CLI authentication forwarding",
),
cliflags.Bool(
&cmd.ConfigureGitHelper,
names.ConfigureGitHelper,
Expand Down Expand Up @@ -91,7 +102,7 @@ func (cmd *CredentialsServerCmd) Run(ctx context.Context, port int) error {
runCtx, cancel := context.WithCancel(ctx)
defer cancel()

ln, err := claimPort(port)
ln, err := cmd.claimPort(ctx, port)
if err != nil {
if errors.Is(err, errPortOwnedByAnotherSession) {
cmd.logPortOwnedByAnotherSession(ctx, port)
Expand All @@ -111,26 +122,43 @@ func (cmd *CredentialsServerCmd) Run(ctx context.Context, port int) error {
}

cmd.maybeForwardPorts(runCtx, tunnelClient)
githubState := "disabled"
if cmd.ConfigureGitHubHelper {
result, installErr := ghcredentials.Install(runCtx, port, cmd.User)
githubState = result.Status
if err := installErr; err != nil {
log.Warnf("GitHub CLI wrapper installation unavailable: %v", err)
}
}
status := credentials.Readiness{
Version: credentials.ReadinessVersion,
Owner: cmd.User,
HostOwner: cmd.HostOwner,
Instance: rand.Text(),
GitHubAllowed: cmd.ConfigureGitHubHelper,
GitHubState: githubState,
}
serverDone := make(chan error, 1)
go func() { serverDone <- credentials.RunPreparedCredentialsServer(runCtx, ln, tunnelClient, status) }()

if err := cmd.configureDockerHelper(port); err != nil {
return err
log.Warnf("Docker credentials setup unavailable: %v", err)
}

if err := configureGitUserLocally(runCtx, cmd.User, tunnelClient); err != nil {
log.Warnf("error configuring git user: %v", err)
return err
}

cleanupGitHelper, err := cmd.configureGitCredentialHelper(runCtx, port)
if err != nil {
return err
log.Warnf("Git credentials setup unavailable: %v", err)
}
defer cleanupGitHelper()

cleanupGitSigning := cmd.configureGitSigningKey()
defer cleanupGitSigning()

return credentials.RunCredentialsServerWithListener(runCtx, ln, tunnelClient, cmd.User)
return <-serverDone
}

// errPortOwnedByAnotherSession marks a bind failure as another session
Expand Down
Loading
Loading