diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b93dfa6..7cceb0e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,7 @@ jobs: outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} + version: ${{ steps.release.outputs.version }} steps: - uses: googleapis/release-please-action@v5 id: release @@ -70,10 +71,8 @@ jobs: SENTRY_URL: https://glitch.grounds.gg/ with: environment: production - # release-please tag_name is e.g. `v0.1.7`; sentry-go's - # Release field is set from internal/version.Version which - # ldflags-baked equals goreleaser's .Version (no `v`). Strip - # the prefix here so the marker matches what the binary - # reports at runtime. - version: ${{ needs.release-please.outputs.tag_name }} - version_prefix: v + # release-please's `version` output is the bare semver + # (e.g. 0.1.8); the binary's runtime release tag also has + # no `v` prefix. Use `version` not `tag_name` so the + # GlitchTip marker matches what the SDK reports at runtime. + version: ${{ needs.release-please.outputs.version }} diff --git a/internal/auth/device.go b/internal/auth/device.go index 3673fed..127a9e9 100644 --- a/internal/auth/device.go +++ b/internal/auth/device.go @@ -51,8 +51,13 @@ func (d *DeviceClient) StartDevice(ctx context.Context) (*DeviceCodeResponse, er return nil, fmt.Errorf("pkce: %w", err) } body := url.Values{ - "client_id": {d.ClientID}, - "scope": {"openid profile email"}, + "client_id": {d.ClientID}, + // `offline_access` upgrades the refresh_token to an "offline + // token" with a 30-day default TTL, decoupled from the SSO + // session-idle window (Keycloak default 30 minutes). Without + // it a user who runs `grounds login` once in the morning + // would have to re-authenticate after lunch. + "scope": {"openid profile email offline_access"}, "code_challenge": {challenge}, "code_challenge_method": {"S256"}, } diff --git a/internal/auth/device_test.go b/internal/auth/device_test.go index b82822b..adde605 100644 --- a/internal/auth/device_test.go +++ b/internal/auth/device_test.go @@ -11,7 +11,7 @@ import ( ) func TestStartDevice(t *testing.T) { - var gotChallenge, gotMethod string + var gotChallenge, gotMethod, gotScope string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/auth/device") { t.Fatalf("path = %s", r.URL.Path) @@ -19,6 +19,7 @@ func TestStartDevice(t *testing.T) { r.ParseForm() gotChallenge = r.Form.Get("code_challenge") gotMethod = r.Form.Get("code_challenge_method") + gotScope = r.Form.Get("scope") json.NewEncoder(w).Encode(DeviceCodeResponse{ DeviceCode: "dc", UserCode: "ABCD-EFGH", @@ -48,6 +49,12 @@ func TestStartDevice(t *testing.T) { if res.CodeVerifier == "" { t.Error("CodeVerifier should be populated for PollToken") } + // offline_access keeps the refresh token alive past the SSO + // session-idle window so the CLI doesn't ask for a re-login + // after a few minutes of inactivity. + if !strings.Contains(gotScope, "offline_access") { + t.Errorf("scope = %q, want it to contain offline_access", gotScope) + } } func TestPollToken_Success(t *testing.T) {