Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/sync_roadmap_status.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ on:
# (already in the event payload) and write to ROADMAP.md on main.
pull_request_target:
types: [opened, ready_for_review, closed]
# Run reconcile.py on demand ("Run workflow" button) and once a day
# as a safety net against any missed event (e.g. an issue closed
# before its ROADMAP.md line had "— #N" linked to it yet).
workflow_dispatch: {}
schedule:
- cron: "0 4 * * *"

permissions:
contents: write

jobs:
sync:
if: github.event_name == 'issues' || github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -39,6 +46,39 @@ jobs:
commit_message: "chore: sync roadmap status [skip ci]"
file_pattern: ROADMAP.md

- name: Sync Discord
if: steps.update.outputs.changed == 'true'
run: python scripts/roadmap/sync_discord.py
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
DISCORD_MESSAGE_IDS: ${{ vars.DISCORD_MESSAGE_IDS }}

reconcile:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Reconcile ROADMAP.md with issue states
id: update
run: python scripts/roadmap/reconcile_roadmap.py
env:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Commit ROADMAP.md
if: steps.update.outputs.changed == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: reconcile roadmap status [skip ci]"
file_pattern: ROADMAP.md

- name: Sync Discord
if: steps.update.outputs.changed == 'true'
run: python scripts/roadmap/sync_discord.py
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This may be subject to change.
1. Introduction
* [x] How course works?
* [x] Motivation and Mindset
* [ ] Asking for Help (WIP) — #32
* [ ] Asking for Help — #32
* [ ] Join the Community — #33
* [x] Computer Science basics, Programming and Python
2. Prerequisites
Expand Down
96 changes: 96 additions & 0 deletions scripts/roadmap/reconcile_roadmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Scans every issue already linked in ROADMAP.md ("— #N") and sets its
checkbox to match that issue's CURRENT state on GitHub — closed,
assigned, or neither.

Use this:
- once, right after linking a batch of issues, to catch anything that
changed *before* the link existed — sync_roadmap.py only reacts to
events on lines that already have "— #N" on them, so a PR merged
before you added the link is invisible to it
- on a schedule, as a safety net in case a future event is ever missed
(a failed workflow run, a webhook delivery hiccup, etc.)

Unlike sync_roadmap.py, this doesn't look at PR state directly — an
issue's own state (closed / has an assignee) is used as the signal.
That covers the common paths (assigned, merged-and-closed) even for
history that predates the automation.
"""

import json
import os
import sys
import urllib.error
import urllib.request

sys.path.insert(0, os.path.dirname(__file__))
from sync_roadmap import LINE_RE, ROADMAP_PATH, apply_updates # noqa: E402


def find_linked_issues() -> set[int]:
with open(ROADMAP_PATH, encoding="utf-8") as f:
lines = f.readlines()

issues = set()
for line in lines:
m = LINE_RE.match(line.rstrip("\n"))
if m:
issues.add(int(m.group("issue")))
return issues


def fetch_issue(repo: str, token: str, number: int) -> dict:
url = f"https://api.github.com/repos/{repo}/issues/{number}"
req = urllib.request.Request(
url,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
},
)
with urllib.request.urlopen(req) as resp:
return json.load(resp)


def target_state(issue: dict) -> str:
if issue.get("state") == "closed":
return "x"
if issue.get("assignees"):
return "-"
return " "


def main() -> None:
repo = os.environ["GITHUB_REPOSITORY"]
token = os.environ["GITHUB_TOKEN"]

issue_numbers = find_linked_issues()
if not issue_numbers:
print("No linked issues found in ROADMAP.md.")
return

updates: dict[int, str] = {}
for number in sorted(issue_numbers):
try:
issue = fetch_issue(repo, token, number)
except urllib.error.HTTPError as e:
print(
f"#{number}: could not fetch issue ({e.code}) — skipping",
file=sys.stderr,
)
continue
updates[number] = target_state(issue)

changed = apply_updates(updates)
if not changed:
print("Roadmap already matched issue states.")

github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(github_output, "a", encoding="utf-8") as f:
f.write(f"changed={'true' if changed else 'false'}\n")


if __name__ == "__main__":
main()
Loading