18 min read

Running Hackathon by Yourself with GitHub Actions, Microsoft 365 and Power Platform

Justin Yoo

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.

HackaLearn Banner

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

  • No Website for Hackathon

    🚨 There was no website for HackaLearn. Usually, the event website is built on a one-off basis, which seems less economical.
    👉 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

    🚨 There was no registration form.
    👉 Therefore, I decided to use Microsoft Forms.

  • No Database for Participant Management

    🚨 There was no database for the participant management to record their challenge progress.
    👉 Therefore, instead of provisioning a database instance, I decided to use Microsoft Lists.

  • No Dashboard for Teams and Individuals Progress Tracking

    🚨 There was no dashboard to track each team's and each participant's progress.
    👉 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 is to sort out those limitations stated above. To me, it was Power Platform and GitHub Actions with minimal coding efforts and maximum outcomes.

The Plans for Process Automation

The limitations above have become opportunities to experiment with the new 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.

Registration Sequence Diagram

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.

Registration Flow 1

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.

Registration Flow 2

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.

Registration Flow 3

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

As 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.

Social Media Challenge Sequence Diagram

  1. 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.
  2. The assigned reviewer checks the social media post whether it's appropriately hashtagged with #hackalearn and #hackalearnkorea.
  3. Once confirmed, the reviewer adds the review-completed label to the PR. Then another GitHub Actions workflow automatically removes the review-required label from the PR.
  4. 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.
  5. The Power Automate workflow calls back to another GitHub Actions workflow to add record-updated and completed-social labels to the PR and remove the review-completed labels from it.
  6. 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 }} 님!
<br>
챌린지 완료 PR를 생성해 주셔서 감사합니다! 🎉 참가자님의 해커톤 완주를 응원해요! 💪🏼
<br>
PR 템플릿 작성 가이드라인을 잘 준수하셨는지 확인해주세요. 최대한 빠르게 리뷰하겠습니다! 😊
<br><br>
🔹 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.

Challenge Update Flow 1

If the challenge is linked to the registered participant's GitHub ID, update the record on Microsoft Lists; otherwise, do nothing.

Challenge Update Flow 2

Finally, it sends a confirmation email using a different email template based on the number of challenges completed.

Challenge Update FLow 3

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.

List of Power Automate Workflows

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 HackaLearn
  • 171: Total number of participants
  • 62: Total number of participants who completed Cloud Skills Challenge
  • 21: Total number of teams who uploaded social media posts
  • 17: Total number of teams who completed building Azure Static Web Apps
  • 16: Total number of teams who completed provided their GitHub repository
  • 20: Total number of teams who published their blog post as a retrospective
  • 13: 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

    👉 The review automation process should be as flexible as possible.

  • Make the review process as simple as possible

    👉 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

    👉 If a reviewer is assigned to a team, the chance for merge conflicts will dramatically decrease.

  • Make Power Automate workflows as modular as possible

    👉 There are many similar sequence of actions across many workflows. 👉 They can be modularised into either sub-workflows or custom APIs through custom connectors.

Side Events

During the event, we ran live hands-on workshop 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.