Skip to content

[EXP-23262]: S7: support subrepo cloning via GH_USER / GH_TOKEN#92

Open
oleksandrhleba wants to merge 1 commit into
mainfrom
task/EXP-23262-github-token-auth
Open

[EXP-23262]: S7: support subrepo cloning via GH_USER / GH_TOKEN#92
oleksandrhleba wants to merge 1 commit into
mainfrom
task/EXP-23262-github-token-auth

Conversation

@oleksandrhleba

@oleksandrhleba oleksandrhleba commented Jun 30, 2026

Copy link
Copy Markdown

Ticket link

EXP-23262

Summary

Adds native support in s7 for authenticating GitHub subrepo network operations over HTTPS with a Personal Access Token, instead of relying on an SSH key.

When S7_GIT_USER + S7_GIT_TOKEN (or the GH_USER / GH_TOKEN fallback) are present, s7 makes git rewrite SSH github.com URLs to HTTPS and supplies the PAT as an HTTP Authorization header. When they're absent, behavior is unchanged — SSH as before.

How it works

All git invocations funnel through +[GitRepository runGitWithArguments:…]. When both credentials are present, s7 injects three GIT_CONFIG_* entries into the child git process's environment:

insteadOf moves the subrepo URL onto HTTPS transport; the github.com‑scoped extraheader authenticates it — the same approach actions/checkout uses. This is git ≥ 2.31's env-based config; no git config file is touched.

Security properties

  • Token never in a URL — it rides in an HTTP header, so it can't leak through git error messages, GIT_TRACE_CURL, or remote.origin.url. base64 (not percent-encoded creds in a URL) also means arbitrary token bytes are safe.
  • Token never in argv — delivered via environment, not -c, so it can't appear in ps / /proc / crash reports.
  • Token never on disk — .git/config and .s7substate keep the original SSH URL; the rewrite is resolution-time only.
  • Header scoped to github.com — never attached to a redirect or other host.

Behavior details

  • No-op off the token path. With no credentials, task.environment is left untouched, so dev machines and SSH users are completely unaffected.
  • Composes with inherited config. New entries append past any existing GIT_CONFIG_COUNT, so a nested s7 (which inherits the parent's injected config) or any other tool's GIT_CONFIG_* is never clobbered.
  • Diagnostic. On the token path, s7 prints s7: subrepo network auth: HTTPS via token to stderr (once) so CI auth failures are easy to spot.

Testing

  • Unit tests (gitGitHubTokenAuthTests.m): the config builder produces the correct entries, appends past an existing count, and the raw token never appears outside the base64 header. Builds clean under -Werror.
  • Manually verified end-to-end against a real PAT on a clone of rd2 (SSH disabled via GIT_SSH_COMMAND=false to prove the HTTPS path).

Scope & follow-ups

  • No automated CI smoke against a live PAT yet — recommend adding a gated git ls-remote check.

@oleksandrhleba oleksandrhleba requested review from a-olkhovskyi and pastey and removed request for a-olkhovskyi June 30, 2026 16:09

@pastey pastey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm still not sure if we need GH_USER/GH_TOKEN in the s7 code. What are your thoughts?

Comment thread system7/git/Git.m

[self logSelectedAuthPathOnce];

// Inject the HTTPS auth config via the child's environment (see

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe a nitpicking, but anyway...
s7 spawns many git operations during its work. Now we will run this env calculation on every git call, even though it will produce exactly the same result we call it. We already have dispatch_once in some of the new methods, so probably would be ok to add "once" here too. It may affect the unit tests and require some refactoring.

Comment thread system7/git/Git.m
// Username for HTTPS subrepo auth. S7_GIT_TOKEN, falling back to GH_TOKEN.
+ (nullable NSString *)envGitAuthUser {
NSDictionary<NSString *, NSString *> *const env = NSProcessInfo.processInfo.environment;
NSString *const user = env[@"S7_GIT_USER"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please document the new env variables in the printHelp in main.m. From the client's standpoint.

Comment thread system7/git/Git.m
return traceEnabled;
}

// Username for HTTPS subrepo auth. S7_GIT_TOKEN, falling back to GH_TOKEN.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

comment seems to have been copy/pasted. Tells about the token. I would rather remove both comments. If we change the code, the comment will get outdated.

Comment thread system7/git/Git.m
+ (void)logSelectedAuthPathOnce {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if ([self envGitHubTokenAuthEnabled]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

envGitHubTokenAuthEnabled method seems to be an overkill for a single time usage.
const BOOL envGitHubTokenAuthEnabled = [self envGitAuthUser].length > 0 && [self envGitAuthToken].length > 0; would be just fine.

Comment thread system7/git/Git.m
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if ([self envGitHubTokenAuthEnabled]) {
fprintf(stderr, "s7: subrepo network auth: HTTPS via token\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please use logInfo instead.

Comment thread system7/git/Git.m
// GIT_CONFIG_COUNT) doesn't clobber it.
+ (NSDictionary<NSString *, NSString *> *)gitHubTokenConfigEnvironmentForUser:(nullable NSString *)user
token:(nullable NSString *)token
existingConfigCount:(NSInteger)existingConfigCount

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

instead of passing existingConfigCount argument, I would directly access the

NSDictionary<NSString *, NSString *> *const env = NSProcessInfo.processInfo.environment;
        const NSInteger existingConfigCount = MAX(0, [env[@"GIT_CONFIG_COUNT"] integerValue]);

here. This way all the logic managing GIT_CONFIG_COUNT would be in a single place. Now it's spread out by two methods.
Plus NSInteger seems odd. Why not unsigned?

Comment thread system7/git/Git.m
Comment on lines +1560 to +1561
NSString *const userColonToken = [NSString stringWithFormat:@"%@:%@", user, token];
NSString *const basic = [[userColonToken dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we move this down to 1576, where it's actually used?

Comment thread system7/git/Git.m
Comment on lines +1572 to +1574
result[[NSString stringWithFormat:@"GIT_CONFIG_KEY_%ld", (long)index]] = @"url.https://github.com/.insteadOf";
result[[NSString stringWithFormat:@"GIT_CONFIG_VALUE_%ld", (long)index]] = source;
++index;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd rather add a local closure that will take key/value, and incapsulate this logic. Something like:

__block nextConfigPairIndex = MAX(0, ...);
__auto_type addConfigKV = ^(NSString *key, NSString *value) {
          result[[NSString stringWithFormat:@"GIT_CONFIG_KEY_%ld", (long)nextConfigPairIndex]] = key;
        result[[NSString stringWithFormat:@"GIT_CONFIG_VALUE_%ld", (long)nextConfigPairIndex]] = value;
        ++nextConfigPairIndex;
};

addConfigKV(url, authorization);

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants