Skip to content

Commit

Permalink
Change the workflow to fork an upstream doc repo
Browse files Browse the repository at this point in the history
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
  • Loading branch information
nerdalert committed Jun 19, 2024
1 parent 9467d83 commit b8273dc
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 89 deletions.
6 changes: 4 additions & 2 deletions src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,17 @@ const authOptions: NextAuthOptions = {
if (user) {
token.id = user.id;
}
console.log('JWT Callback:', token);
// Uncomment for JWT debugging
// console.log('JWT Callback:', token);
return token;
},
async session({ session, token }) {
if (token) {
session.accessToken = token.accessToken;
session.id = token.id;
}
console.log('Session Callback:', session);
// Uncomment for session callback debugging
// console.log('Session Callback:', session);
return session;
},
async signIn({ account, profile }) {
Expand Down
7 changes: 6 additions & 1 deletion src/app/api/pr/knowledge/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export async function POST(req: NextRequest) {
const forkExists = await checkUserForkExists(headers, githubUsername);
if (!forkExists) {
await createFork(headers);
// Add a delay to ensure the fork operation completes to avoid a race condition when retrieving the bas SHA
// This only occurs if this is the first time submitting and the fork isn't present.
// TODO change to a retry
console.log('Pause 5s for the forking operation to complete');
await new Promise((resolve) => setTimeout(resolve, 5000));
}

const branchName = `knowledge-contribution-${Date.now()}`;
Expand All @@ -72,7 +77,7 @@ export async function POST(req: NextRequest) {
}

const yamlData = {
created_by: email,
created_by: githubUsername,
domain: domain,
task_description: task_description,
seed_examples: questions.map((question: string, index: number) => {
Expand Down
180 changes: 96 additions & 84 deletions src/app/api/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,40 +33,50 @@ export async function POST(req: NextRequest) {
console.log('GitHub Username:', githubUsername);
console.log('User Email:', userEmail);

const repoOwner = githubUsername;
const repoName = TAXONOMY_DOCUMENTS_REPO.split('/').pop();

if (!repoName) {
throw new Error('Repository name is undefined');
// Split the TAXONOMY_DOCUMENTS_REPO into owner and repo name
const repoPath = TAXONOMY_DOCUMENTS_REPO.replace('github.com/', '');
const [repoOwner, repoName] = repoPath.split('/');

console.log(`Repo Owner: ${repoOwner}`);
console.log(`Repo Name: ${repoName}`);

// Check if the repository is already forked
const repoForked = await checkIfRepoExists(headers, githubUsername, repoName);
console.log(`Repository forked: ${repoForked}`);
if (!repoForked) {
// Fork the repository if it is not already forked
await forkRepo(headers, repoOwner, repoName, githubUsername);
// Add a delay to ensure the fork operation completes to avoid a race condition when retrieving the bas SHA
// This only occurs if this is the first time submitting and the fork isn't present.
// TODO change to a retry
console.log('Pause 5s for the forking operation to complete');
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log('Repository forked');
}

const newBranchName = `upload-${Date.now()}`;

// Get the base branch SHA
const baseBranchSha = await getBranchSha(headers, repoOwner, repoName, BASE_BRANCH);
// Fetch the latest commit SHA of the base branch
const baseBranchSha = await getBranchSha(headers, githubUsername, repoName, BASE_BRANCH);
console.log(`Base branch SHA: ${baseBranchSha}`);

// Create a new branch
await createBranch(headers, repoOwner, repoName, newBranchName, baseBranchSha);
// Create files in the main branch with unique filenames e.g. foo-20240618T203521842.md
const timestamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', 'T').slice(0, -1);
const filesWithTimestamp = files.map((file: { fileName: string; fileContent: string }) => {
const [name, extension] = file.fileName.split(/\.(?=[^.]+$)/);
return {
fileName: `${name}-${timestamp}.${extension}`,
fileContent: file.fileContent
};
});

// Create files in the new branch
const commitSha = await createFilesCommit(headers, repoOwner, repoName, newBranchName, files, userEmail);

// Create a pull request
const prUrl = await createPullRequest(
headers,
repoOwner,
repoName,
newBranchName,
files.map((file: { fileName: string }) => file.fileName).join(', ')
);
const commitSha = await createFilesCommit(headers, githubUsername, repoName, BASE_BRANCH, filesWithTimestamp, userEmail, baseBranchSha);
console.log(`Created files commit SHA: ${commitSha}`);

return NextResponse.json(
{
repoUrl: `https://github.com/${repoOwner}/${repoName}`,
repoUrl: `https://github.com/${githubUsername}/${repoName}`,
commitSha,
documentNames: files.map((file: { fileName: string }) => file.fileName),
prUrl
documentNames: filesWithTimestamp.map((file: { fileName: string }) => file.fileName),
prUrl: `https://github.com/${githubUsername}/${repoName}`
},
{ status: 201 }
);
Expand All @@ -89,36 +99,60 @@ async function getGitHubUsernameAndEmail(headers: HeadersInit): Promise<{ github
return { githubUsername: data.login, userEmail: data.email };
}

async function getBranchSha(headers: HeadersInit, owner: string, repo: string, branch: string): Promise<string> {
const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/ref/heads/${branch}`, { headers });
async function checkIfRepoExists(headers: HeadersInit, owner: string, repo: string): Promise<boolean> {
const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}`, { headers });
const exists = response.ok;
if (!exists) {
const errorText = await response.text();
console.error('Repository does not exist:', response.status, errorText);
}
return exists;
}

async function forkRepo(headers: HeadersInit, owner: string, repo: string, forkOwner: string) {
const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/forks`, {
method: 'POST',
headers
});

if (!response.ok) {
const errorText = await response.text();
console.error('Failed to get branch SHA:', response.status, errorText);
throw new Error('Failed to get branch SHA');
console.error('Failed to fork repository:', response.status, errorText);
throw new Error('Failed to fork repository');
}

const data = await response.json();
return data.object.sha;
}
// Wait for the fork to be created
let forkCreated = false;
for (let i = 0; i < 10; i++) {
const forkExists = await checkIfRepoExists(headers, forkOwner, repo);
if (forkExists) {
forkCreated = true;
break;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}

async function createBranch(headers: HeadersInit, owner: string, repo: string, branchName: string, baseSha: string) {
const body = JSON.stringify({
ref: `refs/heads/${branchName}`,
sha: baseSha
});
if (!forkCreated) {
throw new Error('Failed to confirm fork creation');
}
}

const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs`, {
method: 'POST',
headers,
body
});
async function getBranchSha(headers: HeadersInit, owner: string, repo: string, branch: string): Promise<string> {
console.log(`Fetching branch SHA for ${branch}...`);
const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/ref/heads/${branch}`, { headers });

if (!response.ok) {
const errorText = await response.text();
console.error('Failed to create branch:', response.status, errorText);
throw new Error('Failed to create branch');
console.error('Failed to get branch SHA:', response.status, errorText);
if (response.status === 409 && errorText.includes('Git Repository is empty')) {
throw new Error('Git Repository is empty.');
}
throw new Error('Failed to get branch SHA');
}

const data = await response.json();
console.log('Branch SHA:', data.object.sha);
return data.object.sha;
}

async function createFilesCommit(
Expand All @@ -127,8 +161,10 @@ async function createFilesCommit(
repo: string,
branchName: string,
files: { fileName: string; fileContent: string }[],
userEmail: string
userEmail: string,
baseSha: string
): Promise<string> {
console.log('Creating files commit...');
// Create blobs for each file
const blobs = await Promise.all(
files.map((file) =>
Expand All @@ -142,16 +178,14 @@ async function createFilesCommit(
}).then((response) => response.json())
)
);

// Get base tree
const baseTreeSha = await getBaseTreeSha(headers, owner, repo, branchName);
console.log('Blobs created:', blobs);

// Create tree
const createTreeResponse = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees`, {
method: 'POST',
headers,
body: JSON.stringify({
base_tree: baseTreeSha,
base_tree: baseSha,
tree: files.map((file, index) => ({
path: file.fileName,
mode: '100644',
Expand All @@ -168,15 +202,19 @@ async function createFilesCommit(
}

const treeData = await createTreeResponse.json();
console.log('Tree created:', treeData);

// Create commit with DCO sign-off
// TODO: if the user's github does not have an associated github email, we need to specify one in the upload section
// or reuse the one from the form. If we use the email field from the form, it needs to be null checked when
// the user clicks the upload documents button.
const createCommitResponse = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits`, {
method: 'POST',
headers,
body: JSON.stringify({
message: `Add files: ${files.map((file) => file.fileName).join(', ')}\n\nSigned-off-by: ${userEmail}`,
tree: treeData.sha,
parents: [await getBranchSha(headers, owner, repo, branchName)]
parents: [baseSha]
})
});

Expand All @@ -187,47 +225,21 @@ async function createFilesCommit(
}

const commitData = await createCommitResponse.json();
console.log('Commit created:', commitData);

// Update branch reference
await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branchName}`, {
const updateBranchResponse = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branchName}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ sha: commitData.sha })
});

return commitData.sha;
}

async function getBaseTreeSha(headers: HeadersInit, owner: string, repo: string, branch: string): Promise<string> {
const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees/${branch}`, { headers });

if (!response.ok) {
const errorText = await response.text();
console.error('Failed to get base tree SHA:', response.status, errorText);
throw new Error('Failed to get base tree SHA');
if (!updateBranchResponse.ok) {
const errorText = await updateBranchResponse.text();
console.error('Failed to update branch reference:', updateBranchResponse.status, errorText);
throw new Error('Failed to update branch reference');
}
console.log('Branch reference updated');

const data = await response.json();
return data.sha;
}

async function createPullRequest(headers: HeadersInit, owner: string, repo: string, branchName: string, fileNames: string): Promise<string> {
const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/pulls`, {
method: 'POST',
headers,
body: JSON.stringify({
title: `Add files: ${fileNames}`,
head: branchName,
base: BASE_BRANCH
})
});

if (!response.ok) {
const errorText = await response.text();
console.error('Failed to create pull request:', response.status, errorText);
throw new Error('Failed to create pull request');
}

const data = await response.json();
return data.html_url;
return commitData.sha;
}
4 changes: 2 additions & 2 deletions src/components/Contribute/Knowledge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,10 @@ export const KnowledgeForm: React.FunctionComponent = () => {
setPatterns(result.documentNames.join(', ')); // Populate the patterns field
console.log('Files uploaded:', result.documentNames);
setSuccessAlertTitle('Document uploaded successfully!');
setSuccessAlertMessage('The document has been uploaded and a PR has been created.');
setSuccessAlertMessage('Documents have been uploaded to your repo to be referenced in the knowledge submission.');
setSuccessAlertLink(result.prUrl);
setIsSuccessAlertVisible(true);
setUseFileUpload(false); // Switch back to manual mode
setUseFileUpload(false); // Switch back to manual mode to display the newly created values in the knowledge submission
} else {
throw new Error(result.error || 'Failed to upload document');
}
Expand Down

0 comments on commit b8273dc

Please sign in to comment.