-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from filecoin-project/init-falcon
Initial commit
- Loading branch information
Showing
19 changed files
with
1,245 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains 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,129 @@ | ||
import axios from "axios"; | ||
import { Buffer } from "buffer"; | ||
|
||
/** | ||
* Fetches the list of filenames that have been changed in a given pull request. | ||
* | ||
* @param {string} owner - The owner of the GitHub repository. | ||
* @param {string} repo - The name of the GitHub repository. | ||
* @param {number} prNumber - The pull request number. | ||
* @param {string} githubToken - The GitHub API token. | ||
* @returns {Promise<string[]|null>} - A promise resolving to an array of filenames or null in case of error. | ||
*/ | ||
async function fetchChangedFilesInPR(owner, repo, prNumber, githubToken) { | ||
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`; | ||
const headers = { Authorization: `token ${githubToken}` }; | ||
|
||
try { | ||
const { data } = await axios.get(url, { headers }); | ||
return data; | ||
} catch (err) { | ||
console.error('Error fetching changed files:', err); | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Fetches the content of a JSON file based on its SHA. | ||
* | ||
* @param {string} owner - The owner of the GitHub repository. | ||
* @param {string} repo - The name of the GitHub repository. | ||
* @param {string} sha - The SHA of the file. | ||
* @param {string} githubToken - The GitHub API token. | ||
* @returns {Promise<Object|null>} - A promise resolving to the parsed JSON content or null in case of error. | ||
*/ | ||
async function fetchJSONFileContent(owner, repo, sha, githubToken) { | ||
const url = `https://api.github.com/repos/${owner}/${repo}/git/blobs/${sha}`; | ||
const headers = { Authorization: `token ${githubToken}` }; | ||
|
||
try { | ||
const { data } = await axios.get(url, { headers }); | ||
const fileContent = Buffer.from(data.content, 'base64').toString('utf-8'); | ||
return JSON.parse(fileContent); | ||
} catch (err) { | ||
throw new Error('Error fetching JSON file content:', err); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Merges a given pull request. | ||
* | ||
* @param {string} owner - The owner of the GitHub repository. | ||
* @param {string} repo - The name of the GitHub repository. | ||
* @param {number} prNumber - The pull request number. | ||
* @param {string} githubToken - The GitHub API token. | ||
* @returns {Promise<void>} - A promise indicating the completion of the operation. | ||
*/ | ||
async function mergePullRequest(owner, repo, prNumber, githubToken) { | ||
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/merge`; | ||
const headers = { Authorization: `token ${githubToken}` }; | ||
|
||
try { | ||
const { data } = await axios.put(url, {}, { headers }); | ||
console.log('Successfully merged:', data); | ||
} catch (err) { | ||
throw new Error('Error merging PR:', err); | ||
} | ||
} | ||
|
||
/** | ||
* Processes a pull request to determine if it should be automatically merged. | ||
* Criteria for merging include having only one changed file that is a JSON file | ||
* with specific properties. | ||
* | ||
* @param {string} owner - The owner of the GitHub repository. | ||
* @param {string} repo - The name of the GitHub repository. | ||
* @param {number} prNumber - The pull request number. | ||
* @param {string} githubToken - The GitHub API token. | ||
* @returns {Promise<void>} - A promise indicating the completion of the operation. | ||
*/ | ||
async function processPullRequest(owner, repo, prNumber, githubToken) { | ||
const changedFiles = await fetchChangedFilesInPR(owner, repo, prNumber, githubToken); | ||
|
||
if (!changedFiles) { | ||
throw new Error('No changed files.'); | ||
} | ||
const changedFilenames = changedFiles.map(file => file.filename); | ||
|
||
console.log('Changed files:', changedFilenames); | ||
|
||
if (changedFilenames.length === 1 && changedFilenames[0].endsWith('.json')) { | ||
console.log('A single JSON file has been modified.'); | ||
const fileContent = await fetchJSONFileContent(owner, repo, changedFiles[0].sha, githubToken); | ||
|
||
if (!fileContent) { | ||
throw new Error('No file content found.'); | ||
} | ||
|
||
if ( | ||
fileContent?.Lifecycle?.State === "Granted" && | ||
( | ||
fileContent?.Lifecycle?.['Validated At'] !== "" || | ||
fileContent?.Lifecycle?.Active === false | ||
) | ||
) { | ||
console.log("Conditions met for automatic merge."); | ||
await mergePullRequest(owner, repo, prNumber, githubToken); | ||
} else { | ||
throw new Error("Conditions not met for automatic merge."); | ||
} | ||
} else { | ||
throw new Error('Either multiple files are modified or the modified file is not a JSON.'); | ||
} | ||
} | ||
|
||
const owner = process.env.OWNER; | ||
const repo = process.env.REPO; | ||
const prNumber = process.env.PR_NUMBER; | ||
const githubToken = process.env.GITHUB_TOKEN; | ||
|
||
(async function run() { | ||
try { | ||
await processPullRequest(owner, repo, prNumber, githubToken); | ||
console.log('Automerge PR completed successfully.') | ||
} catch (error) { | ||
console.error(error.message); | ||
process.exit(1); | ||
} | ||
})(); |
This file contains 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,183 @@ | ||
/** | ||
* Flow Validator Script | ||
* | ||
* Use Cases: | ||
* - When an application is in "submitted state" | ||
* · application.Lifecycle['Validated By'] must be empty | ||
* · application.Lifecycle['Validated At'] must be empty | ||
* · application.Lifecycle['Active Request ID'] must be empty | ||
* · application['Allocation Requests'] array must be empty | ||
* - When an aplication is in some other state | ||
* · actor must be filplus-github-bot-read-write[bot] | ||
*/ | ||
|
||
import axios from "axios"; | ||
|
||
const FILPLUS_BOT="filplus-github-bot-read-write[bot]"; | ||
|
||
/** | ||
* This is the main function that will be executed by the workflow and it will validate the application flow | ||
* | ||
* @param {string} owner | ||
* @param {string} repo | ||
* @param {number} prNumber | ||
* @param {string} githubToken | ||
* @returns | ||
*/ | ||
async function processPullRequest(owner, repo, prNumber, githubToken) { | ||
const lastCommitAuthor = await fetchLastCommitAuthor(owner, repo, prNumber, githubToken); | ||
if (!lastCommitAuthor) { | ||
throw new Error('Error fetching last commit author.'); | ||
} | ||
console.log('Author of the last commit:', lastCommitAuthor); | ||
|
||
const changedFiles = await fetchChangedFiles(owner, repo, prNumber, githubToken); | ||
if (!changedFiles) { | ||
throw new Error('Error fetching changed files.'); | ||
} | ||
|
||
const changedFilenames = changedFiles.map(file => file.filename); | ||
|
||
console.log('List of files changed in PR:', changedFilenames); | ||
|
||
|
||
if (changedFilenames.length > 1 || !changedFilenames[0].endsWith('.json')) { | ||
throw new Error('Either multiple files are modified or the modified file is not a JSON.'); | ||
} | ||
|
||
const application = await fetchJSONFileContent(owner, repo, changedFiles[0].sha, githubToken); | ||
|
||
if (!application) { | ||
throw new Error('Error fetching file content.'); | ||
} | ||
|
||
application?.Lifecycle?.State == 'Submitted' | ||
? await validateSubmittedState(application) | ||
: await validateOtherState(lastCommitAuthor); | ||
|
||
} | ||
|
||
/** | ||
* Fetches the author of the last commit in a given pull request. | ||
* | ||
* @param {string} owner - The owner of the GitHub repository. | ||
* @param {string} repo - The name of the GitHub repository. | ||
* @param {number} prNumber - The pull request number. | ||
* @param {string} githubToken - The GitHub API token. | ||
* @returns {Promise<string|null>} - A promise resolving to the author of the last commit, | ||
* or null in case of error. | ||
*/ | ||
async function fetchLastCommitAuthor(owner, repo, prNumber, githubToken) { | ||
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/commits`; | ||
const headers = { Authorization: `token ${githubToken}` }; | ||
|
||
try { | ||
const { data: commits } = await axios.get(url, { headers }); | ||
const lastCommit = commits[commits.length - 1]; | ||
return lastCommit.author.login; | ||
} catch (err) { | ||
throw new Error('Error fetching last commit author:', err); | ||
} | ||
} | ||
|
||
/** | ||
* Fetches the list of filenames that have been changed in a given pull request. | ||
* | ||
* @param {string} owner - The owner of the GitHub repository. | ||
* @param {string} repo - The name of the GitHub repository. | ||
* @param {number} prNumber - The pull request number. | ||
* @param {string} githubToken - The GitHub API token. | ||
* @returns {Promise<Array|null>} - A promise resolving to an array containing the changed filenames, | ||
* or null in case of error. | ||
*/ | ||
async function fetchChangedFiles(owner, repo, prNumber, githubToken) { | ||
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`; | ||
const headers = { Authorization: `token ${githubToken}` }; | ||
|
||
try { | ||
const { data } = await axios.get(url, { headers }); | ||
return data; | ||
} catch (err) { | ||
throw new Error('Error fetching changed files:', err); | ||
} | ||
} | ||
|
||
/** | ||
* Fetches the content of a JSON file based on its SHA. | ||
* | ||
* @param {string} owner - The owner of the GitHub repository. | ||
* @param {string} repo - The name of the GitHub repository. | ||
* @param {string} sha - The SHA of the file. | ||
* @param {string} githubToken - The GitHub API token. | ||
* @returns {Promise<Object|null>} - A promise resolving to the parsed JSON content or null in case of error. | ||
*/ | ||
async function fetchJSONFileContent(owner, repo, sha, githubToken) { | ||
const url = `https://api.github.com/repos/${owner}/${repo}/git/blobs/${sha}`; | ||
const headers = { Authorization: `token ${githubToken}` }; | ||
|
||
try { | ||
const { data } = await axios.get(url, { headers }); | ||
const fileContent = Buffer.from(data.content, 'base64').toString('utf-8'); | ||
return JSON.parse(fileContent); | ||
} catch (err) { | ||
throw new Error('Error fetching JSON file content:', err); | ||
} | ||
} | ||
|
||
/** | ||
* This function validates an application in a "Submitted" state | ||
* | ||
* @param {Application} application - The application to validate | ||
* @returns Boolean - True if the application is valid, false otherwise | ||
*/ | ||
async function validateSubmittedState(application) { | ||
console.log('Application is in a "Submitted" state'); | ||
|
||
if(application?.Lifecycle?.['Validated by']) { | ||
throw new Error('Application is already validated (Validated by field is not empty)'); | ||
} | ||
|
||
if(application?.Lifecycle?.['Validated at']) { | ||
throw new Error('Application is already validated (Validated time field is not empty)'); | ||
} | ||
|
||
if(application?.Lifecycle?.['Active Request ID']) { | ||
throw new Error('Application has an allocation assigned (Current allocation id field is not empty)'); | ||
} | ||
|
||
if(application?.['Allocation Requests'].length > 0) { | ||
throw new Error('Application has an allocation assigned (Datacap allocations array is not empty)'); | ||
} | ||
|
||
console.log('Application is valid'); | ||
} | ||
|
||
/** | ||
* This function validates an application in a state different than "Submitted" | ||
* | ||
* @param {Application} application | ||
* @returns Boolean - True if the application is valid, false otherwise | ||
*/ | ||
async function validateOtherState(lastCommitAuthor) { | ||
console.log('Application is in a state different than "Submitted"'); | ||
if (lastCommitAuthor !== FILPLUS_BOT) { | ||
throw new Error(`Invalid author. Expected ${FILPLUS_BOT}, got ${lastCommitAuthor}`); | ||
} | ||
console.log('Application is valid'); | ||
|
||
} | ||
|
||
const owner = process.env.OWNER; | ||
const repo = process.env.REPO; | ||
const prNumber = process.env.PR_NUMBER; | ||
const githubToken = process.env.GITHUB_TOKEN; | ||
|
||
(async function run() { | ||
try { | ||
await processPullRequest(owner, repo, prNumber, githubToken); | ||
console.log('Flow validation completed successfully.') | ||
} catch (error) { | ||
console.error(error.message); | ||
process.exit(1); | ||
} | ||
})(); |
This file contains 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,43 @@ | ||
// .github/scripts/notify_stale_issues.js | ||
|
||
import { Octokit } from "@octokit/rest"; | ||
import fetch from "node-fetch"; | ||
|
||
const octokit = new Octokit({ | ||
auth: process.env.GITHUB_TOKEN, | ||
request: { | ||
fetch: fetch | ||
} | ||
}); | ||
|
||
|
||
const owner = process.env.GITHUB_REPOSITORY.split('/')[0]; | ||
const repo = process.env.GITHUB_REPOSITORY.split('/')[1]; | ||
|
||
async function checkAndCommentOnIssues() { | ||
const { data: issues } = await octokit.issues.listForRepo({ | ||
owner, | ||
repo, | ||
state: 'open' | ||
}); | ||
|
||
const tenDaysAgo = new Date(); | ||
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10); | ||
const tenDaysAgoDateString = tenDaysAgo.toISOString().split('T')[0]; | ||
|
||
for (const issue of issues) { | ||
const updatedAt = new Date(issue.updated_at); | ||
const updatedAtDateString = updatedAt.toISOString().split('T')[0]; | ||
|
||
if (updatedAtDateString === tenDaysAgoDateString) { | ||
await octokit.issues.createComment({ | ||
owner, | ||
repo, | ||
issue_number: issue.number, | ||
body: "This application has not seen any responses in the last 10 days. This issue will be marked with Stale label and will be closed in 4 days. Comment if you want to keep this application open." | ||
}); | ||
} | ||
} | ||
} | ||
|
||
checkAndCommentOnIssues(); |
This file contains 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,35 @@ | ||
name: Validate Application Approval | ||
|
||
on: | ||
pull_request: | ||
types: | ||
- opened | ||
- synchronize | ||
pull_request_review: | ||
types: | ||
- submitted | ||
|
||
jobs: | ||
validate-approval: | ||
runs-on: ubuntu-latest | ||
env: | ||
DOMAIN: ${{ secrets.DOMAIN }} | ||
steps: | ||
- name: Call Backend to Validate Application Approval | ||
id: validate | ||
run: | | ||
echo "Calling backend to validate with pr_number: ${{ github.event.pull_request.number }} and user_handle: ${{ github.actor }}" | ||
RESPONSE=$(curl --header "Content-Type: application/json" \ | ||
--request POST \ | ||
--data '{"pr_number": "'${{ github.event.pull_request.number }}'", "user_handle": "'${{ github.actor }}'"}' \ | ||
"${DOMAIN}/application/approval/validate") | ||
echo "Response from validation: $RESPONSE" | ||
if [ "$RESPONSE" != "true" ]; then | ||
echo "Error: Validation returned false" | ||
exit 1 | ||
fi | ||
- name: Validation Success | ||
if: steps.validate.outcome == 'success' | ||
run: echo "Validation successful!" |
Oops, something went wrong.