Skip to content

feat(accounts): offer a repository's immutable name in the trust form - #614

Merged
alukach merged 2 commits into
mainfrom
feat/github-immutable-repository-lookup
Oct 1, 2026
Merged

alukach merged 2 commits into
mainfrom
feat/github-immutable-repository-lookup

Conversation

@alukach

@alukach alukach commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

GitHub signs Actions tokens for repositories created after July 2026 (and any that opted in) with immutable subjects, repo:owner@id/repo@id:…, and a service account's trust has to match that form exactly. Nobody knows their repository's numeric ids, so today the trust form takes owner/repo, the exchange is refused, and the uniform refusal doesn't say why. This came up on the first real GitHub Actions run against staging: alukach/source-coop-upload-test is new, so its tokens say repo:alukach@897290/source-coop-upload-test@1400565438:ref:refs/heads/main. Part of #491.

What

GithubWorkflowFields, the fields behind both the create form and the "Trust a GitHub workflow" dialog:

  • Public repository. When the Repository field holds owner/repo, the form asks https://api.github.com/repos/{owner}/{repo} once typing stops (400 ms) and offers the immutable name, octocat@583231/Hello-World@1296269, with Use it, which puts it in the field. The names come from GitHub's answer, so their case is GitHub's too.
  • Private or missing repository. The anonymous API answers 404, so the form gives the command that prints the name for anyone who can see the repository: gh api repos/{owner}/{repo} --jq '"\(.owner.login)@\(.owner.id)/\(.name)@\(.id)"'. Run against the upload-test repository it prints alukach@897290/source-coop-upload-test@1400565438.
  • A value already in the immutable form isn't looked up.
  • "immutable subjects", in the Repository help and the private-repository line, links to GitHub's changelog, which says what the form is and which repositories get it: those created, renamed or transferred after July 15, 2026, plus any that opt in. The screenshots below predate the link, which adds only an underline.

Decisions to flag:

  • It offers, it doesn't switch. Whether a repository's tokens carry the immutable form is its actions/oidc/customization/sub setting, which only its admins can read, even on a public repository. The creation date would only be a guess, so the form names the July 2026 default and leaves the choice to the user.
  • Asked from the browser, not the server. The anonymous limit (60 requests an hour) is then the viewer's own rather than shared by every Vercel function, and the server needs no new code. The app sets no Content-Security-Policy, so nothing blocks the call. It does tell GitHub, from the viewer's IP, which repository name was typed, which is GitHub's own data.
  • No GitHub sign-in for private repositories. That would mean a GitHub OAuth app and a token held for the session. The gh one-liner covers it for anyone who can already see the repository.

Stories

On this branch's deploy:

PublicRepository: the Repository field holds octocat/Hello-World; under it, "Repositories created after July 2026 sign tokens with their ids. If this one does, it is octocat@583231/Hello-World@1296269" and a Use it button

PrivateRepository: the Repository field holds octocat/a-private-repository; under it, "GitHub doesn't show this repository publicly. If it's private and its tokens carry immutable subjects, this prints the name to use:" and the gh api command

Testing

  • GithubWorkflowFields.test.tsx (new, fetch mocked): a public repository's immutable name is offered and Use it sends it through onChange; a 404 shows the gh command; a value already immutable isn't looked up.
  • src/stories.smoke.test.tsx: 193 pass, the two new stories included.
  • npm run type-check and next lint on the changed files are clean.
  • Screenshots above are from a local Storybook against the real GitHub API. The gh command was run against alukach/source-coop-upload-test and prints its token's subject prefix.

Docs and ADRs

