Generally speaking, an off-line hackathon event takes place with people getting together at the same time and place for about two to three nights, intensively. On the other hand, all events have turned into online-only nowadays, and there's no exception for the hackathon events either. To keep the same event experiences, hackathon organisers use many online collaboration tools. In this case, almost the same number of event staff members are necessary. What if you have limited resources and budget and are required to run the online hackathon event?
For two weeks, I recently ran an online-only hackathon event called HackaLearn from August 2, 2021. This post is the retrospective of the event from the event organiser's perspective. If anyone is planning a hackathon with a similar concept, I hope this post could be helpful.
The Background
In May 2021 at //Build Conference, Azure Static Web Apps (ASWA) became generally available. It's relatively newer than the other competitors' ones meaning it is less popular than the others. This HackaLearn event is one of the practices to promote ASWA. The idea was simple. We're not only running a hackathon event but also offering the participants learning experiences with Microsoft Learn to participants so that they can feel how convenient ASWA is to use. Therefore, all participants can learn ASWA and build their app with ASWA – this was the direction.
In fact, the first HackaLearn event was held in Israel, and other countries in the EMEA region have been running this event. I also borrowed the concept and localised the format for Korean uni students. With support from Microsoft Learn Student Ambassadors (MLSA) and GitHub Campus Experts (GCE), they review the participants pull requests and external field experts were invited as mentors and ran online mentoring sessions.
The Problems
As mentioned above, running a hackathon event requires intensive, dedicated and exclusive resources, including time, people and money. However, none of them was adequate. I've got very limited resources, and even I couldn't dedicate myself to this event either. I was the only one who could operate the event. Both MLSAs and GCEs were dedicated for PR reviews and mentors for mentoring sessions. Automating all the event operation processes was the only answer for me.
How can I automate all the things?
For me, finding out the solution is the key focus area throughout this event.
The Constraints
There were a few constraints to be considered.
-
No Website for Hackathon
:police_car_light: There was no website for HackaLearn. Usually, the event website is built on a one-off basis, which seems less economical.
:backhand_index_pointing_right: Therefore, I decided to use the GitHub repository for the event because it offers many built-in features such as Project, Discussions, Issues, Wiki, etc. -
No Place for Participant Registration
:police_car_light: There was no registration form.
:backhand_index_pointing_right: Therefore, I decided to use Microsoft Forms. -
No Database for Participant Management
:police_car_light: There was no database for the participant management to record their challenge progress.
:backhand_index_pointing_right: Therefore, instead of provisioning a database instance, I decided to use Microsoft Lists. -
No Dashboard for Teams and Individuals Progress Tracking
:police_car_light: There was no dashboard to track each team's and each participant's progress.
:backhand_index_pointing_right: So instead, I decided to use their team page by merging their pull requests.
I've defined the overall business process workflow in the following sequence diagrams. All I needed was to sort out those limitations stated above. To me, it was Power Platform, GitHub Actions and Microsoft 365 with minimal coding efforts and maximum outcomes.
The Plans for Process Automation
All of sudden, the limitations above have become opportunities to experiment with the new process automation!
- The event itself uses the GitHub repository, meaning all PRs and issues can be handled by GitHub Actions.
- Microsoft 365 services like Microsoft Forms and Microsoft Lists are used for data input and storage.
- Power Automate is one of the Power Platform services and is used for the core of process automation.
So, the GitHub repository and Microsoft 365 services are fully integrated with GitHub Actions workflows and Power Automate workflows. As a result, I was able to save a massive amount of time and money with them.
The Result – Participant Registration
The first automation process I worked on was about storing data. The participant details need to be saved in Microsoft Lists. When a participant enters their details through Microsoft Forms, then a Power Automate workflow is triggered to process the registration details. At the same time, the workflow calls a GitHub Actions workflow to create a team page for the participant. Here's the simple sequence diagram describing this process.
The overall process is divided into two parts – one to process participant details in the Power Automate workflow, and the other to process the details in the GitHub Actions workflow.
Power Automate Workflow
Let's have a look at the Power Automate part. When a participant registers through Microsoft Forms, the form automatically triggers a Power Automate workflow. The workflow checks the email address whether the participant has already registered or not. If the email doesn't exist, the participant details are stored to Microsoft Lists.
Then it generates a team page. Instead of creating it directly from the Power Automate workflow, it builds the page content and sends it to the GitHub Actions workflow. The workflow_dispatch
event is triggered for this action.
Finally, the workflow sends a confirmation email. In terms of the name, participants may register themselves with English names or Korean names. Therefore, I need logic to check the participant's name. If the participant name is written in English, it should be [Given Name] [Surname]
(with a space; eg. Justin Yoo). If it's written in Korean, it should be [Surname][Given Name]
(without a space; eg. μ μ μ€ν΄). The red-boxed actions are responsible for identifying the participant's name. It may be simplified by adopting a custom connector with an Azure Functions endpoint.
GitHub Actions Workflow
As mentioned above, the Power Automate workflow calls a GitHub Actions workflow to generate a team page. Let's have a look. The workflow_dispatch
event takes the input details from Power Automate, and they are teamName
and content
.
name: On Team Page Requested on: workflow_dispatch: inputs: teamName: description: The name of team required: true default: Team_HackaLearn content: description: The content of the file to be created required: true default: Hello HackaLearn
Since GitHub Marketplace has various types of Actions, I can simply choose one to create the team page, commit the change and push it back to the repository.
- name: Create team page uses: DamianReeves/write-file-action@master with: path: "./teams/${{ github.event.inputs.teamName }}.md" contents: ${{ github.event.inputs.content }} write-mode: overwrite - name: Commit team page shell: bash run: | git config --local user.email "hackalearn.korea@outlook.com" git config --local user.name "HackaLearn Korea" git add ./teams/\* --force git commit -m "Team: ${{ github.event.inputs.teamName }} added" - name: Push team page uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.ref }}
Now, I've got the registration process fully automated. Let's move on.
The Result – Challenges Update
In this HackaLearn event, each participant was required to complete six challenges. Every time they finish one challenge, they MUST update their team page and create a PR to reflect their progress. As there are not many differences between the challenges, I'm going to use the Social Media Challenge as an example.
Here's the simple sequence diagram describing the process.
- After the participant posts a post to their social media, they update their team page and raise a PR. Then, a GitHub Actions workflow labels the PR with
review-required
and assigns a reviewer. - The assigned reviewer checks the social media post whether it's appropriately hashtagged with
#hackalearn
and#hackalearnkorea
. - Once confirmed, the reviewer adds the
review-completed
label to the PR. Then another GitHub Actions workflow automatically removes thereview-required
label from the PR. - The reviewer completes the review by leaving a comment of
/socialsignoff
, and the comment triggers another GitHub Actions workflow. The workflow calls the Power Automate workflow that updates the record on Microsoft Lists with the challenge progress. - The Power Automate workflow calls back to another GitHub Actions workflow to add
record-updated
andcompleted-social
labels to the PR and remove thereview-completed
labels from it. - If there is an issue while updating the record, the GitHub Actions workflow adds the
review-required
label so that the assigned reviewer starts review again.
GitHub Actions Workflow
As described above, there are five GitHub Actions workflow used to handle this request.
Challenge Update PR
The GitHub Actions workflow is triggered by the participant requesting a new PR. The event triggered is pull_request_target
, and it's only activated when the changes occur under the teams
directory.
name: On Challenge Submitted on: pull_request_target: types: - opened branches: - main paths: - 'teams/**/*.md'
If the PR is created later than the due date and time, the PR should not be accepted. Therefore, A PowerShell script is used to check the due date automatically. Since the PR's created_at
value is the UTC value, it should be converted to the Korean local time, included in the PowerShell script.
jobs: labelling: name: 'Add a label on submission: review-required' runs-on: ubuntu-latest steps: - name: Get PR date/time id: checkpoint shell: pwsh run: | $tz = [TimeZoneInfo]::FindSystemTimeZoneById("Asia/Seoul") $dateSubmitted = [DateTimeOffset]::Parse("${{ github.event.pull_request.created_at }}") $offset = $tz.GetUtcOffset($dateSubmitted) $dateSubmitted = $dateSubmitted.ToOffset($offset) $dateDue = $([DateTimeOffset]::Parse("2021-08-16T00:00:00.000+09:00")) $isOverdue = "$($dateSubmitted -gt $dateDue)".ToLowerInvariant() $dateSubmittedValue = $dateSubmitted.ToString("yyyy-MM-ddTHH:mm:ss.fffzzz") $dateDueValue = $dateDue.ToString("yyyy-MM-ddTHH:mm:ss.fffzzz") echo "::set-output name=dateSubmitted::$dateSubmittedValue" echo "::set-output name=dateDue::$dateDueValue" echo "::set-output name=isOverdue::$isOverdue"
If the PR is over the due, it's immediately rejected and closed.
- name: Add a label - Overdue if: ${{ steps.checkpoint.outputs.isOverdue == 'true' }} uses: buildsville/add-remove-label@v1 with: token: "${{ secrets.GITHUB_TOKEN }}" label: 'OVERDUE-SUBMIT' type: add - name: Comment to PR - Overdue if: ${{ steps.checkpoint.outputs.isOverdue == 'true' }} uses: bubkoo/auto-comment@v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pullRequestOpened: | ππΌ @{{ author }} λ! * PR μ μΆ μκ°: ${{ steps.checkpoint.outputs.dateSubmitted }} * PR λ§κ° μκ°: ${{ steps.checkpoint.outputs.dateDue }} μνκΉκ²λ μ μΆνμ PRμ λ§κ° κΈ°νμΈ ${{ steps.checkpoint.outputs.dateDue }}μ λκΈ°μ ¨μ΅λλ€. π λ°λΌμ, μ΄λ² HackaLearn μ΄λ²€νΈμ λ°μλμ§ μμ΅λλ€. κ·Έλμ HackaLearn μ΄λ²€νΈμ μ°Έμ¬ν΄ μ£Όμ μ κ°μ¬ λ립λλ€. λ€μ κΈ°νμ λ€μ λ§λμ! - name: Close PR - Overdue if: ${{ steps.checkpoint.outputs.isOverdue == 'true' }} uses: superbrothers/close-pull-request@v3 with: comment: "μ μΆ κΈ°ν μ’ λ£"
If it's before the due, label the PR, leave a comment and randomly assign a reviewer.
- name: Add a label if: ${{ steps.checkpoint.outputs.isOverdue == 'false' }} uses: actions/labeler@v3 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" configuration-path: '.github/labeler.yml' - name: Comment to PR if: ${{ steps.checkpoint.outputs.isOverdue == 'false' }} uses: bubkoo/auto-comment@v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pullRequestOpenedReactions: 'rocket, +1' pullRequestOpened: > ππΌ @{{ author }} λ! μ±λ¦°μ§ μλ£ PRλ₯Ό μμ±ν΄ μ£Όμ μ κ°μ¬ν©λλ€! π μ°Έκ°μλμ ν΄μ»€ν€ μμ£Όλ₯Ό μμν΄μ! πͺπΌ PR ν νλ¦Ώ μμ± κ°μ΄λλΌμΈμ μ μ€μνμ ¨λμ§ νμΈν΄μ£ΌμΈμ. μ΅λν λΉ λ₯΄κ² 리뷰νκ² μ΅λλ€! π πΉ From. HackaLearn μ΄μμ§ μΌλ πΉ - name: Randomly assign a staff if: ${{ steps.checkpoint.outputs.isOverdue == 'false' }} uses: gerardabello/auto-assign@v1.0.1 with: github-token: "${{ secrets.GITHUB_TOKEN }}" number-of-assignees: 1 assignee-pool: "${{ secrets.PR_REVIEWERS }}"
Challenge Review Completed
The assigned reviewer confirms the challenge and labels the result. This labelling action triggers the following GitHub Actions workflow.
name: On Challenge Labelled on: pull_request_target: types: - labeled - unlabeled jobs: labelling: name: 'Update a label' runs-on: ubuntu-latest steps: - name: Respond to label uses: dessant/label-actions@v2 with: process-only: prs
Challenge Review Approval
Commenting like /socialsignoff
for the social media post challenge automatically triggers the following GitHub Actions workflow, with the event of issue_comment
.
name: On Challenge Review Commented on: issue_comment: types: - created
The first step of this workflow is to check whether the commenter is the assigned reviewer, then find out which challenge is approved. The review-completed
label MUST exist on the PR, and the commenter MUST be in the reviewer list (secrets.PR_REVIEWERS
).
env: PR_REVIEWERS: ${{ secrets.PR_REVIEWERS }} jobs: signoff: if: ${{ github.event.issue.pull_request }} name: 'Sign-off challenge' runs-on: ubuntu-latest steps: - name: Get checkpoints id: checkpoint shell: pwsh run: | $hasValidLabel = "${{ contains(github.event.issue.labels.*.name, 'review-completed') }}" $isCommenterAssignee = "${{ github.event.comment.user.login == github.event.issue.assignee.login }}" $isValidCommenter = "${{ contains(env.PR_REVIEWERS, github.event.comment.user.login) }}" $isAswaSignoff = "${{ github.event.comment.body == '/aswasignoff' }}" $isGhaSignoff = "${{ github.event.comment.body == '/ghasignoff' }}" $isSocialSignoff = "${{ github.event.comment.body == '/socialsignoff' }}" $isAppSignoff = "${{ github.event.comment.body == '/appsignoff' }}" $isRepoSignoff = "${{ github.event.comment.body == '/reposignoff' }}" $isRetroSignoff = "${{ github.event.comment.body == '/retrosignoff' }}" $timestamp = "${{ github.event.comment.created_at }}" echo "::set-output name=hasValidLabel::$hasValidLabel" echo "::set-output name=isCommenterAssignee::$isCommenterAssignee" echo "::set-output name=isValidCommenter::$isValidCommenter" echo "::set-output name=isAswaSignoff::$isAswaSignoff" echo "::set-output name=isGhaSignoff::$isGhaSignoff" echo "::set-output name=isSocialSignoff::$isSocialSignoff" echo "::set-output name=isAppSignoff::$isAppSignoff" echo "::set-output name=isRepoSignoff::$isRepoSignoff" echo "::set-output name=isRetroSignoff::$isRetroSignoff" echo "::set-output name=timestamp::$timestamp"
If all conditions are met, the workflow takes one action based on the type of the challenge. Each action calls a Power Automate workflow to update the record on Microsoft Lists, send a confirmation email, and calls back to another GitHub Actions workflow.
- name: Record challenge ASWA if: ${{ steps.checkpoint.outputs.hasValidLabel == 'true' && steps.checkpoint.outputs.isCommenterAssignee == 'true' && steps.checkpoint.outputs.isValidCommenter == 'true' && steps.checkpoint.outputs.isAswaSignoff == 'true' }} uses: joelwmale/webhook-action@2.1.0 with: url: ${{ secrets.FLOW_URL }} body: '{"gitHubId": "${{ github.event.issue.user.login }}", "challengeType": "aswa", "timestamp": "${{ steps.checkpoint.outputs.timestamp }}", "prId": ${{ github.event.issue.number }} }' - name: Record challenge GHA if: ${{ steps.checkpoint.outputs.hasValidLabel == 'true' && steps.checkpoint.outputs.isCommenterAssignee == 'true' && steps.checkpoint.outputs.isValidCommenter == 'true' && steps.checkpoint.outputs.isGhaSignoff == 'true' }} uses: joelwmale/webhook-action@2.1.0 with: url: ${{ secrets.FLOW_URL }} body: '{"gitHubId": "${{ github.event.issue.user.login }}", "challengeType": "gha", "timestamp": "${{ steps.checkpoint.outputs.timestamp }}", "prId": ${{ github.event.issue.number }} }' - name: Record challenge SOCIAL if: ${{ steps.checkpoint.outputs.hasValidLabel == 'true' && steps.checkpoint.outputs.isCommenterAssignee == 'true' && steps.checkpoint.outputs.isValidCommenter == 'true' && steps.checkpoint.outputs.isSocialSignoff == 'true' }} uses: joelwmale/webhook-action@2.1.0 with: url: ${{ secrets.FLOW_URL }} body: '{"gitHubId": "${{ github.event.issue.user.login }}", "challengeType": "social", "timestamp": "${{ steps.checkpoint.outputs.timestamp }}", "prId": ${{ github.event.issue.number }} }' - name: Record challenge APP if: ${{ steps.checkpoint.outputs.hasValidLabel == 'true' && steps.checkpoint.outputs.isCommenterAssignee == 'true' && steps.checkpoint.outputs.isValidCommenter == 'true' && steps.checkpoint.outputs.isAppSignoff == 'true' }} uses: joelwmale/webhook-action@2.1.0 with: url: ${{ secrets.FLOW_URL }} body: '{"gitHubId": "${{ github.event.issue.user.login }}", "challengeType": "app", "timestamp": "${{ steps.checkpoint.outputs.timestamp }}", "prId": ${{ github.event.issue.number }} }' - name: Record challenge REPO if: ${{ steps.checkpoint.outputs.hasValidLabel == 'true' && steps.checkpoint.outputs.isCommenterAssignee == 'true' && steps.checkpoint.outputs.isValidCommenter == 'true' && steps.checkpoint.outputs.isRepoSignoff == 'true' }} uses: joelwmale/webhook-action@2.1.0 with: url: ${{ secrets.FLOW_URL }} body: '{"gitHubId": "${{ github.event.issue.user.login }}", "challengeType": "repo", "timestamp": "${{ steps.checkpoint.outputs.timestamp }}", "prId": ${{ github.event.issue.number }} }' - name: Record challenge RETRO if: ${{ steps.checkpoint.outputs.hasValidLabel == 'true' && steps.checkpoint.outputs.isCommenterAssignee == 'true' && steps.checkpoint.outputs.isValidCommenter == 'true' && steps.checkpoint.outputs.isRetroSignoff == 'true' }} uses: joelwmale/webhook-action@2.1.0 with: url: ${{ secrets.FLOW_URL }} body: '{"gitHubId": "${{ github.event.issue.user.login }}", "challengeType": "retro", "timestamp": "${{ steps.checkpoint.outputs.timestamp }}", "prId": ${{ github.event.issue.number }} }'
Challenge Complete or Further Review
This GitHub Actions workflow completes the challenge, triggered by a Power Automate workflow through the workflow_dispatch
event. Power Automate sends values of prId
, labelsToAdd
, labelsToRemove
and isMergeable
.
name: On Challenge Completed on: workflow_dispatch: inputs: prId: description: PR ID required: true default: '' labelsToAdd: description: The comma delimited labels to add required: true default: record-updated labelsToRemove: description: The comma delimited labels to remove required: true default: review-completed isMergeable: description: The value indicating whether the challenge is mergeable or not. required: true default: 'false'
The first action is to add labels to the PR and remove labels from the PR.
jobs: update_labels: name: 'Update labels' runs-on: ubuntu-latest steps: - name: Update labels on PR shell: pwsh run: | $headers = @{ "Authorization" = "token ${{ secrets.GITHUB_TOKEN }}"; "User-Agent" = "HackaLearn Bot"; "Accept" = "application/vnd.github.v3+json" } $owner = "devrel-kr" $repository = "HackaLearn" $issueId = "${{ github.event.inputs.prId }}" $labelsToAdd = "${{ github.event.inputs.labelsToAdd }}" -split "," $body = @{ "labels" = $labelsToAdd } $url = "https://api.github.com/repos/$owner/$repository/issues/$issueId/labels" Invoke-RestMethod -Method Post -Uri $url -Headers $headers -Body $($body | ConvertTo-Json) $labelsToRemove = "${{ github.event.inputs.labelsToRemove }}" -split "," $labelsToRemove | ForEach-Object { $label = $_; $url = "https://api.github.com/repos/$owner/$repository/issues/$issueId/labels/$label"; Invoke-RestMethod -Method Delete -Uri $url -Headers $headers }
And finally, this action merges the PR. If there's an error on the Power Automate workflow side, the isMeargeable
value MUST be false
, meaning it won't execute the merge action.
merge_pr: name: 'Merge PR' needs: update_labels runs-on: ubuntu-latest steps: - name: Merge PR if: ${{ github.event.inputs.isMergeable == 'true' }} shell: pwsh run: | $headers = @{ "Authorization" = "token ${{ secrets.WORKFLOW_DISPATCH_TOKEN }}"; "User-Agent" = "HackaLearn Bot"; "Accept" = "application/vnd.github.v3+json" } $owner = "devrel-kr" $repository = "HackaLearn" $issueId = "${{ github.event.inputs.prId }}" $url = "https://api.github.com/repos/$owner/$repository/pulls/$issueId" $pr = Invoke-RestMethod -Method Get -Uri $url -Headers $headers $sha = $pr.head.sha $title = "" $message = "" $merge = "squash" $body = @{ "commit_title" = $title; "commit_message" = $message; "sha" = $sha; "merge_method" = $merge; } $url = "https://api.github.com/repos/$owner/$repository/pulls/$issueId/merge" Invoke-RestMethod -Method Put -Uri $url -Headers $headers -Body $($body | ConvertTo-Json)
Power Automate Workflow
The challenge approval workflow calls this Power Automate workflow. Firstly, it checks the type of challenges. If no challenge is identified, it does nothing.
If the challenge is linked to the registered participant's GitHub ID, update the record on Microsoft Lists; otherwise, do nothing.
Finally, it sends a confirmation email using a different email template based on the number of challenges completed.
The Others: Other Power Automate Workflows
Previously described Power Automate workflows are triggered by GitHub Actions for integration. However, there are other workflows only for management purposes. As most processes are similar to each other, I'm not going to describe them all. Instead, it's the total number of workflows that I used for the event, which is 15 in total.
Now all my business processes are fully automated. As an operator, I can focus on questions and PR reviews, but nothing else.
The Stats
The HackaLearn even was over! Here are some numbers related to this HackaLearn event.
14
: Number of days for HackaLearn171
: Total number of participants62
: Total number of participants who completed Cloud Skills Challenge21
: Total number of teams who uploaded social media posts17
: Total number of teams who completed building Azure Static Web Apps16
: Total number of teams who completed provided their GitHub repository20
: Total number of teams who published their blog post as a retrospective13
: Total number of teams who completed all six challenges
The Lessons Learnt
Surely, there are many spaces for future improvement. What I've learnt from the automation exercise are:
-
Do not assume the way participants create their PRs is expected
:backhand_index_pointing_right: The review automation process should be as flexible as possible.
-
Make the review process as simple as possible
:backhand_index_pointing_right: The reviewers should only be required to focus on the PR, not anything else.
-
Reviewer should be assigned to a team instead of individual PRs
:backhand_index_pointing_right: If a reviewer is assigned to a team, the chance for merge conflicts will dramatically decrease.
-
Make Power Automate workflows as modular as possible
:backhand_index_pointing_right: There are many similar sequence of actions across many workflows.
:backhand_index_pointing_right: They can be modularised into either sub-workflows or custom APIs through custom connectors.
The Side Events
During the event, we ran live hands-on workshops for GitHub Actions and Azure Static Web Apps led by a GCE and an MLSA respectively.
-
Live Hands-on: GitHub Actions (in Korean)
-
Live Hands-on: Azure Static Web Apps (in Korean)
-
Live Hands-on: Azure Static Web Apps with Headless CMS (in Korean)
So far, I summarised what I've learnt from this event and what I've done for workflow automation, using GitHub Actions, Microsoft 365 and Power Automate. Although there are lots of spaces to improve, I managed to run the online hackathon event with a fully automated process. I can now do it again in the future.
Specially thanks to MLSAs and GCEs to review all the PRs, and mentors who answered questions from participants. Without them, regardless of the fully automated workflows, this event wouldn't be successfully running.
This article was originally published on Dev Kimchi.
Posted at https://sl.advdat.com/2XRDCAr