Skip to content

[GitHub] Store accepted token scopes#11915

Open
DCjanus wants to merge 23 commits into
badges:masterfrom
DCjanus:internal-gh-token-scopes
Open

[GitHub] Store accepted token scopes#11915
DCjanus wants to merge 23 commits into
badges:masterfrom
DCjanus:internal-gh-token-scopes

Conversation

@DCjanus

@DCjanus DCjanus commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Why

GitHub token pool entries are currently persisted as token strings only. That makes it hard to support GitHub API features which need token-level metadata, such as knowing which OAuth scopes were granted to each accepted token.

This is a preparatory change for scope-aware GitHub API usage. It stores accepted token scopes without changing the scopes requested by the existing GitHub OAuth flow.

Related background:

What

  • Add a nullable scopes text[] column to github_user_tokens.
  • Load, store, and refresh token scopes through SQL persistence, GithubConstellation, and the GitHub API provider token pools.
  • Parse granted scopes from the GitHub OAuth token exchange response, including comma-separated values with spaces.
  • Allow TokenPool.add() to refresh explicitly provided metadata for an existing token without clearing metadata when no new data is provided.

Notes

  • scopes = NULL means the token scope metadata is unknown, which is the state for existing persisted tokens.
  • scopes = '{}' means the token was accepted with no granted scopes.
  • This does not request new OAuth scopes, backfill existing token scopes, select tokens by required scopes, or add any GHCR / GitHub Packages badge behavior.
  • TokenPool still uses the existing FIFO / priority queue scheduling model. A more complete design would refactor token management so token identity, persistent metadata, and runtime scheduling are handled separately. This PR intentionally avoids that larger refactor and only patches duplicate-token handling so in-process token metadata can stay in sync with persistence.

Validation

Manual validation beyond CI:

  • Ran the PostgreSQL migration up against a local PostgreSQL 16 database and confirmed github_user_tokens.scopes text[] was created.
  • Inserted NULL, empty, and populated scopes values and confirmed they round-trip with distinct semantics.
  • Simulated accepting a token with scopes: ['read:packages', 'read:user'], confirmed it persisted to PostgreSQL, then created a fresh GithubConstellation and confirmed the metadata loaded into the standard, search, and GraphQL token pools.
  • Ran the PostgreSQL migration down and confirmed the scopes column was removed.

DCjanus and others added 20 commits May 30, 2026 17:46
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
Assisted-by: Codex <gpt-5.5>
…copes

# Conflicts:
#	core/token-pooling/sql-token-persistence.integration.js
Assisted-by: Codex:gpt-5.5
Assisted-by: Codex:gpt-5.5
Assisted-by: Codex:gpt-5.5
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

📚 Remember to ensure any changes to config.private in services/github/auth/acceptor.js are reflected in the server secrets documentation.

⚠️

📚 Remember to ensure any changes to config.private in services/github/github-constellation.js are reflected in the server secrets documentation.

Messages
📖 ✨ Thanks for your contribution to Shields, @DCjanus!

Generated by 🚫 dangerJS against a43419e

@PyvesB

PyvesB commented Jun 7, 2026

Copy link
Copy Markdown
Member

Thanks for the contribution! We've got an ongoing token issue in #11912, let's put this aside for now. I don't think your work would have much impact, but I want to minimise the number of moving parts. :)

@DCjanus DCjanus marked this pull request as draft June 7, 2026 12:43
@DCjanus

DCjanus commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the contribution! We've got an ongoing token issue in #11912, let's put this aside for now. I don't think your work would have much impact, but I want to minimise the number of moving parts. :)

Moving this back to draft for now.

Feel free to set this aside while #11912 is being investigated. I can rebase or rewrite it later if needed.

@PyvesB

PyvesB commented Jun 27, 2026

Copy link
Copy Markdown
Member

Okay, I think it's a good point to resume this work. :)

@DCjanus DCjanus marked this pull request as ready for review June 27, 2026 19:10
@DCjanus

DCjanus commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

I noticed a couple of checks are still failing after this was brought back up to date.

I pushed an empty commit to trigger a fresh CI run, but the failures still look external:

  • danger failed while fetching PR files/commits from the GitHub API with ERR_STREAM_PREMATURE_CLOSE.
  • test-services failed in a live GitHub service test with a 429 calling https://api.github.com.

This seems likely related to the ongoing GitHub incident: https://www.githubstatus.com/incidents/v0b3bpsyvqtk

We may need to retry the failed checks later, once GitHub's API status has recovered.

@PyvesB

PyvesB commented Jun 27, 2026

Copy link
Copy Markdown
Member

The Danger failure will hopefully be fixed by #11957!

@PyvesB PyvesB left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks good, I think it will work as expected. A couple of simplifying thoughts inline.

Comment on lines +69 to +90
expect(tokens.map(({ token }) => token).sort()).to.deep.equal(
initialTokens,
)
expect(tokens.map(({ scopes }) => scopes)).to.deep.equal([
null,
null,
null,
])
})

it('loads token scopes', async function () {
const scopedToken = 'd'.repeat(40)
await pool.query(
`INSERT INTO pg_temp.${tableName} (token, scopes) VALUES ($1::text, $2::text[]);`,
[scopedToken, ['read:packages', 'read:user']],
)

const tokens = await persistence.initialize(pool)
expect(tokens).to.deep.include({
token: scopedToken,
scopes: ['read:packages', 'read:user'],
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could combine these two tests, and insert a mixture of tokens with and without scopes in beforeEach? My understanding is that this will more closely mirror what is effectively in the pool once this is running in production.

Comment on lines 325 to 327
if (!next.isValid) {
// Discard, and
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether we should be calling this.priorityQueue.deq() here. This side effect would be that we wouldn't have to call this._removeFromPriorityQueue(existingToken) and rebuild a priority queue when adding a token, we'd effectively lazily remove from priorityQueue on access.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants