From 2e490bfe0105d584968f7609535e349d9197e98c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:05:29 +0000 Subject: [PATCH 01/21] Initial plan From f286832263a4560115cccec47d977a99ccdbcd9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:15:21 +0000 Subject: [PATCH 02/21] Add UTexas GitHub Enterprise Managed User login button Co-authored-by: saracarl <708566+saracarl@users.noreply.github.com> --- .env.example | 10 ++- README.md | 19 +++++ src/components/SignInGithub/SignInGitHub.css | 7 +- src/components/SignInGithub/SignInGitHub.tsx | 83 ++++++++++++-------- src/i18n/en/sign-in.json | 1 + src/pages/git-enterprise/index.ts | 49 ++++++++++++ 6 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 src/pages/git-enterprise/index.ts diff --git a/.env.example b/.env.example index 2137af67..948dcec4 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,12 @@ GIT_REPO_ADMIN_DATA="https://github.com/AVAnnotate/admin-data.git" GIT_REPO_ORG="AVAnnotate" GIT_ADMIN_REPO="admin" PUBLIC_GIT_REPO_PROJECT_TEMPLATE="project-template" -PUBLIC_REDIRECT_URL="http://localhost:4321" \ No newline at end of file +PUBLIC_REDIRECT_URL="http://localhost:4321" + +# UTexas GitHub Enterprise Managed User (EMU) OAuth credentials +# Create an OAuth App inside the UTexas enterprise at: +# https://github.com/enterprises/utexas-internal +# Set the Authorization callback URL to: {PUBLIC_REDIRECT_URL}/git-enterprise +# Leave blank to hide the UTexas EID login button. +PUBLIC_UTEXAS_GITHUB_CLIENT_ID= +UTEXAS_GITHUB_CLIENT_SECRET= \ No newline at end of file diff --git a/README.md b/README.md index 7d23575e..5e51415b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,25 @@ You can also run a local environment without linking to Netlify by running `npm Note that running the site locally still requires authentication via GitHub, and changes made to projects will be applied, so be sure to create testing projects for playing around with any new and untested features. +### UTexas GitHub Enterprise Managed User (EMU) login + +A second "Sign in with UTexas EID" button can be shown on the sign-in page to let UTexas users authenticate via the [GitHub Enterprise Managed Users SSO](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/configuring-authentication-for-enterprise-managed-users/configuring-saml-single-sign-on-for-enterprise-managed-users) for the `utexas-internal` enterprise (). + +To enable this button you need a separate OAuth App registered inside the UTexas enterprise and two additional environment variables: + +| Variable | Description | +| --- | --- | +| `PUBLIC_UTEXAS_GITHUB_CLIENT_ID` | Client ID of the OAuth App created inside the UTexas enterprise | +| `UTEXAS_GITHUB_CLIENT_SECRET` | Client secret for that OAuth App (server-side only) | + +**Creating the OAuth App:** +1. Navigate to (requires enterprise admin access). +2. Go to *Settings → OAuth Apps → New OAuth App*. +3. Set **Authorization callback URL** to `{PUBLIC_REDIRECT_URL}/git-enterprise` (e.g. `https://avannotate.netlify.app/git-enterprise`). +4. Copy the **Client ID** into `PUBLIC_UTEXAS_GITHUB_CLIENT_ID` and generate/copy a **Client secret** into `UTEXAS_GITHUB_CLIENT_SECRET`. + +When `PUBLIC_UTEXAS_GITHUB_CLIENT_ID` is blank (the default), the UTexas EID button is hidden, so the sign-in page works exactly as before for non-UTexas deployments. + # Astro Basics ```sh diff --git a/src/components/SignInGithub/SignInGitHub.css b/src/components/SignInGithub/SignInGitHub.css index d276d824..8c26ca82 100644 --- a/src/components/SignInGithub/SignInGitHub.css +++ b/src/components/SignInGithub/SignInGitHub.css @@ -18,7 +18,12 @@ border: 1px solid black; color: white !important; background-color: #015f86; - width: 2o0px; + width: 200px; +} + +/* UTexas burnt-orange accent for the Enterprise Managed User button */ +.sign-in-anchor-utexas { + background-color: #bf5700 !important; } .authorize-anchor { diff --git a/src/components/SignInGithub/SignInGitHub.tsx b/src/components/SignInGithub/SignInGitHub.tsx index f6b75df7..78564274 100644 --- a/src/components/SignInGithub/SignInGitHub.tsx +++ b/src/components/SignInGithub/SignInGitHub.tsx @@ -6,24 +6,41 @@ import type { Translations } from '@ty/Types.ts'; interface SignInGitHubProps { i18n: Translations; } -// href= + +// GitHub logo SVG shared by all sign-in buttons. +const GitHubLogo = () => ( + + + +); export const SignInGitHub = (props: SignInGitHubProps) => { + // Show the loading overlay whenever any sign-in link is clicked. useEffect(() => { - const fireEvent = document.getElementById('sign-in'); - - if (fireEvent) { - fireEvent.addEventListener('click', () => { - const elOverlay = document.getElementById('page-loader-overlay'); - const elSpinner = document.getElementById('page-loader-spinner'); + const showLoader = () => { + const elOverlay = document.getElementById('page-loader-overlay'); + const elSpinner = document.getElementById('page-loader-spinner'); + if (elOverlay && elSpinner) { + elOverlay.classList.add('loading-state'); + elSpinner.classList.add('loading'); + } + }; - if (elOverlay && elSpinner) { - elOverlay.classList.add('loading-state'); - elSpinner.classList.add('loading'); - } - }); - } + ['sign-in', 'sign-in-utexas'].forEach((id) => { + document.getElementById(id)?.addEventListener('click', showLoader); + }); }, []); + + // Only show the UTexas EID button when the enterprise OAuth app is configured. + const utexasClientId = import.meta.env.PUBLIC_UTEXAS_GITHUB_CLIENT_ID; + return (
{ }/git&scope=repo%20workflow`} >
- - - +
{props.i18n.t['Sign in with GitHub']}
+ {utexasClientId && ( + +
+ +
+ {props.i18n.t['Sign in with UTexas EID']} +
+
+
+ )}
- - - +
{props.i18n.t['Reauthorize App and Organizations']}
diff --git a/src/i18n/en/sign-in.json b/src/i18n/en/sign-in.json index 4ea7b1b1..e0231476 100644 --- a/src/i18n/en/sign-in.json +++ b/src/i18n/en/sign-in.json @@ -1,5 +1,6 @@ { "Sign in with GitHub": "Sign in with GitHub", + "Sign in with UTexas EID": "Sign in with UTexas EID", "Welcome To": "Welcome To", "Reauthorize App and Organizations": "Reauthorize App and Organizations" } \ No newline at end of file diff --git a/src/pages/git-enterprise/index.ts b/src/pages/git-enterprise/index.ts new file mode 100644 index 00000000..313094b0 --- /dev/null +++ b/src/pages/git-enterprise/index.ts @@ -0,0 +1,49 @@ +import type { APIRoute } from 'astro'; + +// OAuth callback for UTexas GitHub Enterprise Managed User (EMU) login. +// GitHub redirects here after the user authorises the enterprise OAuth app at +// https://github.com/enterprises/utexas-internal. +// Required environment variables: +// PUBLIC_UTEXAS_GITHUB_CLIENT_ID – Client ID of the OAuth App registered +// inside the UTexas enterprise. +// UTEXAS_GITHUB_CLIENT_SECRET – Corresponding client secret (server-side +// only; never exposed to the browser). +export const GET: APIRoute = async ({ request }) => { + const code = new URL(request.url).searchParams.get('code'); + const data = new FormData(); + data.append('client_id', import.meta.env.PUBLIC_UTEXAS_GITHUB_CLIENT_ID); + data.append('client_secret', import.meta.env.UTEXAS_GITHUB_CLIENT_SECRET); + data.append('code', code ?? ''); + + // Exchange the authorisation code for an access token using the standard + // GitHub OAuth endpoint (same endpoint used for both regular and EMU apps). + return await fetch(`https://github.com/login/oauth/access_token`, { + method: 'POST', + body: data, + }) + .then((response) => response.text()) + .then((paramsString) => { + const params = new URLSearchParams(paramsString); + const access_token = params.get('access_token'); + if (!access_token) { + console.error( + 'UTexas OAuth token exchange failed: no access_token in response', + paramsString + ); + return new Response(undefined, { status: 401 }); + } + return new Response(undefined, { + status: 302, + headers: { + 'Set-Cookie': `access-token=${access_token}; HttpOnly; SameSite=Lax; Path=/`, + Location: `${import.meta.env.PUBLIC_REDIRECT_URL}/en/projects`, + }, + }); + }) + .catch((error) => { + console.error('UTexas OAuth token exchange failed:', error); + return new Response(undefined, { + status: 500, + }); + }); +}; From 995c94dd3f94af984fe7582eccc93ddf76e47eac Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Fri, 19 Jun 2026 13:34:48 -0500 Subject: [PATCH 03/21] Update README with OAuth App creation instructions Clarify instructions for creating the OAuth App and add details about the settings location. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e51415b..4c5a4d4a 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ To enable this button you need a separate OAuth App registered inside the UTexas | `PUBLIC_UTEXAS_GITHUB_CLIENT_ID` | Client ID of the OAuth App created inside the UTexas enterprise | | `UTEXAS_GITHUB_CLIENT_SECRET` | Client secret for that OAuth App (server-side only) | -**Creating the OAuth App:** +**Creating the OAuth App:** 1. Navigate to (requires enterprise admin access). -2. Go to *Settings → OAuth Apps → New OAuth App*. +2. Go to *Settings → OAuth Apps → New OAuth App*. (this is actually under the developer section at the very bottom of your org's settings on the left.) 3. Set **Authorization callback URL** to `{PUBLIC_REDIRECT_URL}/git-enterprise` (e.g. `https://avannotate.netlify.app/git-enterprise`). 4. Copy the **Client ID** into `PUBLIC_UTEXAS_GITHUB_CLIENT_ID` and generate/copy a **Client secret** into `UTEXAS_GITHUB_CLIENT_SECRET`. From 5d03eb87e2d10577abc45b7c8e3f92e7e1342ddd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:52:16 +0000 Subject: [PATCH 04/21] fix: use window.location.origin for UTexas OAuth redirect_uri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build-time PUBLIC_REDIRECT_URL was always the canonical staging URL, so preview deployments (e.g. deploy-preview-438--…) sent a redirect_uri that wasn't registered in the OAuth App, producing GitHub's "Be careful!" warning. - SignInGitHub.tsx: replace PUBLIC_REDIRECT_URL with window.location.origin (safe because the component is client:only="react") - git-enterprise/index.ts: replace PUBLIC_REDIRECT_URL with new URL(request.url).origin for the post-auth redirect so the user lands back on the same deployment URL after sign-in --- src/components/SignInGithub/SignInGitHub.tsx | 2 +- src/pages/git-enterprise/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SignInGithub/SignInGitHub.tsx b/src/components/SignInGithub/SignInGitHub.tsx index 78564274..73d1d3fa 100644 --- a/src/components/SignInGithub/SignInGitHub.tsx +++ b/src/components/SignInGithub/SignInGitHub.tsx @@ -64,7 +64,7 @@ export const SignInGitHub = (props: SignInGitHubProps) => { id='sign-in-utexas' className='sign-in-anchor sign-in-anchor-utexas' href={`https://github.com/login/oauth/authorize?client_id=${utexasClientId}&redirect_uri=${ - import.meta.env.PUBLIC_REDIRECT_URL + window.location.origin }/git-enterprise&scope=repo%20workflow`} >
diff --git a/src/pages/git-enterprise/index.ts b/src/pages/git-enterprise/index.ts index 313094b0..67e6da0d 100644 --- a/src/pages/git-enterprise/index.ts +++ b/src/pages/git-enterprise/index.ts @@ -36,7 +36,7 @@ export const GET: APIRoute = async ({ request }) => { status: 302, headers: { 'Set-Cookie': `access-token=${access_token}; HttpOnly; SameSite=Lax; Path=/`, - Location: `${import.meta.env.PUBLIC_REDIRECT_URL}/en/projects`, + Location: `${new URL(request.url).origin}/en/projects`, }, }); }) From 32ac1fb538f92aeb7b9299938890ac3dfd873954 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:19:48 +0000 Subject: [PATCH 05/21] fix: use enterprise template for UTexas EMU project creation GitHub Enterprise Managed Users cannot access repos outside their enterprise, so the standard AVAnnotate/project-template generate call fails with their token. - git-enterprise/index.ts: set auth-provider=utexas cookie on EMU login so the server can identify the session later - GitHub/index.ts: add optional templateOwner parameter to createRepositoryFromTemplate (defaults to GIT_REPO_ORG as before) - api/projects/[projectName]/index.ts: when auth-provider=utexas and UTEXAS_GIT_REPO_ORG is configured, use the enterprise copy of the template - .env.example / README.md: document UTEXAS_GIT_REPO_ORG and UTEXAS_GIT_REPO_PROJECT_TEMPLATE plus step-by-step fork instructions --- .env.example | 14 ++++++++++++- README.md | 17 ++++++++++++++++ src/lib/GitHub/index.ts | 8 ++++---- src/pages/api/projects/[projectName]/index.ts | 19 ++++++++++++++++-- src/pages/git-enterprise/index.ts | 20 +++++++++++++------ 5 files changed, 65 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 948dcec4..7530165e 100644 --- a/.env.example +++ b/.env.example @@ -15,4 +15,16 @@ PUBLIC_REDIRECT_URL="http://localhost:4321" # Set the Authorization callback URL to: {PUBLIC_REDIRECT_URL}/git-enterprise # Leave blank to hide the UTexas EID login button. PUBLIC_UTEXAS_GITHUB_CLIENT_ID= -UTEXAS_GITHUB_CLIENT_SECRET= \ No newline at end of file +UTEXAS_GITHUB_CLIENT_SECRET= + +# UTexas enterprise project template +# GitHub Enterprise Managed Users cannot access templates outside their enterprise. +# Fork AVAnnotate/project-template into a UTexas enterprise org, mark it as a +# template repository, then set these two variables so AVAnnotate can use it. +# +# UTEXAS_GIT_REPO_ORG – the GitHub org (inside the enterprise) that owns +# the forked template (e.g. "utexas-avannotate") +# UTEXAS_GIT_REPO_PROJECT_TEMPLATE – the repo name of the fork; defaults to +# "project-template" if left blank +UTEXAS_GIT_REPO_ORG= +UTEXAS_GIT_REPO_PROJECT_TEMPLATE= \ No newline at end of file diff --git a/README.md b/README.md index 4c5a4d4a..0f40dc17 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,23 @@ To enable this button you need a separate OAuth App registered inside the UTexas When `PUBLIC_UTEXAS_GITHUB_CLIENT_ID` is blank (the default), the UTexas EID button is hidden, so the sign-in page works exactly as before for non-UTexas deployments. +#### Creating projects as a UTexas EMU user + +GitHub Enterprise Managed Users are isolated to their enterprise and cannot access GitHub repositories outside it — including the AVAnnotate project template (`AVAnnotate/project-template`). Without extra setup, UTexas users will see a "repo create failed" error when they try to create a new project. + +To fix this, fork the template into the UTexas enterprise and tell AVAnnotate where to find it: + +1. **Fork the template** — inside the UTexas enterprise, fork or import [`AVAnnotate/project-template`](https://github.com/AVAnnotate/project-template) into a UTexas enterprise org (e.g. `utexas-avannotate/project-template`). +2. **Mark it as a template** — in the forked repo's *Settings → General*, check **"Template repository"**. +3. **Set the two new environment variables**: + +| Variable | Description | +| --- | --- | +| `UTEXAS_GIT_REPO_ORG` | GitHub org (inside the enterprise) that owns the forked template, e.g. `utexas-avannotate` | +| `UTEXAS_GIT_REPO_PROJECT_TEMPLATE` | Repo name of the fork; leave blank to use `project-template` | + +With these variables set, UTexas users' project creation calls will use the enterprise copy of the template instead of the one outside the enterprise. + # Astro Basics ```sh diff --git a/src/lib/GitHub/index.ts b/src/lib/GitHub/index.ts index 06578a0d..157c2f9a 100644 --- a/src/lib/GitHub/index.ts +++ b/src/lib/GitHub/index.ts @@ -153,8 +153,10 @@ export const createRepositoryFromTemplate = async ( token: string, newRepoName: string, description: string, - visibility?: 'private' | 'public' // Defaults to private + visibility?: 'private' | 'public', // Defaults to private + templateOwner?: string // Defaults to GIT_REPO_ORG ): Promise => { + const owner = templateOwner || import.meta.env.GIT_REPO_ORG; const body = { owner: org, name: newRepoName, @@ -163,9 +165,7 @@ export const createRepositoryFromTemplate = async ( }; return await fetch( - `https://api.github.com/repos/${ - import.meta.env.GIT_REPO_ORG - }/${templateRepo}/generate`, + `https://api.github.com/repos/${owner}/${templateRepo}/generate`, { method: 'POST', headers: { diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 795890c1..8d2ee57e 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -70,14 +70,29 @@ export const POST: APIRoute = async ({ } ); } + // For UTexas EMU users the standard AVAnnotate template is outside their + // enterprise and cannot be accessed with their token. If a UTexas-specific + // template org is configured and this session was started via the EMU login + // path, use that org's copy of the template instead. + const isUtexasSession = cookies.get('auth-provider')?.value === 'utexas'; + const utexasTemplateOrg = import.meta.env.UTEXAS_GIT_REPO_ORG; + const utexasTemplateRepo = + import.meta.env.UTEXAS_GIT_REPO_PROJECT_TEMPLATE || body.templateRepo; + + const templateOwner = + isUtexasSession && utexasTemplateOrg ? utexasTemplateOrg : undefined; + const templateRepo = + isUtexasSession && utexasTemplateOrg ? utexasTemplateRepo : body.templateRepo; + // Create the new repo from template const resp: Response = await createRepositoryFromTemplate( - body.templateRepo, + templateRepo, body.gitHubOrg, token?.value as string, projectName as string, body.title, - body.visibility + body.visibility, + templateOwner ); if (!resp.ok) { diff --git a/src/pages/git-enterprise/index.ts b/src/pages/git-enterprise/index.ts index 67e6da0d..b3f8d1c2 100644 --- a/src/pages/git-enterprise/index.ts +++ b/src/pages/git-enterprise/index.ts @@ -32,13 +32,21 @@ export const GET: APIRoute = async ({ request }) => { ); return new Response(undefined, { status: 401 }); } - return new Response(undefined, { - status: 302, - headers: { - 'Set-Cookie': `access-token=${access_token}; HttpOnly; SameSite=Lax; Path=/`, - Location: `${new URL(request.url).origin}/en/projects`, - }, + // Set both the access token and an auth-provider marker so the server + // can identify this session as a UTexas EMU login and use the correct + // template repo when creating projects. + const headers = new Headers({ + Location: `${new URL(request.url).origin}/en/projects`, }); + headers.append( + 'Set-Cookie', + `access-token=${access_token}; HttpOnly; SameSite=Lax; Path=/` + ); + headers.append( + 'Set-Cookie', + `auth-provider=utexas; HttpOnly; SameSite=Lax; Path=/` + ); + return new Response(undefined, { status: 302, headers }); }) .catch((error) => { console.error('UTexas OAuth token exchange failed:', error); From 94d5c4b0baea6b7686cf32bfbc5bea2cfcda028e Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Tue, 30 Jun 2026 10:37:33 -0500 Subject: [PATCH 06/21] Remove Astro Basics section from README Removed Astro Basics section and related content from README. --- README.md | 52 ---------------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/README.md b/README.md index 0f40dc17..3e207f95 100644 --- a/README.md +++ b/README.md @@ -46,57 +46,5 @@ To fix this, fork the template into the UTexas enterprise and tell AVAnnotate wh With these variables set, UTexas users' project creation calls will use the enterprise copy of the template instead of the one outside the enterprise. -# Astro Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## 🚀 Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -├── public/ -│ └── favicon.svg -├── src/ -│ ├── components/ -│ │ └── Card.astro -│ ├── layouts/ -│ │ └── Layout.astro -│ └── pages/ -│ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## 👀 Want to learn more? Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). From 97ab50917df023018e378ae754045c04ac97a0d9 Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Tue, 30 Jun 2026 10:49:50 -0500 Subject: [PATCH 07/21] Improve GitHub API error logging for project creation --- src/pages/api/projects/[projectName]/index.ts | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 795890c1..bd57bbfb 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -24,6 +24,41 @@ import { import { v4 as uuidv4 } from 'uuid'; import { updateProjectLastUpdated } from '@lib/pages/index.ts'; +const logGitHubErrorResponse = async ( + stage: string, + response: Response, + context?: Record +) => { + const requestId = + response.headers.get('x-github-request-id') || + response.headers.get('x-request-id') || + 'unknown'; + const rateLimitRemaining = response.headers.get('x-ratelimit-remaining'); + const scope = response.headers.get('x-oauth-scopes'); + const acceptedScope = response.headers.get('x-accepted-oauth-scopes'); + + let responseBody: unknown = null; + const rawBody = await response.text(); + if (rawBody) { + try { + responseBody = JSON.parse(rawBody); + } catch { + responseBody = rawBody; + } + } + + console.error(`[GitHub ${stage}] request failed`, { + status: response.status, + statusText: response.statusText, + requestId, + rateLimitRemaining, + scope, + acceptedScope, + responseBody, + ...context, + }); +}; + // Note: this POST route is the only /api/projects route that expects the // `projectName` param to be the bare name instead of the slug version that // combines org and name. All other /api/projects API routes expect the full slug. @@ -81,13 +116,17 @@ export const POST: APIRoute = async ({ ); if (!resp.ok) { - console.error('Failed to create project repo: ', resp.statusText); - console.error('Body: ', body); + await logGitHubErrorResponse('repo-create-from-template', resp, { + gitHubOrg: body.gitHubOrg, + templateRepo: body.templateRepo, + projectName, + visibility: body.visibility, + }); return new Response( JSON.stringify({ avaError: '_repo_create_failed_', }), - { status: 500, statusText: resp.statusText } + { status: resp.status || 500, statusText: resp.statusText } ); } @@ -102,14 +141,16 @@ export const POST: APIRoute = async ({ ); if (!respPages.ok) { - console.error('Status: ', respPages.status); - console.error('Failed to enable GitHub pages: ', respPages.statusText); + await logGitHubErrorResponse('pages-enable', respPages, { + gitHubOrg: body.gitHubOrg, + projectName, + }); return new Response( JSON.stringify({ avaError: '_failed_pages_enable_', }), { - status: 500, + status: respPages.status || 500, statusText: respPages.statusText, } ); @@ -127,14 +168,16 @@ export const POST: APIRoute = async ({ ); if (!respTopics.ok) { - console.error('Status: ', respTopics.status); - console.error('Failed to add topic: ', respTopics.statusText); + await logGitHubErrorResponse('topics-replace', respTopics, { + gitHubOrg: body.gitHubOrg, + projectName, + }); return new Response( JSON.stringify({ avaError: '_failed_adding_topic_', }), { - status: 500, + status: respTopics.status || 500, statusText: respTopics.statusText, } ); From e3e42c2ee3a7f9314fe8c25689d8956924db3ece Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Tue, 30 Jun 2026 11:51:35 -0500 Subject: [PATCH 08/21] Log template repository creation request --- src/lib/GitHub/index.ts | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/lib/GitHub/index.ts b/src/lib/GitHub/index.ts index 06578a0d..c0daf612 100644 --- a/src/lib/GitHub/index.ts +++ b/src/lib/GitHub/index.ts @@ -161,21 +161,31 @@ export const createRepositoryFromTemplate = async ( description: description, private: visibility !== 'public', }; + const url = `https://api.github.com/repos/${ + import.meta.env.GIT_REPO_ORG + }/${templateRepo}/generate`; + const headers = { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': '2022-11-28', + }; + const requestOptions = { + method: 'POST', + headers, + body: JSON.stringify(body), + }; - return await fetch( - `https://api.github.com/repos/${ - import.meta.env.GIT_REPO_ORG - }/${templateRepo}/generate`, - { - method: 'POST', - headers: { - Accept: 'application/vnd.github+json', - Authorization: `Bearer ${token}`, - 'X-GitHub-Api-Version': '2022-11-28', - }, - body: JSON.stringify(body), - } - ); + console.info('Creating repository from template', { + url, + method: requestOptions.method, + body, + headers: { + ...headers, + Authorization: 'Bearer [redacted]', + }, + }); + + return await fetch(url, requestOptions); }; export const addRepositoryHomepage = async ( From d91ae2d8c8374b3869833e76a72ccc1b5bf9cff1 Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Tue, 30 Jun 2026 13:47:34 -0500 Subject: [PATCH 09/21] Enterprise-aware GitHub project creation & internal visibility support ### Motivation - Support bimodal repository creation so public template generation remains unchanged while enterprise-managed orgs can use an enterprise template owner. - Allow creating and representing GitHub `internal` repos for enterprise orgs even though the template API only accepts a `private` boolean. - Improve GitHub failure observability by logging request ids and response bodies for template generation, visibility changes, and Pages operations. ### Description - Added `src/lib/GitHub/config.ts` with helpers to parse `PUBLIC_GITHUB_ENTERPRISE_ORGS`/`GITHUB_ENTERPRISE_ORGS` and to resolve `GIT_REPO_ORG_ENTERPRISE` vs `GIT_REPO_ORG` for template owner selection. - Updated `createRepositoryFromTemplate` in `src/lib/GitHub/index.ts` to pick the template owner via `getTemplateOwnerForDestinationOrg`, log the resolved generation URL and sanitized payload, and map `visibility` to the template API `private` boolean (treat `internal` as `private` initially). - Extended `changeRepoVisibility` to accept a `boolean | RepoVisibility` and to PATCH either `{ private: ... }` or `{ visibility: 'internal' }` so internal visibility can be applied after template creation. - Updated the front-end `NewProject` flow in `src/apps/NewProject/NewProject.tsx` to send `visibility: 'internal'` for non-private projects targeted at enterprise orgs and to keep `visibility: 'public'` for public orgs or `private` when requested. - Enhanced the API handler in `src/pages/api/projects/[projectName]/index.ts` to call the visibility helper after template creation when `body.visibility === 'internal'`, to add `logGitHubFailure` logging (including `x-github-request-id` and response body) on failures, and to persist `visibility` into the project config while retaining `is_private` for backwards compatibility. - Updated types in `src/types/api.ts` and `src/types/Types.ts` to allow `visibility: 'private' | 'public' | 'internal'` and to include `visibility` on the `Project` type. ### Testing - Ran the project build and typecheck with `npm run build` (which runs `astro check` and the client/server build), and the build completed successfully with no type errors. - Verified that `createRepositoryFromTemplate` composes and logs the resolved GitHub template URL and payload during local runs and that `changeRepoVisibility` can be called with `'internal'` to issue the appropriate PATCH request (failure cases are logged with request id and response body). --- src/apps/NewProject/NewProject.tsx | 2 +- src/lib/GitHub/config.ts | 24 ++++++++ src/lib/GitHub/index.ts | 55 ++++++++++++------- src/pages/api/projects/[projectName]/index.ts | 50 ++++++++++++++++- src/types/api.ts | 2 +- 5 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 src/lib/GitHub/config.ts diff --git a/src/apps/NewProject/NewProject.tsx b/src/apps/NewProject/NewProject.tsx index 1579dff8..cb54f784 100644 --- a/src/apps/NewProject/NewProject.tsx +++ b/src/apps/NewProject/NewProject.tsx @@ -44,7 +44,7 @@ export const NewProject = (props: NewProjectProps) => { additionalUsers: project.additional_users.map((u) => u.login_name), language: project.language, autoPopulateHomePage: project.auto_populate_home_page, - visibility: project.is_private ? 'private' : 'public', + is_private: !!project.is_private, generate_pages_site: !!project.generate_pages_site, tags: project.tags, }; diff --git a/src/lib/GitHub/config.ts b/src/lib/GitHub/config.ts new file mode 100644 index 00000000..eec9b6c7 --- /dev/null +++ b/src/lib/GitHub/config.ts @@ -0,0 +1,24 @@ +export type RepoVisibility = 'private' | 'public' | 'internal'; + +export const getEnterpriseGitHubOrgs = (): string[] => { + const enterpriseOrgs = + import.meta.env.PUBLIC_GITHUB_ENTERPRISE_ORGS || + import.meta.env.GITHUB_ENTERPRISE_ORGS || + ''; + + return enterpriseOrgs + .split(',') + .map((org: string) => org.trim().toLowerCase()) + .filter(Boolean); +}; + +export const isEnterpriseGitHubOrg = (org: string): boolean => + getEnterpriseGitHubOrgs().includes(org.trim().toLowerCase()); + +export const getTemplateOwnerForDestinationOrg = (org: string): string => { + if (isEnterpriseGitHubOrg(org) && import.meta.env.GIT_REPO_ORG_ENTERPRISE) { + return import.meta.env.GIT_REPO_ORG_ENTERPRISE; + } + + return import.meta.env.GIT_REPO_ORG; +}; diff --git a/src/lib/GitHub/index.ts b/src/lib/GitHub/index.ts index 06578a0d..fb7b6a76 100644 --- a/src/lib/GitHub/index.ts +++ b/src/lib/GitHub/index.ts @@ -1,3 +1,17 @@ +import { + getEnterpriseGitHubOrgs, + getTemplateOwnerForDestinationOrg, + isEnterpriseGitHubOrg, +} from './config.ts'; +import type { RepoVisibility } from './config.ts'; + +export { + getEnterpriseGitHubOrgs, + getTemplateOwnerForDestinationOrg, + isEnterpriseGitHubOrg, +}; +export type { RepoVisibility } from './config.ts'; + export const paginate = async (url: string, token: string) => { let results: any[] = []; @@ -153,7 +167,7 @@ export const createRepositoryFromTemplate = async ( token: string, newRepoName: string, description: string, - visibility?: 'private' | 'public' // Defaults to private + visibility?: RepoVisibility // Defaults to private ): Promise => { const body = { owner: org, @@ -161,21 +175,21 @@ export const createRepositoryFromTemplate = async ( description: description, private: visibility !== 'public', }; + const templateOwner = getTemplateOwnerForDestinationOrg(org); + const url = `https://api.github.com/repos/${templateOwner}/${templateRepo}/generate`; - return await fetch( - `https://api.github.com/repos/${ - import.meta.env.GIT_REPO_ORG - }/${templateRepo}/generate`, - { - method: 'POST', - headers: { - Accept: 'application/vnd.github+json', - Authorization: `Bearer ${token}`, - 'X-GitHub-Api-Version': '2022-11-28', - }, - body: JSON.stringify(body), - } - ); + console.info('GitHub template generation URL:', url); + console.info('GitHub template generation payload:', body); + + return await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + body: JSON.stringify(body), + }); }; export const addRepositoryHomepage = async ( @@ -372,8 +386,13 @@ export const changeRepoVisibility = async ( token: string, org: string, slug: string, - isPrivate: boolean + visibility: boolean | RepoVisibility ): Promise => { + const body = + typeof visibility === 'boolean' + ? { private: visibility } + : { visibility: visibility }; + return await fetch(`https://api.github.com/repos/${org}/${slug}`, { method: 'PATCH', headers: { @@ -381,9 +400,7 @@ export const changeRepoVisibility = async ( Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': '2022-11-28', }, - body: JSON.stringify({ - private: isPrivate, - }), + body: JSON.stringify(body), }); }; diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 795890c1..12e7a932 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -7,6 +7,7 @@ import { changeRepoVisibility, disablePages, removeRepositoryHomepage, + isEnterpriseGitHubOrg, } from '@lib/GitHub/index.ts'; import type { APIRoute } from 'astro'; import type { apiProjectPut, apiProjectsProjectNamePost } from '@ty/api.ts'; @@ -24,6 +25,19 @@ import { import { v4 as uuidv4 } from 'uuid'; import { updateProjectLastUpdated } from '@lib/pages/index.ts'; + +const logGitHubFailure = async (stage: string, response: Response) => { + const requestId = response.headers.get('x-github-request-id'); + const responseBody = await response.clone().text(); + + console.error(`GitHub ${stage} failed`, { + status: response.status, + statusText: response.statusText, + requestId, + responseBody, + }); +}; + // Note: this POST route is the only /api/projects route that expects the // `projectName` param to be the bare name instead of the slug version that // combines org and name. All other /api/projects API routes expect the full slug. @@ -51,6 +65,12 @@ export const POST: APIRoute = async ({ const body: apiProjectsProjectNamePost = await request.json(); + const repoVisibility = body.is_private + ? 'private' + : isEnterpriseGitHubOrg(body.gitHubOrg) + ? 'internal' + : 'public'; + // First see if we can create this repo const check: Response = await getRepo( token?.value as string, @@ -77,10 +97,11 @@ export const POST: APIRoute = async ({ token?.value as string, projectName as string, body.title, - body.visibility + repoVisibility ); if (!resp.ok) { + await logGitHubFailure('repo-template-generate', resp); console.error('Failed to create project repo: ', resp.statusText); console.error('Body: ', body); return new Response( @@ -93,6 +114,28 @@ export const POST: APIRoute = async ({ const repo: FullRepository = await resp.json(); + if (repoVisibility === 'internal') { + const visibilityResp = await changeRepoVisibility( + token?.value as string, + body.gitHubOrg, + projectName as string, + 'internal' + ); + + if (!visibilityResp.ok) { + await logGitHubFailure('repo-visibility-internal', visibilityResp); + return new Response( + JSON.stringify({ + avaError: '_repo_create_failed_', + }), + { + status: 500, + statusText: visibilityResp.statusText, + } + ); + } + } + if (body.generate_pages_site) { // Enable pages const respPages: Response = await enablePages( @@ -102,6 +145,7 @@ export const POST: APIRoute = async ({ ); if (!respPages.ok) { + await logGitHubFailure('pages-enable', respPages); console.error('Status: ', respPages.status); console.error('Failed to enable GitHub pages: ', respPages.statusText); return new Response( @@ -187,7 +231,8 @@ export const POST: APIRoute = async ({ users: collabs, project: { github_org: body.gitHubOrg, - is_private: body.visibility === 'private', + // Internal GitHub repos are represented as non-private in project metadata. + is_private: body.is_private, title: body.title, description: body.description, language: body.language, @@ -425,6 +470,7 @@ export const PUT: APIRoute = async ({ cookies, params, request, redirect }) => { ); if (!respPages.ok) { + await logGitHubFailure('pages-enable', respPages); console.error('Status: ', respPages.status); console.error('Failed to enable GitHub pages: ', respPages.statusText); return new Response( diff --git a/src/types/api.ts b/src/types/api.ts index 4b0c5e5a..68db4e81 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -32,7 +32,7 @@ export type apiAnnotationSetPost = { export type apiProjectsProjectNamePost = { templateRepo: string; description: string; - visibility: 'private' | 'public'; + is_private: boolean; generate_pages_site: boolean; title: string; slug: string; From 72d62913459b9c45fa70f1da78183a55bc2a9770 Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Mon, 6 Jul 2026 14:00:15 -0500 Subject: [PATCH 10/21] Replace visibility with repoVisibility in API call --- src/pages/api/projects/[projectName]/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 11429bb7..573c8f28 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -133,7 +133,7 @@ export const POST: APIRoute = async ({ token?.value as string, projectName as string, body.title, - body.visibility, + repoVisibility templateOwner ); @@ -142,7 +142,7 @@ export const POST: APIRoute = async ({ gitHubOrg: body.gitHubOrg, templateRepo: body.templateRepo, projectName, - visibility: body.visibility, + visibility: repoVisibility, }); return new Response( JSON.stringify({ From b98921f60a99ed186742125e7569d439944d881d Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Mon, 6 Jul 2026 14:01:45 -0500 Subject: [PATCH 11/21] Fix formatting issue in API project index file --- src/pages/api/projects/[projectName]/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 573c8f28..f345fadf 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -133,7 +133,7 @@ export const POST: APIRoute = async ({ token?.value as string, projectName as string, body.title, - repoVisibility + repoVisibility, templateOwner ); From 4704dd1d76fcb5aa386f2b3db6219040107a29f6 Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Mon, 6 Jul 2026 14:17:16 -0500 Subject: [PATCH 12/21] Add 'internal' visibility option for repository creation --- src/lib/GitHub/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/GitHub/index.ts b/src/lib/GitHub/index.ts index 51ad5f86..35803010 100644 --- a/src/lib/GitHub/index.ts +++ b/src/lib/GitHub/index.ts @@ -167,7 +167,7 @@ export const createRepositoryFromTemplate = async ( token: string, newRepoName: string, description: string, - visibility?: 'private' | 'public', // Defaults to private + visibility?: 'private' | 'public' | 'internal', // Defaults to private templateOwner?: string // Defaults to GIT_REPO_ORG ): Promise => { const owner = templateOwner || import.meta.env.GIT_REPO_ORG; @@ -175,7 +175,7 @@ export const createRepositoryFromTemplate = async ( owner: org, name: newRepoName, description: description, - private: visibility !== 'public', + private: visibility === 'private', }; const url = `https://api.github.com/repos/${ import.meta.env.GIT_REPO_ORG From 7a82adafdab3f6b0939c52cdcfe5785b7c7bfa52 Mon Sep 17 00:00:00 2001 From: Sara Brumfield Date: Mon, 6 Jul 2026 14:24:04 -0500 Subject: [PATCH 13/21] Implement GitHub failure logging function Added a function to log GitHub API failures with detailed information. --- src/pages/api/projects/[projectName]/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index f345fadf..a47fab3a 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -25,6 +25,18 @@ import { import { v4 as uuidv4 } from 'uuid'; import { updateProjectLastUpdated } from '@lib/pages/index.ts'; +const logGitHubFailure = async (stage: string, response: Response) => { + const requestId = response.headers.get('x-github-request-id'); + const responseBody = await response.clone().text(); + + console.error(`GitHub ${stage} failed`, { + status: response.status, + statusText: response.statusText, + requestId, + responseBody, + }); +}; + const logGitHubErrorResponse = async ( stage: string, response: Response, From 7bfb961be56f27f3ed8bf29ace909973fb4764f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:28:12 +0000 Subject: [PATCH 14/21] Add publish include_slug_in_base flag --- src/backend/projectHelpers.ts | 4 ++++ src/backend/tagHelpers.ts | 10 ++++++++++ src/lib/GitHub/config.ts | 9 +++++++++ src/lib/GitHub/index.ts | 2 ++ .../[projectName]/events/import-manifest.ts | 4 ++++ src/pages/api/projects/[projectName]/index.ts | 13 +++++++++++++ src/types/Types.ts | 1 + 7 files changed, 43 insertions(+) diff --git a/src/backend/projectHelpers.ts b/src/backend/projectHelpers.ts index c6c2411e..d7f5bae5 100644 --- a/src/backend/projectHelpers.ts +++ b/src/backend/projectHelpers.ts @@ -11,6 +11,7 @@ import { getWorkflowContent, updateWorkflowContent, triggerWorkflow, + shouldIncludeSlugInBase, } from '@lib/GitHub/index.ts'; import { gitRepo, type GitRepoContext } from './gitRepo.ts'; import type { @@ -595,6 +596,9 @@ export const publishSite = async ( project.publish.publish_pages_app = !!options.publish_pages; project.publish.publish_static_site = !!options.publish_static; + project.publish.include_slug_in_base = shouldIncludeSlugInBase( + project.project.github_org + ); const res = await writeFile('/data/project.json', JSON.stringify(project)); diff --git a/src/backend/tagHelpers.ts b/src/backend/tagHelpers.ts index bd4a3684..1c9c6b92 100644 --- a/src/backend/tagHelpers.ts +++ b/src/backend/tagHelpers.ts @@ -12,6 +12,7 @@ import { getDirData, buildProjectData, } from '@backend/projectHelpers.ts'; +import { shouldIncludeSlugInBase } from '@lib/GitHub/index.ts'; import { updateProjectLastUpdated } from '@lib/pages/index.ts'; export const createTagGroup = async ( @@ -34,6 +35,9 @@ export const createTagGroup = async ( const project: ProjectData = JSON.parse(proj as string); + project.publish.include_slug_in_base = shouldIncludeSlugInBase( + project.project.github_org + ); project.project.tags?.tagGroups.push(group); project.project.updated_at = new Date().toISOString(); @@ -138,6 +142,9 @@ export const updateTagGroup = async ( } } + project.publish.include_slug_in_base = shouldIncludeSlugInBase( + project.project.github_org + ); project.project.updated_at = new Date().toISOString(); const result = await writeFile('/data/project.json', JSON.stringify(project)); @@ -488,6 +495,9 @@ export const deleteAll = async (htmlUrl: string, userInfo: UserInfo) => { const project: ProjectData = JSON.parse(proj as string); + project.publish.include_slug_in_base = shouldIncludeSlugInBase( + project.project.github_org + ); project.project.tags = { tagGroups: [], tags: [] }; await writeFile('/data/project.json', JSON.stringify(project)); diff --git a/src/lib/GitHub/config.ts b/src/lib/GitHub/config.ts index eec9b6c7..fe072590 100644 --- a/src/lib/GitHub/config.ts +++ b/src/lib/GitHub/config.ts @@ -15,6 +15,15 @@ export const getEnterpriseGitHubOrgs = (): string[] => { export const isEnterpriseGitHubOrg = (org: string): boolean => getEnterpriseGitHubOrgs().includes(org.trim().toLowerCase()); +export const shouldIncludeSlugInBase = ( + org: string, + authProvider?: string +): boolean => + !( + (authProvider ?? '').trim().toLowerCase() === 'utexas' || + isEnterpriseGitHubOrg(org) + ); + export const getTemplateOwnerForDestinationOrg = (org: string): string => { if (isEnterpriseGitHubOrg(org) && import.meta.env.GIT_REPO_ORG_ENTERPRISE) { return import.meta.env.GIT_REPO_ORG_ENTERPRISE; diff --git a/src/lib/GitHub/index.ts b/src/lib/GitHub/index.ts index 35803010..4265b142 100644 --- a/src/lib/GitHub/index.ts +++ b/src/lib/GitHub/index.ts @@ -2,6 +2,7 @@ import { getEnterpriseGitHubOrgs, getTemplateOwnerForDestinationOrg, isEnterpriseGitHubOrg, + shouldIncludeSlugInBase, } from './config.ts'; import type { RepoVisibility } from './config.ts'; @@ -9,6 +10,7 @@ export { getEnterpriseGitHubOrgs, getTemplateOwnerForDestinationOrg, isEnterpriseGitHubOrg, + shouldIncludeSlugInBase, }; export type { RepoVisibility } from './config.ts'; diff --git a/src/pages/api/projects/[projectName]/events/import-manifest.ts b/src/pages/api/projects/[projectName]/events/import-manifest.ts index 66aa362b..7d0458e9 100644 --- a/src/pages/api/projects/[projectName]/events/import-manifest.ts +++ b/src/pages/api/projects/[projectName]/events/import-manifest.ts @@ -4,6 +4,7 @@ import type { APIRoute } from 'astro'; import { importIIIFManifest } from '@lib/iiif/import.ts'; import { gitRepo } from '@backend/gitRepo.ts'; import { getRepositoryUrl } from '@backend/projectHelpers.ts'; +import { shouldIncludeSlugInBase } from '@lib/GitHub/index.ts'; import { initFs } from '@lib/memfs/index.ts'; import type { UserInfo, Event, ProjectData } from '@ty/Types.ts'; import { emptyParagraph } from '@lib/slate/index.tsx'; @@ -158,6 +159,9 @@ export const POST: APIRoute = async ({ }); project.project.updated_at = new Date().toISOString(); + project.publish.include_slug_in_base = shouldIncludeSlugInBase( + project.project.github_org + ); await writeFile('/data/project.json', JSON.stringify(project, null, 2)); } diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index a47fab3a..fde4e2c5 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -8,6 +8,7 @@ import { disablePages, removeRepositoryHomepage, isEnterpriseGitHubOrg, + shouldIncludeSlugInBase, } from '@lib/GitHub/index.ts'; import type { APIRoute } from 'astro'; import type { apiProjectPut, apiProjectsProjectNamePost } from '@ty/api.ts'; @@ -280,8 +281,13 @@ export const POST: APIRoute = async ({ const project = { publish: { publish_pages_app: body.generate_pages_site, + publish_static_site: false, publish_sha: '', publish_iso_date: '', + include_slug_in_base: shouldIncludeSlugInBase( + body.gitHubOrg, + cookies.get('auth-provider')?.value + ), }, users: collabs, project: { @@ -578,6 +584,13 @@ export const PUT: APIRoute = async ({ cookies, params, request, redirect }) => { // override existing properties with new ones from the request const newConfig: ProjectData = { ...projectConfig, + publish: { + ...projectConfig.publish, + include_slug_in_base: shouldIncludeSlugInBase( + projectConfig.project.github_org, + cookies.get('auth-provider')?.value + ), + }, project: { ...projectConfig.project, ...body, diff --git a/src/types/Types.ts b/src/types/Types.ts index 721db946..4e4e3814 100644 --- a/src/types/Types.ts +++ b/src/types/Types.ts @@ -71,6 +71,7 @@ export type Publish = { publish_static_site: boolean; publish_sha: string; publish_iso_date: string; + include_slug_in_base?: boolean; }; export type CaptionSet = { From cc6d9a8e29a9129a4d4abc2a50d2301dc505744c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:38:00 +0000 Subject: [PATCH 15/21] Preserve include_slug_in_base after project creation --- src/backend/projectHelpers.ts | 4 ---- src/backend/tagHelpers.ts | 10 ---------- .../projects/[projectName]/events/import-manifest.ts | 4 ---- src/pages/api/projects/[projectName]/index.ts | 4 ---- 4 files changed, 22 deletions(-) diff --git a/src/backend/projectHelpers.ts b/src/backend/projectHelpers.ts index d7f5bae5..c6c2411e 100644 --- a/src/backend/projectHelpers.ts +++ b/src/backend/projectHelpers.ts @@ -11,7 +11,6 @@ import { getWorkflowContent, updateWorkflowContent, triggerWorkflow, - shouldIncludeSlugInBase, } from '@lib/GitHub/index.ts'; import { gitRepo, type GitRepoContext } from './gitRepo.ts'; import type { @@ -596,9 +595,6 @@ export const publishSite = async ( project.publish.publish_pages_app = !!options.publish_pages; project.publish.publish_static_site = !!options.publish_static; - project.publish.include_slug_in_base = shouldIncludeSlugInBase( - project.project.github_org - ); const res = await writeFile('/data/project.json', JSON.stringify(project)); diff --git a/src/backend/tagHelpers.ts b/src/backend/tagHelpers.ts index 1c9c6b92..bd4a3684 100644 --- a/src/backend/tagHelpers.ts +++ b/src/backend/tagHelpers.ts @@ -12,7 +12,6 @@ import { getDirData, buildProjectData, } from '@backend/projectHelpers.ts'; -import { shouldIncludeSlugInBase } from '@lib/GitHub/index.ts'; import { updateProjectLastUpdated } from '@lib/pages/index.ts'; export const createTagGroup = async ( @@ -35,9 +34,6 @@ export const createTagGroup = async ( const project: ProjectData = JSON.parse(proj as string); - project.publish.include_slug_in_base = shouldIncludeSlugInBase( - project.project.github_org - ); project.project.tags?.tagGroups.push(group); project.project.updated_at = new Date().toISOString(); @@ -142,9 +138,6 @@ export const updateTagGroup = async ( } } - project.publish.include_slug_in_base = shouldIncludeSlugInBase( - project.project.github_org - ); project.project.updated_at = new Date().toISOString(); const result = await writeFile('/data/project.json', JSON.stringify(project)); @@ -495,9 +488,6 @@ export const deleteAll = async (htmlUrl: string, userInfo: UserInfo) => { const project: ProjectData = JSON.parse(proj as string); - project.publish.include_slug_in_base = shouldIncludeSlugInBase( - project.project.github_org - ); project.project.tags = { tagGroups: [], tags: [] }; await writeFile('/data/project.json', JSON.stringify(project)); diff --git a/src/pages/api/projects/[projectName]/events/import-manifest.ts b/src/pages/api/projects/[projectName]/events/import-manifest.ts index 7d0458e9..66aa362b 100644 --- a/src/pages/api/projects/[projectName]/events/import-manifest.ts +++ b/src/pages/api/projects/[projectName]/events/import-manifest.ts @@ -4,7 +4,6 @@ import type { APIRoute } from 'astro'; import { importIIIFManifest } from '@lib/iiif/import.ts'; import { gitRepo } from '@backend/gitRepo.ts'; import { getRepositoryUrl } from '@backend/projectHelpers.ts'; -import { shouldIncludeSlugInBase } from '@lib/GitHub/index.ts'; import { initFs } from '@lib/memfs/index.ts'; import type { UserInfo, Event, ProjectData } from '@ty/Types.ts'; import { emptyParagraph } from '@lib/slate/index.tsx'; @@ -159,9 +158,6 @@ export const POST: APIRoute = async ({ }); project.project.updated_at = new Date().toISOString(); - project.publish.include_slug_in_base = shouldIncludeSlugInBase( - project.project.github_org - ); await writeFile('/data/project.json', JSON.stringify(project, null, 2)); } diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index fde4e2c5..9ba21f97 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -586,10 +586,6 @@ export const PUT: APIRoute = async ({ cookies, params, request, redirect }) => { ...projectConfig, publish: { ...projectConfig.publish, - include_slug_in_base: shouldIncludeSlugInBase( - projectConfig.project.github_org, - cookies.get('auth-provider')?.value - ), }, project: { ...projectConfig.project, From 264eab721e6cc868e952afc6cc58850df5819e8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:02:52 +0000 Subject: [PATCH 16/21] fix: auto-enable GitHub Pages (Actions source) for EMU repo creation --- src/pages/api/projects/[projectName]/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 9ba21f97..a7cc98a1 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -100,9 +100,11 @@ export const POST: APIRoute = async ({ const body: apiProjectsProjectNamePost = await request.json(); + const isUtexasSession = cookies.get('auth-provider')?.value === 'utexas'; + const repoVisibility = body.is_private ? 'private' - : isEnterpriseGitHubOrg(body.gitHubOrg) + : isEnterpriseGitHubOrg(body.gitHubOrg) || isUtexasSession ? 'internal' : 'public'; @@ -129,7 +131,6 @@ export const POST: APIRoute = async ({ // enterprise and cannot be accessed with their token. If a UTexas-specific // template org is configured and this session was started via the EMU login // path, use that org's copy of the template instead. - const isUtexasSession = cookies.get('auth-provider')?.value === 'utexas'; const utexasTemplateOrg = import.meta.env.UTEXAS_GIT_REPO_ORG; const utexasTemplateRepo = import.meta.env.UTEXAS_GIT_REPO_PROJECT_TEMPLATE || body.templateRepo; From 82aa2104c0fae6e0d799903b5016421acff7d553 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:04:29 +0000 Subject: [PATCH 17/21] fix: auto-enable GitHub Pages (Actions source) for EMU repo creation --- src/lib/GitHub/index.ts | 4 ---- src/pages/api/projects/[projectName]/index.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/GitHub/index.ts b/src/lib/GitHub/index.ts index 4265b142..01dc56de 100644 --- a/src/lib/GitHub/index.ts +++ b/src/lib/GitHub/index.ts @@ -253,10 +253,6 @@ export const enablePages = async ( token: string ): Promise => { const body = { - source: { - branch: 'main', - path: '/', - }, build_type: 'workflow', }; return await fetch(`https://api.github.com/repos/${org}/${repo}/pages`, { diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index a7cc98a1..81ddab38 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -190,8 +190,8 @@ export const POST: APIRoute = async ({ } } - if (body.generate_pages_site) { - // Enable pages + if (isUtexasSession || body.generate_pages_site) { + // Enable pages; for EMU (UTexas) sessions this is always required. const respPages: Response = await enablePages( body.gitHubOrg, projectName as string, From d8bdddbc93c62f718190de8bc8fec36a21a7ca4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:24:50 +0000 Subject: [PATCH 18/21] fix: create internal repos as private initially and fix template URL owner bug --- src/lib/GitHub/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/GitHub/index.ts b/src/lib/GitHub/index.ts index 01dc56de..aba64854 100644 --- a/src/lib/GitHub/index.ts +++ b/src/lib/GitHub/index.ts @@ -177,11 +177,9 @@ export const createRepositoryFromTemplate = async ( owner: org, name: newRepoName, description: description, - private: visibility === 'private', + private: visibility === 'private' || visibility === 'internal', }; - const url = `https://api.github.com/repos/${ - import.meta.env.GIT_REPO_ORG - }/${templateRepo}/generate`; + const url = `https://api.github.com/repos/${owner}/${templateRepo}/generate`; const headers = { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, From 2e494ebe9e0478a72bcb6c413eef7db9c97139a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:30:06 +0000 Subject: [PATCH 19/21] fix: correctly change EMU repo visibility to internal in both create and settings-update paths --- src/pages/api/projects/[projectName]/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 81ddab38..07d08254 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -168,6 +168,9 @@ export const POST: APIRoute = async ({ const repo: FullRepository = await resp.json(); + // Two-step visibility change for GitHub EMU organizations: the template + // generation API does not support `visibility: 'internal'`, so the repo + // is first created as private (above) and then patched to internal here. if (repoVisibility === 'internal') { const visibilityResp = await changeRepoVisibility( token?.value as string, @@ -464,11 +467,21 @@ export const PUT: APIRoute = async ({ cookies, params, request, redirect }) => { // Has repo visibility changed? if (projectConfig.project.is_private !== body.is_private) { + // For GitHub EMU organizations, repos can only be private or internal (not + // public). Sending `private: false` to the API would attempt a public + // visibility change which would fail for EMU orgs. Use `'internal'` + // instead so the PATCH sets visibility explicitly. + const isEnterprise = + isEnterpriseGitHubOrg(slugContents.org) || + cookies.get('auth-provider')?.value === 'utexas'; + const targetVisibility: boolean | 'internal' = + !body.is_private && isEnterprise ? 'internal' : body.is_private; + const visResponse = await changeRepoVisibility( info?.token as string, slugContents.org, slugContents.repo, - body.is_private + targetVisibility ); if (!visResponse.ok) { From 5466d24bcac966c8a003f7fefe1e93a62131d029 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:05:43 +0000 Subject: [PATCH 20/21] fix: add delay and retry for EMU internal visibility change, improve error message --- src/i18n/en/error.json | 1 + src/pages/api/projects/[projectName]/index.ts | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/i18n/en/error.json b/src/i18n/en/error.json index 3ae92ab9..e7a75285 100644 --- a/src/i18n/en/error.json +++ b/src/i18n/en/error.json @@ -3,6 +3,7 @@ "Something went wrong.": "Something went wrong.", "_repo_exists_": "The repository name already exists in the GitHub Organization.", "_repo_create_failed_": "The repository was not successfully created", + "_repo_visibility_change_failed_": "Repository was created but could not be set to internal visibility. Please delete the repository from GitHub and try again.", "_failed_pages_enable_": "Failed to enable GitHub Pages for this repository", "_failed_adding_topic_": "Failed to add the AVAnnotate-Project topis to repository", "_failed_adding_collaborators_": "Failed to add the additional collaborators to the project.", diff --git a/src/pages/api/projects/[projectName]/index.ts b/src/pages/api/projects/[projectName]/index.ts index 07d08254..c2870f8b 100644 --- a/src/pages/api/projects/[projectName]/index.ts +++ b/src/pages/api/projects/[projectName]/index.ts @@ -171,8 +171,12 @@ export const POST: APIRoute = async ({ // Two-step visibility change for GitHub EMU organizations: the template // generation API does not support `visibility: 'internal'`, so the repo // is first created as private (above) and then patched to internal here. + // A short delay is required because GitHub initializes the repository + // asynchronously after returning 201, and the PATCH can fail if issued + // immediately. if (repoVisibility === 'internal') { - const visibilityResp = await changeRepoVisibility( + await delay(2000); + let visibilityResp = await changeRepoVisibility( token?.value as string, body.gitHubOrg, projectName as string, @@ -180,10 +184,22 @@ export const POST: APIRoute = async ({ ); if (!visibilityResp.ok) { - await logGitHubFailure('repo-visibility-internal', visibilityResp); + // One retry after an additional short delay to handle transient failures. + await logGitHubFailure('repo-visibility-internal-attempt-1', visibilityResp); + await delay(3000); + visibilityResp = await changeRepoVisibility( + token?.value as string, + body.gitHubOrg, + projectName as string, + 'internal' + ); + } + + if (!visibilityResp.ok) { + await logGitHubFailure('repo-visibility-internal-attempt-2', visibilityResp); return new Response( JSON.stringify({ - avaError: '_repo_create_failed_', + avaError: '_repo_visibility_change_failed_', }), { status: 500, From 6341d4376eaa09808f0502d2ddb2e21c391cdfc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:10:09 +0000 Subject: [PATCH 21/21] feat: exclude personal account for EMU users on new project form --- src/backend/projectHelpers.ts | 20 ++++++++---- src/components/ProjectForm/NewProjectForm.tsx | 32 ++++++++++++------- src/components/ProjectForm/ProjectForm.css | 9 ++++++ src/i18n/en/new-project.json | 3 +- src/pages/[lang]/projects/new.astro | 3 +- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/backend/projectHelpers.ts b/src/backend/projectHelpers.ts index c6c2411e..b8bed48d 100644 --- a/src/backend/projectHelpers.ts +++ b/src/backend/projectHelpers.ts @@ -42,15 +42,23 @@ export const parseURL = (url: string) => { const TEMPLATE_BRANCH = import.meta.env.OVERRIDE_TEMPLATE_BRANCH || 'main'; export const getOrgs = async ( - userInfo: UserInfo + userInfo: UserInfo, + authProvider?: string ): Promise => { const orgs = await getUserOrgs(userInfo.token); - orgs.unshift({ - login: userInfo.profile.gitHubName, - url: `https://https://github.com/${userInfo.profile.gitHubName}`, - description: '', - }); + // Enterprise Managed Users (EMU) cannot create repositories in their personal + // account — only in organisations. Skip prepending the personal account when + // the session was initiated via an enterprise OAuth app (e.g. utexas). + const isEnterpriseUser = !!authProvider && authProvider !== ''; + + if (!isEnterpriseUser) { + orgs.unshift({ + login: userInfo.profile.gitHubName, + url: `https://github.com/${userInfo.profile.gitHubName}`, + description: '', + }); + } return orgs.map((o) => ({ orgName: o.login, diff --git a/src/components/ProjectForm/NewProjectForm.tsx b/src/components/ProjectForm/NewProjectForm.tsx index 6457b745..dbb45b8a 100644 --- a/src/components/ProjectForm/NewProjectForm.tsx +++ b/src/components/ProjectForm/NewProjectForm.tsx @@ -38,7 +38,7 @@ const FormContents = (props: NewProjectFormProps) => { const { headerMap } = useContext(SpreadsheetInputContext); const emptyProject: Project = { - github_org: props.orgs[0].orgName, + github_org: props.orgs[0]?.orgName ?? '', is_private: false, generate_pages_site: true, title: '', @@ -106,15 +106,25 @@ const FormContents = (props: NewProjectFormProps) => { return (

{t['General']}

- ({ - value: o.orgName, - label: o.orgName, - }))} - required - /> + {props.orgs.length === 0 ? ( +
+ { + t[ + '_no_orgs_warning_' + ] + } +
+ ) : ( + ({ + value: o.orgName, + label: o.orgName, + }))} + required + /> + )} { diff --git a/src/components/ProjectForm/ProjectForm.css b/src/components/ProjectForm/ProjectForm.css index f188f4d1..e3546104 100644 --- a/src/components/ProjectForm/ProjectForm.css +++ b/src/components/ProjectForm/ProjectForm.css @@ -28,3 +28,12 @@ .project-form-actions-container > button { min-width: 200px; } + +.project-form-no-orgs-warning { + padding: 12px 16px; + border: 1px solid var(--amber-7, #f5a623); + border-radius: 6px; + background-color: var(--amber-2, #fff8eb); + color: var(--amber-11, #7e4a0d); + margin-bottom: 16px; +} diff --git a/src/i18n/en/new-project.json b/src/i18n/en/new-project.json index 4da1dfeb..586784ca 100644 --- a/src/i18n/en/new-project.json +++ b/src/i18n/en/new-project.json @@ -37,5 +37,6 @@ "Use Private Repository": "Private Repository", "_private_repository_helper_text_": "Private repositories do not generate GitHub Pages sites.", "Generate GitHub Pages Site": "Generate GitHub Pages Site", - "_generate_pages_site_helper_text_": "Generate a GitHub Pages site. If unchecked a static site which you can deploy will be created and checked into your project repository." + "_generate_pages_site_helper_text_": "Generate a GitHub Pages site. If unchecked a static site which you can deploy will be created and checked into your project repository.", + "_no_orgs_warning_": "Your account is a GitHub Enterprise Managed User account. You must be a member of a GitHub organization to create a project. Please contact your organization administrator to be added to an organization." } diff --git a/src/pages/[lang]/projects/new.astro b/src/pages/[lang]/projects/new.astro index f6d38bdd..ea4338f3 100644 --- a/src/pages/[lang]/projects/new.astro +++ b/src/pages/[lang]/projects/new.astro @@ -19,7 +19,8 @@ if (!info) { return Astro.redirect(`/${lang}/sign-in`); } -const orgs = await getOrgs(info); +const authProvider = Astro.cookies.get('auth-provider')?.value ?? ''; +const orgs = await getOrgs(info, authProvider); ---