Skip to content

Commit

Permalink
Merge pull request #3 from filecoin-project/init-falcon
Browse files Browse the repository at this point in the history
Initial commit
  • Loading branch information
kokal33 authored Nov 13, 2023
2 parents 4274aff + df2e489 commit 730f5df
Show file tree
Hide file tree
Showing 19 changed files with 1,245 additions and 0 deletions.
427 changes: 427 additions & 0 deletions .github/ISSUE_TEMPLATE/ldn.yaml

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions .github/scripts/automerge-pr.mjs
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);
}
})();
183 changes: 183 additions & 0 deletions .github/scripts/flow-validator.mjs
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);
}
})();
43 changes: 43 additions & 0 deletions .github/scripts/notify_stale_issues.mjs
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();
35 changes: 35 additions & 0 deletions .github/workflows/approval-validator.yml
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!"
Loading

0 comments on commit 730f5df

Please sign in to comment.