repo_update #305
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
name: Sync Rest | |
on: | |
workflow_dispatch: | |
inputs: | |
repos: | |
description: 'List of repos to rerun the syncing workflow for, formatted as JSON array, e.g. ["exercism/configlet"]' | |
required: true | |
default: "" | |
repository_dispatch: | |
types: [appends_update, repo_update] | |
push: | |
branches: | |
- main | |
paths-ignore: | |
- README.md | |
- LICENSE | |
- .github/labels.yml | |
- .github/workflows/sync-labels.yml | |
env: | |
BOT_USERNAME: "exercism-bot" | |
GIT_USERNAME: "Exercism Bot" | |
GIT_EMAIL: "[email protected]" | |
jobs: | |
open-tracking-issue: | |
name: Open tracking issue | |
runs-on: ubuntu-20.04 | |
outputs: | |
issue-url: ${{ steps.open-tracking-issue.outputs.result }} | |
steps: | |
- name: Open tracking issue | |
id: open-tracking-issue | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
with: | |
result-encoding: string | |
script: | | |
const title = `[Tracking] ${context.workflow} - ${context.eventName} - ${context.actor} - ${context.sha}` | |
const label = '👁 tracking issue' | |
const existingIssue = (await github.paginate(github.rest.issues.listForRepo, { | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
labels: label, | |
state: 'open', | |
})).find((issue) => issue.title === title) | |
if (existingIssue) { | |
console.log(`::notice::Existing issue found: ${existingIssue.html_url}`) | |
return existingIssue.html_url | |
} | |
const issue = (await github.rest.issues.create({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
title: title, | |
body: `Workflow run: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
labels: [label] | |
})).data | |
return issue.html_url | |
fetch-repos: | |
name: Fetch & fork target repos | |
runs-on: ubuntu-20.04 | |
outputs: | |
repos: ${{ steps.fetch-repos.outputs.result }} | |
steps: | |
- name: Fetch list of target repos | |
id: fetch-repos | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
with: | |
script: | | |
if (context.eventName === 'repository_dispatch') { | |
return context.payload.client_payload.repos | |
} else if (context.eventName === 'workflow_dispatch') { | |
return JSON.parse(context.payload.inputs.repos) | |
} else { | |
const allRepos = (await github.paginate(github.rest.search.repos, { | |
q: 'org:exercism+is:public+archived:false' | |
})).flatMap(({ full_name }) => [full_name]) | |
const toolingRepos = (await github.paginate(github.rest.search.repos, { | |
q: 'org:exercism+topic:exercism-tooling+is:public+archived:false' | |
})).flatMap(({ full_name }) => [full_name]) | |
const trackRepos = (await github.paginate(github.rest.search.repos, { | |
q: 'org:exercism+topic:exercism-track+is:public+archived:false' | |
})).flatMap(({ full_name }) => [full_name]) | |
return allRepos.filter(r => !toolingRepos.includes(r) && !trackRepos.includes(r)) | |
} | |
- name: Determine target repos needing to be forked | |
id: required-forks | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
with: | |
github-token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
script: | | |
const botRepos = await github.paginate(github.rest.repos.listForAuthenticatedUser, { | |
affiliation: 'owner' | |
}) | |
// Replacing the bot username with exercism is slightly hacky, but necessary | |
// for the comparison below. Unfortunately, GitHub's API response does not seem | |
// to contain any reference to the upstream repo of the fork. | |
// TODO: The GraphQL API does have this info. Look into changing to that. | |
const botForks = botRepos.flatMap(({ full_name, fork }) => fork ? [full_name.replace(process.env.BOT_USERNAME, 'exercism')] : []) | |
const repos = ${{ steps.fetch-repos.outputs.result }} | |
return repos.filter(repo => !botForks.includes(repo)) | |
- name: Fork repos that haven't been forked yet | |
if: steps.required-forks.outputs.result != '[]' | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
with: | |
github-token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
script: | | |
const reposFullNames = ${{ steps.required-forks.outputs.result }} | |
const repos = reposFullNames.map((fullName) => fullName.split('/')[1]) | |
for (let repo of repos) { | |
await github.rest.repos.createFork({ | |
owner: 'exercism', | |
repo | |
}) | |
// Sleep a bit to avoid rate-limiting | |
await new Promise(x => setTimeout(x, 2000)) | |
} | |
// Forking repos happens asynchronously on GitHub, therefore | |
// we need to sleep a bit to ensure it has been created | |
await new Promise(x => setTimeout(x, 60000)) | |
abort-timer: | |
needs: [fetch-repos, open-tracking-issue] | |
runs-on: ubuntu-20.04 | |
name: ⚠ 5 MINUTES TO ABORT | |
environment: abort-timer | |
steps: | |
- run: exit 0 | |
push-to-repos: | |
needs: [fetch-repos, abort-timer, open-tracking-issue] | |
name: Push to ${{ matrix.repo }} | |
runs-on: ubuntu-20.04 | |
timeout-minutes: 30 | |
# Launch one job per track. | |
# This is probably less efficient than running everything in one job | |
# and manually cloning and checking out the repos. However, launching a job | |
# lets us use actions like actions/checkout. | |
# It also gives us a pretty job overview that makes it easy to spot issues with | |
# particular tracks. | |
strategy: | |
fail-fast: false | |
max-parallel: 1 | |
matrix: | |
repo: ${{ fromJson(needs.fetch-repos.outputs.repos) }} | |
steps: | |
- name: Check if pull request has already been opened | |
id: pr-already-exists | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
env: | |
TARGET_REPO_FULLNAME: ${{ matrix.repo }} | |
TRACKING_ISSUE_URL: ${{ needs.open-tracking-issue.outputs.issue-url }} | |
with: | |
github-token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
script: | | |
const repo = process.env.TARGET_REPO_FULLNAME.split('/')[1] | |
const existingPr = ( | |
await github.paginate(github.rest.issues.listForRepo, { | |
owner: "exercism", | |
repo: repo, | |
state: 'open', | |
}) | |
).find((pr) => | |
pr.body && pr.body.includes(process.env.TRACKING_ISSUE_URL) | |
) | |
if (existingPr) { | |
console.log(`::notice::Existing pull request found: ${existingPr.html_url}`) | |
return true | |
} | |
return false | |
############################################################ | |
# ONLY RUN THE REST OF THE SCRIPT IF THE PR DOES NOT EXIST # | |
# All following steps must have: # | |
# if: steps.pr-already-exists.outputs.result == 'false' # | |
########################################################### | |
- name: Checkout main repo | |
if: steps.pr-already-exists.outputs.result == 'false' | |
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 | |
with: | |
path: main | |
- name: Checkout target repo | |
if: steps.pr-already-exists.outputs.result == 'false' | |
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 | |
with: | |
repository: ${{ matrix.repo }} | |
token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
path: track-repo | |
fetch-depth: 0 | |
- name: Checkout new branch on track repo | |
if: steps.pr-already-exists.outputs.result == 'false' | |
id: branch | |
run: | | |
cd track-repo | |
# github.run_id is a unique number for each run within a repository. | |
# WARNING: the run_id will NOT be updated if a workflow is re-run | |
branch="org-wide-files/${{ github.run_id }}" | |
git checkout -b "$branch" | |
echo "name=$branch" >> $GITHUB_OUTPUT | |
- name: Copy globally synced files to target repo | |
if: steps.pr-already-exists.outputs.result == 'false' | |
run: | | |
cp -a main/global-files/. track-repo/ | |
- name: Determine if repo is a tooling repo | |
if: steps.pr-already-exists.outputs.result == 'false' | |
id: is-tooling-repo | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
env: | |
TARGET_REPO_FULLNAME: ${{ matrix.repo }} | |
with: | |
github-token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
script: | | |
const repoTopics = await github.paginate(github.rest.repos.getAllTopics, { | |
owner: 'exercism', | |
repo: process.env.TARGET_REPO_FULLNAME.split('/')[1] | |
}) | |
return repoTopics[0].names.includes('exercism-tooling') | |
- name: Copy tooling-only synced files to target repo | |
if: steps.is-tooling-repo.outputs.result == 'true' | |
run: | | |
cp -a main/tooling-files/. track-repo/ | |
- name: Determine if repo is a track repo | |
if: steps.pr-already-exists.outputs.result == 'false' | |
id: is-track-repo | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
env: | |
TARGET_REPO_FULLNAME: ${{ matrix.repo }} | |
with: | |
github-token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
script: | | |
const repoTopics = await github.paginate(github.rest.repos.getAllTopics, { | |
owner: 'exercism', | |
repo: process.env.TARGET_REPO_FULLNAME.split('/')[1] | |
}) | |
return repoTopics[0].names.includes('exercism-track') | |
- name: Copy tracks-only synced files to target repo | |
if: steps.is-track-repo.outputs.result == 'true' | |
run: | | |
cp -a main/tracks-files/. track-repo/ | |
- name: Apply transformations based on track config | |
if: steps.pr-already-exists.outputs.result == 'false' | |
env: | |
TRACK: ${{ matrix.repo }} | |
run: julia --color=yes main/scripts/apply-track-config.jl | |
- name: Check for changes | |
if: steps.pr-already-exists.outputs.result == 'false' | |
id: changes | |
run: | | |
cd track-repo | |
if [ -z "$(git status --porcelain)" ]; then | |
echo "No files have changed." | |
exit 0 | |
fi | |
echo "changes=true" >> $GITHUB_OUTPUT | |
######################################################## | |
# ONLY RUN THE REST OF THE SCRIPT IF THERE ARE CHANGES # | |
# All following steps must have: # | |
# if: steps.changes.outputs.changes == 'true' # | |
######################################################## | |
- name: Configure the git user | |
if: steps.changes.outputs.changes == 'true' | |
run: | | |
git config --global user.email "$GIT_EMAIL" | |
git config --global user.name "$GIT_USERNAME" | |
- name: Commit and push changes | |
if: steps.changes.outputs.changes == 'true' | |
env: | |
TARGET_REPO_FULLNAME: ${{ matrix.repo }} | |
BOT_PERSONAL_ACCESS_TOKEN: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
run: | | |
cd track-repo | |
# Show diff for debugging | |
git diff | |
git add . | |
# The commit message assumes that one push maps to exactly one commit | |
# If a push contains several commits, the commit message will only link to the last one | |
git commit -m "🤖 Sync org-wide files to upstream repo" -m "More info: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA" | |
# Push commit | |
target_repo="${TARGET_REPO_FULLNAME/exercism/$BOT_USERNAME}" | |
git push --force "$GITHUB_SERVER_URL/$target_repo.git" | |
- name: Open pull request on track repo | |
if: steps.changes.outputs.changes == 'true' | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
env: | |
TARGET_REPO_FULLNAME: ${{ matrix.repo }} | |
BRANCH_NAME: ${{ steps.branch.outputs.name }} | |
TRACKING_ISSUE_URL: ${{ needs.open-tracking-issue.outputs.issue-url }} | |
with: | |
github-token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} | |
script: | | |
const repo = process.env.TARGET_REPO_FULLNAME.split('/')[1] | |
github.rest.pulls.create({ | |
owner: 'exercism', | |
repo, | |
title: '🤖 Sync org-wide files to upstream repo', | |
head: `exercism-bot:${process.env.BRANCH_NAME}`, | |
base: 'main', | |
body: `ℹ More info: ${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}\n👁 Tracking issue: ${process.env.TRACKING_ISSUE_URL}`, | |
maintainer_can_modify: false | |
}) |