Checked ADR-014 (source-cooperative/data.source.coop): it says a trust names a subject exactly, in either form, and this changes only how the form helps fill one in, so it still holds. docs.source.coop: the automated-access guide (source-cooperative/docs.source.coop#37) has its GitHub Actions section marked "coming soon", so no existing page describes this field. That section should mention the immutable form when it's written.

🤖 Generated with Claude Code

GitHub signs tokens for repositories created after July 2026 with immutable subjects, repo:owner@id/repo@id, and a trust has to match that form exactly, which nobody can type from memory. When the Repository field holds owner/repo, the form asks GitHub's public API for the repository's ids once typing stops and offers the immutable name with a button that puts it in the field. A repository the API can't see, private or missing, gets the gh command that prints the name instead. Whether a repository's tokens actually carry the immutable form is a setting only its admins can read, so the form offers the name rather than switching to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
source-coop-ui Ready Ready Preview Oct 1, 2026 9:25pm UTC
source-cooperative Ready Ready Preview Oct 1, 2026 9:25pm UTC

Request Review

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 15s —— View job


✅ No blocking issues — safe to merge.

I read the diff for GithubWorkflowFields.tsx and its new test. I didn't run the tests or the type-check.

  • Stale answers: answer?.repository === repository discards a response for an earlier value, and the effect aborts the in-flight request and clears the timer on every change. Both are handled correctly.
  • Shell safety: the gh api command interpolates repository, but it is only rendered after the SHORT_REPOSITORY regex has matched. The regex allows only [A-Za-z0-9_.-], so the copied command can't contain shell metacharacters.
  • Non-404 failures: a rate-limit response (403/429) or a network error also sets immutable: null. The form then says "GitHub doesn't show this repository publicly", which is wrong for a public repository. Anonymous browser calls are limited to 60 an hour, so this is plausible. The gh fallback still works, so it isn't blocking. If you want it accurate, treat only 404 as null and leave other failures as undefined (no hint shown).
  • Dot-segment inputs: the regex accepts . and .. as segments, so a/.. is fetched. The browser normalizes the URL and GitHub returns an error, so it's harmless.

Simplify (ponytail)

Nothing worth cutting. The hook is small, the regex guards the shell command, and there is no new dependency.

Docs

The PR description covers this. It names ADR-014 in data.source.coop and says why it still holds, and it points to the "coming soon" GitHub Actions section of docs.source.coop#37. Nothing is missing.


💰 Estimated review cost: $0.13 · 0m15s · 4 turns

The trust form says a repository may need its immutable name without saying what that is or which repositories get it. GitHub's changelog says both: the owner@id/repo@id subject, the default for repositories created, renamed or transferred after July 15, 2026, and the opt-in for older ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach
alukach merged commit 06a832f into main Oct 1, 2026
8 checks passed
@alukach
alukach deleted the feat/github-immutable-repository-lookup branch October 1, 2026 22:57
alukach added a commit that referenced this pull request Oct 2, 2026
…615)

CLAUDE.md told agents to build Storybook preview links as
`source-coop-ui-git-<branch>-radiantearth.vercel.app`. Vercel caps a
hostname label at 63 characters and truncates long branch names with a
hash suffix, so for `feat/github-immutable-repository-lookup`
([#614](#614)) the
guessed host doesn't resolve; the real one is
`source-coop-ui-git-feat-github-immutable-re-82950d-radiantearth.vercel.app`.

The Vercel bot comment carries the real host, but only after the PR is
open — too late for the description written when it's created. So
CLAUDE.md now gives a shell snippet that computes the host from the
branch: `<project>-git-<slug>-radiantearth` when that fits in 63
characters, otherwise `<project>-git-<slug>` cut to 43 characters, then
the first six hex characters of `sha256("git-" + <raw branch> +
<project>)`, then `-radiantearth`. It also says to check the links
against the bot comment once the PR is open.

## Testing

Compared the snippet's output with the `source-coop-ui` host in the
Vercel bot comment on the last 40 PRs: 39 match. The one miss,
[#602](#602), is
from a fork, which Vercel names `fork-<owner>-<branch>`; the snippet
doesn't handle forks. Ran the snippet as written under bash and zsh for
a short branch and a long (truncated) one.

No UI, docs.source.coop or data-proxy ADR impact: this only changes
contributor guidance in this repo.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – source-cooperative — 78115e4f Deployed Oct 1, 2026 by vercel[bot]
Preview – source-coop-ui — 78115e4f Deployed Oct 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant