Repository navigation
feat: add gcb repo fork for person-owned forks #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
5a23a11
feat: add gcb repo fork for person-owned forks
JounQin 741fe8f
fix: make fork retries idempotent and tighten repo fork inputs
JounQin c9f6637
fix: allow fork through the broker guard
JounQin bea9ecb
test: refuse a fork as the app
JounQin 6bef245
fix: validate fork hosts and exact existing-fork output
JounQin 308991e
test: pin claimRequest's host check
JounQin 60aee21
fix: run fork against the requested host
JounQin 4e524d7
docs: mention --host for repo fork
JounQin c60c2b8
fix: drop inherited tokens when the fork host is explicit
JounQin a0a7b2e
test: restore GH_TOKEN even when the opener throws
JounQin bc5e2c2
fix: check the broker allowlist before a host opener fork
JounQin 8a3951b
docs: note the opener fork config
JounQin 2601f73
fix: match forgotten repo case and report config errors
JounQin 008b8ae
fix: report an existing fork URL without a request host
JounQin 4576f33
fix: force the resolved host for hostless fork requests
JounQin 2f67148
docs: tighten the repo fork host default
JounQin f897583
fix: keep github.com tokens for accountless forks
JounQin 5ed4183
fix: let gh choose the inherited token class by host
JounQin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| 'gcb-core': minor | ||
| 'gcb-client': minor | ||
| 'gcb-host': minor | ||
| --- | ||
|
|
||
| Add `gcb repo fork [owner/name]`: create a fork under a person's account with `--as-user` (broker | ||
| authorized user or host opener). The app identity cannot own a fork, so it is refused. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| /** | ||
| * `repo` — repo-level actions that need a person. | ||
| * | ||
| * `repo fork` is the one operation an app cannot perform for itself: a fork must be owned by a user | ||
| * account, and an installation token has none. The command therefore requires `--as-user`; bare | ||
| * selects the broker's authorized person, a login selects the host opener. | ||
| */ | ||
| import { LOGIN, REPOSITORY, fail, parseArgs, say } from 'gcb-core'; | ||
|
|
||
| import { requestOverSocket, resolveSocketPath } from '../helper.ts'; | ||
| import { remoteRepo } from './remotes.ts'; | ||
| import { submitToOpener } from './host-submit.ts'; | ||
|
|
||
| const KNOWN_FLAGS = new Set([ | ||
| 'help', 'h', 'as-user', 'proxy', 'no-proxy', 'socket', 'host', | ||
| ]); | ||
|
|
||
| export const REPO_USAGE = `Usage: gcb repo fork [owner/name] [options] | ||
|
|
||
| Create a fork under a person's account. The app identity cannot own a fork, so --as-user is | ||
| required. Without an owner/name the source comes from the checkout's origin remote. | ||
|
|
||
| --as-user Use the broker's authorized user token | ||
| --as-user <login> Use the host opener with ~/.config/gh-<login> | ||
| --host <host> Host as configured (default: the origin remote's host, else github.com) | ||
| --socket <path> Broker socket (default: GIT_BROKER_SOCKET) | ||
| --proxy <url> Use this proxy for this request | ||
| --no-proxy Reach GitHub directly for this request | ||
| -h, --help Show this help | ||
| `; | ||
|
|
||
| export async function runRepo(argv: readonly string[]): Promise<number> { | ||
| const args = parseArgs(argv, { booleans: ['help', 'no-proxy'] }); | ||
| if (args.has('help') || args.has('h')) { | ||
| say(REPO_USAGE); | ||
| return 0; | ||
| } | ||
| if (args.positionals[0] !== 'fork') fail('usage: gcb repo fork [owner/name]'); | ||
| if (args.positionals.length > 2) fail('unexpected argument ' + JSON.stringify(args.positionals[2])); | ||
| for (const name of args.names) { | ||
| if (!KNOWN_FLAGS.has(name)) fail('unknown option --' + name + ' for gcb repo fork'); | ||
| } | ||
|
|
||
| const accounts = args.values('as-user'); | ||
| const asUser = args.has('as-user'); | ||
| if (accounts.length > 1) fail('--as-user may only be given once'); | ||
| if (asUser && accounts.length > 0) fail('--as-user cannot be both bare and given a login'); | ||
| // `--as-user acme/widget` is an unambiguous source repository, not an account: a login cannot | ||
| // contain a slash. It saves making the positional depend on whether a bare flag was used. | ||
| let account = accounts[0]; | ||
| let sourceFromAsUser: string | undefined; | ||
| if (account !== undefined && REPOSITORY.test(account)) { | ||
| sourceFromAsUser = account; | ||
| account = undefined; | ||
| } | ||
| if (account !== undefined && !LOGIN.test(account)) { | ||
| fail(`--as-user must be a GitHub login, a repository, or no value, got ${JSON.stringify(account)}`); | ||
| } | ||
| if (account === undefined && sourceFromAsUser === undefined && !asUser) { | ||
| fail('--as-user is required for fork: the app identity cannot own a fork'); | ||
| } | ||
|
|
||
| const proxyValues = args.values('proxy'); | ||
| if (args.has('proxy') && proxyValues.length === 0) fail('--proxy needs a value'); | ||
| if (args.has('no-proxy') && (args.has('proxy') || proxyValues.length > 0)) { | ||
| fail('--proxy and --no-proxy are mutually exclusive'); | ||
| } | ||
| const proxy = args.has('no-proxy') ? null : args.value('proxy'); | ||
|
|
||
| const origin = remoteRepo('origin', process.cwd()); | ||
| const source = | ||
| sourceFromAsUser ?? args.positionals[1] ?? (origin === undefined ? undefined : `${origin.owner}/${origin.repo}`); | ||
| if (source === undefined) fail('pass owner/name, or run this from a checkout with an origin remote'); | ||
| if (!REPOSITORY.test(source)) fail(`not a repository: ${JSON.stringify(source)}`); | ||
| const hostValue = args.value('host'); | ||
| if ((args.has('host') || args.values('host').length > 0) && (hostValue === undefined || hostValue.trim() === '')) { | ||
| fail('--host needs a value'); | ||
| } | ||
| const host = hostValue ?? origin?.host ?? 'github.com'; | ||
|
|
||
| if (account !== undefined) { | ||
| return submitToOpener( | ||
| { kind: 'fork', repo: source, host, account, proxy, session: 'repo', pid: process.pid }, | ||
| (answer) => `forked ${source} as ${account}: ${answer.url ?? '(no url)'}`, | ||
| ); | ||
| } | ||
|
|
||
| const socketPath = resolveSocketPath(args.value('socket'), process.env); | ||
| if (socketPath === undefined) { | ||
| fail('no broker socket: pass --socket, set GIT_BROKER_SOCKET, or run `gcb setup`'); | ||
| } | ||
| let response; | ||
| try { | ||
| response = await requestOverSocket( | ||
| socketPath, | ||
| { | ||
| v: 1, | ||
| op: 'pull-request', | ||
| host, | ||
| path: source, | ||
| action: 'fork', | ||
| identity: 'user', | ||
| session: 'repo', | ||
| pid: process.pid, | ||
| proxy, | ||
| }, | ||
| 60_000, | ||
| ); | ||
| } catch (error) { | ||
| process.stderr.write(`unreachable: ${(error as Error).message}\n`); | ||
| return 3; | ||
| } | ||
| if (!response.ok) { | ||
| process.stderr.write(`refused [${response.code ?? 'unknown'}]: ${response.reason ?? 'no reason given'}\n`); | ||
| return 1; | ||
| } | ||
| if (response.prUrl === undefined) fail('the broker did not answer with a fork URL'); | ||
| say(`forked ${source}: ${response.prUrl}`); | ||
| return 0; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.