diff --git a/.cbc/CBClogo.png b/.cbc/CBClogo.png new file mode 100644 index 000000000..466825af2 Binary files /dev/null and b/.cbc/CBClogo.png differ diff --git a/.cbc/addLabel.js b/.cbc/addLabel.js new file mode 100644 index 000000000..9118643ba --- /dev/null +++ b/.cbc/addLabel.js @@ -0,0 +1,18 @@ +const { Octokit } = require("@octokit/core"); + +const addLabel = async (authToken, issueNumber, labelToAdd) => { + const octokit = new Octokit({ auth: authToken }); + const response = await octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/labels", { + owner: "BYUComputingBootCampTests", + repo: "githubTest", + issue_number: issueNumber, + labels: [labelToAdd] + }); + +} + +// Start +var authToken = process.argv[2]; +var issueNumber = process.argv[3]; +var labelToAdd = process.argv[4]; +addLabel(authToken, issueNumber, labelToAdd); \ No newline at end of file diff --git a/.cbc/assertContains.js b/.cbc/assertContains.js new file mode 100644 index 000000000..be14e62f1 --- /dev/null +++ b/.cbc/assertContains.js @@ -0,0 +1,15 @@ +const fs = require('fs') + +var fileToReadPath = process.argv[2]; +var required = process.argv[3].split(','); + +const data = fs.readFileSync(fileToReadPath, 'utf8') +required.forEach(string => { + assertContains(data, string); +}); + +function assertContains(data, string) { + if (!data.includes(string)) { + throw ("Error: File does not contain " + string); + } +} \ No newline at end of file diff --git a/.cbc/assertDoesNotContain.js b/.cbc/assertDoesNotContain.js new file mode 100644 index 000000000..abb8b4e59 --- /dev/null +++ b/.cbc/assertDoesNotContain.js @@ -0,0 +1,15 @@ +const fs = require('fs') + +var fileToReadPath = process.argv[2]; +var notAllowed = process.argv[3].split(','); + +const data = fs.readFileSync(fileToReadPath, 'utf8') +notAllowed.forEach(string => { + assertDoesNotContain(data, string); +}); + +function assertDoesNotContain(data, string) { + if (data.includes(string)) { + throw ("Error: File contains " + string); + } +} \ No newline at end of file diff --git a/.cbc/badgeAPI.js b/.cbc/badgeAPI.js new file mode 100644 index 000000000..ba0be9a5a --- /dev/null +++ b/.cbc/badgeAPI.js @@ -0,0 +1,145 @@ +var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; +var computingBootCampId = '0YOSWoQPQO-ehX8P3o7ZFw'; +var githubBadgeEntityID = 'MnrvOXV8QpC2VOYcgVTOlQ'; + +//Program starts here +var username = process.argv[2]; +var password = process.argv[3]; +var userEmail = process.argv[4]; + +//Check to make sure there are contents for each arguments +if (username.localeCompare('') == 0 || password.localeCompare('') == 0 || userEmail.localeCompare('') == 0) { + throw "Error: Missing argument"; +} + +var accessToken = getAuthenticationToken(username, password); +issueAssertionToTestUser(computingBootCampId, githubBadgeEntityID, userEmail, accessToken); + + +//This function can be used to get an authentication token to make requests with the server for the Computing Boot Camp +function getAuthenticationToken(username, password) { + var url = "https://api.badgr.io/o/token"; + + var xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + console.log(xhr.status); + console.log(xhr.responseText); + } + }; + + var data = "username=" + username + "&password=" + password; + + xhr.send(data); + + if (xhr.status != 200) { + throw "Error: Invalid Credentials for the BYU Computing BootCamp - Please contact the BYU Computing BootCamp \ +through the Support section on the README.md for help" + } + var accessToken = JSON.parse(xhr.responseText).access_token; + return accessToken; +} + +//This function uses a refresh Token to make a certain authToken reusable again to make requests with the server. +//If you have a refresh token but not the corresponding authToken that goes with it, you'll have to get a new +//authentication token. +function refreshStoredAuthToken() { + var url = "https://api.badgr.io/o/token"; + + var xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + console.log(xhr.status); + console.log(xhr.responseText); + } + }; + + var data = "grant_type=refresh_token&refresh_token=" + refreshToken; + + xhr.send(data); + return JSON.parse(xhr.responseText); +} + +//This function will take an authToken and get the issuerInformation tied to that account +function getIssuerInformation(authToken) { + var url = "https://api.badgr.io/v2/issuers"; + + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + + xhr.setRequestHeader("Authorization", "Bearer " + authToken); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + console.log(xhr.status); + console.log(xhr.responseText); + } + }; + + xhr.send(); + + return JSON.parse(xhr.responseText); +} + +//This function will take an authToken and issuerID and return the badgeClass information for that Issuer +function getBadgeClassInformation(issuerEntityID) { + var url = "https://api.badgr.io/v2/issuers/" + issuerEntityID + "/badgeclasses"; + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + + xhr.setRequestHeader("Authorization", "Bearer " + accessToken); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + console.log(xhr.status); + console.log(xhr.responseText); + } + }; + + xhr.send(); + + return JSON.parse(xhr.responseText); +} + +//This function will take an authToken, issuerId and badgeID and issue a badge to the person with the information provided. +function issueAssertionToTestUser(issuerEntityID, badgeEntityID, userEmail, accessToken) { //Assertion is another name for Badge + //Issue the Assertion that we want to + var url = "https://api.badgr.io/v2/issuers/" + issuerEntityID + "/assertions"; + + var xhr = new XMLHttpRequest(); + xhr.open("POST", url, false); + + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.setRequestHeader("Authorization", "Bearer " + accessToken); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + console.log(xhr.status); + console.log(xhr.responseText); + } + }; + + var data = { + "badgeclass": badgeEntityID, + "recipient": { + "identity": userEmail, + "hashed": false, + "type": "email", + }, + "notify": true, + }; + var dataString = JSON.stringify(data); + + xhr.send(dataString); + + if (xhr.status != 201) throw "Error: Invalid Email Address - Please put a valid email address into email.txt on your \ +Forked Repository"; + else console.log("Success! github Badge has been issued to" + userEmail); + //return JSON.parse(xhr.responseText); +} \ No newline at end of file diff --git a/.cbc/getFile.js b/.cbc/getFile.js new file mode 100644 index 000000000..52ee13c46 --- /dev/null +++ b/.cbc/getFile.js @@ -0,0 +1,17 @@ +const { Octokit } = require("@octokit/core"); + +const getFile = async (authToken, repoInfo, filePath) => { + const octokit = new Octokit({auth: authToken}); + const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { + owner: repoInfo[0], + repo: repoInfo[1], + path: filePath + }) + + console.log(Buffer.from(response.data.content, 'base64').toString('binary')); +} + +var authToken = process.argv[2]; +var repoInfo = process.argv[3].split("/"); +var filePath = process.argv[4]; +getFile(authToken, repoInfo, filePath); diff --git a/.cbc/getRepoInfo.js b/.cbc/getRepoInfo.js new file mode 100644 index 000000000..1d3da1826 --- /dev/null +++ b/.cbc/getRepoInfo.js @@ -0,0 +1,37 @@ +const { Octokit } = require("@octokit/core"); + +const getRepoInfo = async (authToken, infoNeeded) => { + const octokit = new Octokit({ auth: authToken }); + const response = await octokit.request("GET /repos/{owner}/{repo}/pulls?state=open", { + owner: "BYUComputingBootCampTests", + repo: "githubTest" + }); + + let index = 0; + //While there are still repositories that are open + while (response.data.length > index) { + //If the repository doesn't have a "currently being checked" label + let label = "hi"; + if (response.data[index].labels.length != 0) { + label = response.data[index].labels[0]; + } + if (response.data[index].labels.length == 0 || label.name.toString().localeCompare("currentlyBeingChecked") != 0) { + if (infoNeeded.toString().localeCompare('full_name') == 0) { + repoInfo = response.data[index].head.repo.full_name; + process.stdout.write(repoInfo); + return; + } else { + repoInfo = response.data[index].number; + var repoNumber = repoInfo.toString(); + process.stdout.write(repoNumber); + return; + } + } + index++; + } + throw "No Repositories waiting to be checked"; +} + +var authToken = process.argv[2]; +var infoNeeded = process.argv[3]; +getRepoInfo(authToken, infoNeeded); diff --git a/.cbc/makeComment.js b/.cbc/makeComment.js new file mode 100644 index 000000000..5b723ee34 --- /dev/null +++ b/.cbc/makeComment.js @@ -0,0 +1,18 @@ +const { Octokit } = require("@octokit/core"); + +const makeComment = async (authToken, issueNumber, comment) => { + const octokit = new Octokit({ auth: authToken }); + const response = await octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", { + owner: "BYUComputingBootCampTests", + repo: "githubTest", + issue_number: issueNumber, + body: comment + }); + +} + +// Start +var authToken = process.argv[2]; +var issueNumber = process.argv[3]; +var comment = process.argv[4]; +makeComment(authToken, issueNumber, comment); \ No newline at end of file diff --git a/.cbc/removeAllLabels.js b/.cbc/removeAllLabels.js new file mode 100644 index 000000000..cb86c92d7 --- /dev/null +++ b/.cbc/removeAllLabels.js @@ -0,0 +1,15 @@ +const { Octokit } = require("@octokit/core"); + +const deleteAllLabels = async (authToken, issueNumber) => { + const octokit = new Octokit({ auth: authToken }); + const response = await octokit.request("DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels", { + owner: "BYUComputingBootCampTests", + repo: "githubTest", + issue_number: issueNumber + }); +} + +// Start +var authToken = process.argv[2]; +var issueNumber = process.argv[3]; +deleteAllLabels(authToken, issueNumber); \ No newline at end of file diff --git a/.cbc/triggerRunForAllPRs.js b/.cbc/triggerRunForAllPRs.js new file mode 100644 index 000000000..f2ba19a00 --- /dev/null +++ b/.cbc/triggerRunForAllPRs.js @@ -0,0 +1,34 @@ +const { Octokit } = require("@octokit/core"); + +const triggerRunForAllPRs = async (authToken) => { + const octokit = new Octokit({ auth: authToken }); + const response = await octokit.request("GET /repos/{owner}/{repo}/pulls?state=open", { + owner: "BYUComputingBootCampTests", + repo: "githubTest" + }); + + console.log(response) + var numberOfPRs = response.data.length; + if (numberOfPRs > 12) numberOfPRs = 12; //So that the github action doesn't overlap with the next scheduled round + for (let i = 0; i < numberOfPRs; i++) { + const octokitMakeTest = new Octokit({ auth: authToken }); + const responseNew = await octokitMakeTest.request("POST /repos/{owner}/{repo}/dispatches", { + owner: "BYUComputingBootCampTests", + repo: "githubTest", + event_type: "test_pr" + }); + sleep(10000); //Wait 10 seconds so the last workflow has time to label the PR as "currently being checked" + } +} + +function sleep(milliseconds) { + const date = Date.now(); + let currentDate = null; + do { + currentDate = Date.now(); + } while (currentDate - date < milliseconds); +} + +// Start +var authToken = process.argv[2]; +triggerRunForAllPRs(authToken); diff --git a/.github/workflows/githubTest.yml b/.github/workflows/githubTest.yml new file mode 100644 index 000000000..e43618461 --- /dev/null +++ b/.github/workflows/githubTest.yml @@ -0,0 +1,145 @@ +name: GitHub Test + +on: + repository_dispatch: + types: + [test_pr] + +jobs: + runTests: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Setup Node.js environment + uses: actions/setup-node@v2.1.5 + + - name: Install octokit/core.js + run: npm install @octokit/core + + - name: Install xmlhttprequest + run: npm install xmlhttprequest + + - name: Setup Python environment + uses: actions/setup-python@v4 + with: + python-version: '>=3.7' + + - name: Setup Git environment + run: | + git config --global user.email "byucomputingbootcamptests@gmail.com" + git config --global user.name "BYU Computing Bootcamp Tests" + + #Get Repository that isn't currently being checked + - name: Get a Pull Request's Repo Name that isn't already being checked + run: node .cbc/getRepoInfo.js ${{ secrets.AUTH_TOKEN }} full_name > repo.txt + + - name: Save Repository name as Output Variable + id: repo + uses: juliangruber/read-file-action@v1 + with: + path: repo.txt + + - name: Get the Pull Request's Number + run: node .cbc/getRepoInfo.js ${{ secrets.AUTH_TOKEN }} number > number.txt + + - name: Save Repository Number as Output Variable + id: number + uses: juliangruber/read-file-action@v1 + with: + path: number.txt + + - name: Add "currentlyBeingChecked" label + run: node .cbc/addLabel.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} currentlyBeingChecked + + #INSERT TESTING CODE HERE! + + - name : Get Step File + id: getStepFile + run: node .cbc/getFile.js ${{ secrets.AUTH_TOKEN }} ${{ steps.repo.outputs.content }} .github/script/STEP > step + + - name: Comment + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Got step file" + + - name: Read Step File + id: readStepFile + uses: juliangruber/read-file-action@v1 + with: + path: step + + - name: Comment + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Read step file" + + - name: Assert correct step + id: checkStepFile + uses: therussiankid92/gat@v1.5 + with: + assertion: should.equal + actual: ${{ steps.readStepFile.outputs.content }} + expected: ${{ secrets.LAST_STEP }} + + - name: Comment + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Last step found" + + #Issue Badge + - name: Get email.txt + id: getEmail + run: node .cbc/getFile.js ${{ secrets.AUTH_TOKEN }} ${{ steps.repo.outputs.content }} email.txt > email.txt + + - name: Save Email as Output Variable + id: userEmail + uses: juliangruber/read-file-action@v1 + with: + path: email.txt + + - name: Comment + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Email found - ${{ steps.userEmail.outputs.content }}" + + - name: Issue Badge + id: issueBadge + run: node .cbc/badgeAPI.js ${{secrets.USERNAME}} ${{secrets.PASSWORD}} ${{ steps.userEmail.outputs.content }} + + - name: Comment + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Badge Issued. Congratulations!" + + #INSERT FAILURE COMMENTS HERE + + - name: Failure Comment + if: always() && steps.getStepFile.outcome == 'failure' + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Error - step file not found" + + - name: Failure Comment + if: always() && steps.readStepFile.outcome == 'failure' + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Error - step file not readable" + + - name: Failure Comment + if: always() && steps.checkStepFile.outcome == 'failure' + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Error - step file incorrect" + + + #Failure Output for Issue Badge + - name: Failure Comment + if: always() && steps.getEmail.outcome == 'failure' + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Error - No email.txt found" + + - name: Failure Comment + if: always() && steps.issueBadge.outcome == 'failure' + run: node .cbc/makeComment.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} "Error - Badge issue failed - Email Address wasn't valid" + + #Close Pull Request + - name: Close Pull Request + if: always() + uses: peter-evans/close-pull@v1 + with: + pull-request-number: ${{ steps.number.outputs.content }} + comment: Auto-closing pull request after submission + delete-branch: false + + - name: Remove Previous Labels + if: always() + run: node .cbc/removeAllLabels.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} + + - name: Add "checkComplete" label + if: always() + run: node .cbc/addLabel.js ${{ secrets.AUTH_TOKEN }} ${{ steps.number.outputs.content }} checkComplete diff --git a/.github/workflows/triggerPRruns.yml b/.github/workflows/triggerPRruns.yml new file mode 100644 index 000000000..cafaea5b9 --- /dev/null +++ b/.github/workflows/triggerPRruns.yml @@ -0,0 +1,22 @@ +name: Trigger PR Test Runs + +on: + workflow_dispatch: + schedule: + - cron: '0/5 * * * *' + +jobs: + triggerRuns: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Setup Node.js environment + uses: actions/setup-node@v2.1.5 + + - name: Install octokit/core.js + run: npm install @octokit/core + + - name: Trigger makeTest.yml for each PR + run: node .cbc/triggerRunForAllPRs.js ${{ secrets.AUTH_TOKEN }} diff --git a/README.md b/README.md index e185299b3..591720005 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ _Get started using GitHub in less than an hour._ +**Hey, Friend! If you got here from the BYU Computing Bootcamp, great! We've made some modifications to this repository so that you can still get the badge for it at the end. Just follow these instructions here, and then when you get to the final step, there will be a few extra things you'll need to do to get the badge.** + People use GitHub to build some of the most advanced technologies in the world. Whether you’re visualizing data or building a new game, there’s a whole community and set of tools on GitHub that can help you do it even better. GitHub Skills’ “Introduction to GitHub” course guides you through everything you need to start contributing in less than an hour. - **Who is this for**: New developers, new GitHub users, and students. @@ -226,6 +228,8 @@ Check out these resources to learn more or get involved: - [Read the GitHub Getting Started docs](https://docs.github.com/en/get-started). - To find projects to contribute to, check out [GitHub Explore](https://github.com/explore). +**For those of you in the BYU Computing Bootcamp, you may want to obtain the badge, from Badgr! To do this, you'll need to fork this template repository. Once you've done that, you'll need to add your email to the email.txt file, and then copy the contents of your .github/script/STEP file from the repository you created earlier to your newly forked template repository. Then you'll need to make a pull request from your forked repository to this repository. This will award you the badge! Good luck!** +