LGTM (40 Part Series)
1 LGTM Devlog 0: Teaching Git through playing a game
2 LGTM Devlog 1: New Logo, and jumping the gun by buying a domain
… 36 more parts…
3 LGTM Devlog 2: Design Ideas for the game I
4 LGTM Devlog 3: Design Ideas for the game 2
5 LGTM Devlog 4: Technical Decisions – GitHub API and Python
6 LGTM Devlog 5: Technical Decisions – Serverless Architecture
7 LGTM Devlog 6: Sprint 1 plans
8 LGTM Devlog 7: Creating a Firebase project
9 LGTM Devlog 8: Starting a git repo for an open source project from scratch
10 LGTM Devlog 9: Python Google Cloud Functions with Unit Tests and Linting
11 LGTM Devlog 10: Capturing the GitHub webhook for fork requests
12 LGTM Devlog 11: Writing the Serverless Function for receiving GitHub webhooks with Pydantic validation
13 LGTM Devlog 12: CI/CD with GitHub Actions to run Unit Tests and deploy Firebase Functions
14 LGTM Devlog 13: GitHub Branch Protection and Security
15 LGTM Devlog 14: Sprint 1 Retrospective
16 LGTM Devlog 15: Sprint 2 plans
17 LGTM Devlog 16: A serverless data base access rule conundrum
18 LGTM Devlog 17: Website and GitHub OAuth
19 LGTM Devlog 18: Python Serverless functions using GitHub API to validate users
20 LGTM Devlog 19: Game data/quest storage
21 LGTM Devlog 20: Python Abstract Base Class-based data/quest storage
22 LGTM Devlog 21: Deploying Pub/Sub-triggered Python Google Cloud Functions
23 LGTM Devlog 22: Modularization
24 LGTM Devlog 23: Sprint 2 Retrospective
25 LGTM Devlog 24: Sprint 3 plans
26 LGTM Devlog 25: Some cleanup
27 LGTM Devlog 26: Python Graphlib DAGs for Quest Stages
28 LGTM Devlog 27: Branching quests
29 LGTM Devlog 28: Game event loop using Google Cloud Scheduler and PubSub
30 LGTM Devlog 29: ORM for Firestore and __init__subclass__ dunders and metaclasses
31 LGTM Devlog 30: Sprint 3 Retrospective
32 LGTM Devlog 31: Sprint 4 Plan
33 LGTM Devlog 32: Secrets Management to avoid storing API keys in services
34 LGTM Devlog 33: Using PyGithub to post GitHub issues and comments
35 LGTM Devlog 34: Characters Posting on GitHub Issues
36 LGTM Devlog 35: Responding to the player’s answers on GitHub Issues Comments
37 LGTM Devlog 36: Character profiles! On GitHub
38 LGTM Devlog 37: Sprint 4 Retrospective
39 LGTM Devlog 38: Sprint 5 Plan
40 LGTM Devlog 39: Planning the story
Phew, it’s been a few days. I’ve done yet more refactoring, and am finally happy with how the quest definitions work. The code for this post is at 921090e
Stage execute loop
Stages now have pre-defined functions to execute, the execution looks like:
- Fetch quest stages that are ready to go from the quest graph
- Skip if already complete, and mark done
- Instantiate the stage
- run stage’s
prepare()
method. This is intended in case the stage needs to do any tasks to fetch the data it needs - run stage’s
condition()
to see whether the stage should execute. This method should decide whether the stage is ready to execute - If the condition returned True, then we can run
execute()
to do the task - Finally, check
is_done()
to decide whether this was a quest completion.
The actual code, which additionally handles a few edge-conditions.
<span>while</span> <span>self</span><span>.</span><span>graph</span><span>.</span><span>is_active</span><span>():</span><span>ready_nodes</span> <span>=</span> <span>self</span><span>.</span><span>graph</span><span>.</span><span>get_ready</span><span>()</span><span>if</span> <span>not</span> <span>ready_nodes</span><span>:</span><span>log</span><span>.</span><span>info</span><span>(</span><span>"No more ready nodes, stopping execution"</span><span>)</span><span>break</span><span>log</span><span>.</span><span>info</span><span>(</span><span>"Got Ready nodes"</span><span>,</span> <span>ready_nodes</span><span>=</span><span>ready_nodes</span><span>)</span><span>for</span> <span>node</span> <span>in</span> <span>ready_nodes</span><span>:</span><span># skip if completed, avoids triggering two final stages </span> <span>if</span> <span>self</span><span>.</span><span>complete</span><span>:</span><span>log</span><span>.</span><span>info</span><span>(</span><span>"Done flag set, skipping the rest"</span><span>)</span><span>return</span><span># completed node: TODO: just not put completed nodes into the graph? </span> <span>if</span> <span>node</span> <span>in</span> <span>self</span><span>.</span><span>completed_stages</span><span>:</span><span>self</span><span>.</span><span>graph</span><span>.</span><span>done</span><span>(</span><span>node</span><span>)</span><span>log</span><span>.</span><span>info</span><span>(</span><span>"Node is already complete, skipping"</span><span>,</span><span>node</span><span>=</span><span>node</span><span>,</span><span>complete</span><span>=</span><span>self</span><span>.</span><span>completed_stages</span><span>,</span><span>)</span><span>continue</span><span>log_node</span> <span>=</span> <span>log</span><span>.</span><span>bind</span><span>(</span><span>node</span><span>=</span><span>node</span><span>)</span><span>log_node</span><span>.</span><span>info</span><span>(</span><span>"Begin processing stage"</span><span>)</span><span># instantiate stage and execute </span> <span>StageClass</span> <span>=</span> <span>self</span><span>.</span><span>stages</span><span>[</span><span>node</span><span>]</span><span>stage</span> <span>=</span> <span>StageClass</span><span>(</span><span>self</span><span>)</span><span>stage</span><span>.</span><span>prepare</span><span>()</span><span>if</span> <span>stage</span><span>.</span><span>condition</span><span>():</span><span>log_node</span><span>.</span><span>info</span><span>(</span><span>"Condition check passed, executing"</span><span>)</span><span>stage</span><span>.</span><span>execute</span><span>()</span><span>if</span> <span>stage</span><span>.</span><span>is_done</span><span>():</span><span>log_node</span><span>.</span><span>info</span><span>(</span><span>"Stage reports done"</span><span>)</span><span>self</span><span>.</span><span>completed_stages</span><span>.</span><span>append</span><span>(</span><span>node</span><span>)</span><span>self</span><span>.</span><span>graph</span><span>.</span><span>done</span><span>(</span><span>node</span><span>)</span><span>log</span><span>.</span><span>info</span><span>(</span><span>"Done processing node"</span><span>)</span><span>while</span> <span>self</span><span>.</span><span>graph</span><span>.</span><span>is_active</span><span>():</span> <span>ready_nodes</span> <span>=</span> <span>self</span><span>.</span><span>graph</span><span>.</span><span>get_ready</span><span>()</span> <span>if</span> <span>not</span> <span>ready_nodes</span><span>:</span> <span>log</span><span>.</span><span>info</span><span>(</span><span>"No more ready nodes, stopping execution"</span><span>)</span> <span>break</span> <span>log</span><span>.</span><span>info</span><span>(</span><span>"Got Ready nodes"</span><span>,</span> <span>ready_nodes</span><span>=</span><span>ready_nodes</span><span>)</span> <span>for</span> <span>node</span> <span>in</span> <span>ready_nodes</span><span>:</span> <span># skip if completed, avoids triggering two final stages </span> <span>if</span> <span>self</span><span>.</span><span>complete</span><span>:</span> <span>log</span><span>.</span><span>info</span><span>(</span><span>"Done flag set, skipping the rest"</span><span>)</span> <span>return</span> <span># completed node: TODO: just not put completed nodes into the graph? </span> <span>if</span> <span>node</span> <span>in</span> <span>self</span><span>.</span><span>completed_stages</span><span>:</span> <span>self</span><span>.</span><span>graph</span><span>.</span><span>done</span><span>(</span><span>node</span><span>)</span> <span>log</span><span>.</span><span>info</span><span>(</span> <span>"Node is already complete, skipping"</span><span>,</span> <span>node</span><span>=</span><span>node</span><span>,</span> <span>complete</span><span>=</span><span>self</span><span>.</span><span>completed_stages</span><span>,</span> <span>)</span> <span>continue</span> <span>log_node</span> <span>=</span> <span>log</span><span>.</span><span>bind</span><span>(</span><span>node</span><span>=</span><span>node</span><span>)</span> <span>log_node</span><span>.</span><span>info</span><span>(</span><span>"Begin processing stage"</span><span>)</span> <span># instantiate stage and execute </span> <span>StageClass</span> <span>=</span> <span>self</span><span>.</span><span>stages</span><span>[</span><span>node</span><span>]</span> <span>stage</span> <span>=</span> <span>StageClass</span><span>(</span><span>self</span><span>)</span> <span>stage</span><span>.</span><span>prepare</span><span>()</span> <span>if</span> <span>stage</span><span>.</span><span>condition</span><span>():</span> <span>log_node</span><span>.</span><span>info</span><span>(</span><span>"Condition check passed, executing"</span><span>)</span> <span>stage</span><span>.</span><span>execute</span><span>()</span> <span>if</span> <span>stage</span><span>.</span><span>is_done</span><span>():</span> <span>log_node</span><span>.</span><span>info</span><span>(</span><span>"Stage reports done"</span><span>)</span> <span>self</span><span>.</span><span>completed_stages</span><span>.</span><span>append</span><span>(</span><span>node</span><span>)</span> <span>self</span><span>.</span><span>graph</span><span>.</span><span>done</span><span>(</span><span>node</span><span>)</span> <span>log</span><span>.</span><span>info</span><span>(</span><span>"Done processing node"</span><span>)</span>while self.graph.is_active(): ready_nodes = self.graph.get_ready() if not ready_nodes: log.info("No more ready nodes, stopping execution") break log.info("Got Ready nodes", ready_nodes=ready_nodes) for node in ready_nodes: # skip if completed, avoids triggering two final stages if self.complete: log.info("Done flag set, skipping the rest") return # completed node: TODO: just not put completed nodes into the graph? if node in self.completed_stages: self.graph.done(node) log.info( "Node is already complete, skipping", node=node, complete=self.completed_stages, ) continue log_node = log.bind(node=node) log_node.info("Begin processing stage") # instantiate stage and execute StageClass = self.stages[node] stage = StageClass(self) stage.prepare() if stage.condition(): log_node.info("Condition check passed, executing") stage.execute() if stage.is_done(): log_node.info("Stage reports done") self.completed_stages.append(node) self.graph.done(node) log.info("Done processing node")
Enter fullscreen mode Exit fullscreen mode
Running this loop will cycle through all of the processable parts of the quest tree, including nodes that become available only after the previous node is complete.
The Stage
Abstract Base Class now looks like:
<span>class</span> <span>Stage</span><span>(</span><span>ABC</span><span>):</span><span>@</span><span>property</span><span>@</span><span>abstractmethod</span><span>def</span> <span>children</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>List</span><span>[</span><span>str</span><span>]:</span><span>""" List of children nodes of this stage """</span><span>return</span> <span>NotImplemented</span><span>def</span> <span>prepare</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span><span>""" Any preparation for the stage """</span><span>return</span><span>def</span> <span>condition</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span><span>""" Function that will decide whether to execute """</span><span>return</span> <span>True</span><span>def</span> <span>execute</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span><span>""" Run the stage """</span><span>return</span><span>def</span> <span>is_done</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span><span>""" Returns whether quest was completed """</span><span>return</span> <span>True</span><span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>quest</span><span>:</span> <span>Quest</span><span>):</span><span>self</span><span>.</span><span>quest</span> <span>=</span> <span>quest</span><span>def</span> <span>__repr__</span><span>(</span><span>self</span><span>):</span><span>return</span> <span>f</span><span>"</span><span>{</span><span>self</span><span>.</span><span>__class__</span><span>.</span><span>__name__</span><span>}</span><span>(quest=</span><span>{</span><span>repr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>)</span><span>}</span><span>)"</span><span>class</span> <span>Stage</span><span>(</span><span>ABC</span><span>):</span> <span>@</span><span>property</span> <span>@</span><span>abstractmethod</span> <span>def</span> <span>children</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>List</span><span>[</span><span>str</span><span>]:</span> <span>""" List of children nodes of this stage """</span> <span>return</span> <span>NotImplemented</span> <span>def</span> <span>prepare</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span> <span>""" Any preparation for the stage """</span> <span>return</span> <span>def</span> <span>condition</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span> <span>""" Function that will decide whether to execute """</span> <span>return</span> <span>True</span> <span>def</span> <span>execute</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span> <span>""" Run the stage """</span> <span>return</span> <span>def</span> <span>is_done</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span> <span>""" Returns whether quest was completed """</span> <span>return</span> <span>True</span> <span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>quest</span><span>:</span> <span>Quest</span><span>):</span> <span>self</span><span>.</span><span>quest</span> <span>=</span> <span>quest</span> <span>def</span> <span>__repr__</span><span>(</span><span>self</span><span>):</span> <span>return</span> <span>f</span><span>"</span><span>{</span><span>self</span><span>.</span><span>__class__</span><span>.</span><span>__name__</span><span>}</span><span>(quest=</span><span>{</span><span>repr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>)</span><span>}</span><span>)"</span>class Stage(ABC): @property @abstractmethod def children(cls) -> List[str]: """ List of children nodes of this stage """ return NotImplemented def prepare(self) -> None: """ Any preparation for the stage """ return def condition(self) -> bool: """ Function that will decide whether to execute """ return True def execute(self) -> None: """ Run the stage """ return def is_done(self) -> bool: """ Returns whether quest was completed """ return True def __init__(self, quest: Quest): self.quest = quest def __repr__(self): return f"{self.__class__.__name__}(quest={repr(self.quest)})"
Enter fullscreen mode Exit fullscreen mode
As can be seen, only the children
property is required to implement; by default condition()
and is_done()
returns True if not overridden.
The reason there’s both a condition()
and an is_done()
is take for example a quest that looks like this:
- Quest asks player to find some information and reply in a comment
-
condition()
decides whether this quest stage should fetch the comment (from some notification trigger?) -
execute()
fetches the data, checks the value, and then sends a reply saying “Yes that was it” or “No, that’s not right, try again” - In the latter case,
is_done()
returns False, and the player doesn’t progress, and can make another attempt; in the former case,is_done()
returns True, and the player progresses to the next stage
Conditions
I’ve added a new kind of stage called a ConditionStage
which lets us handle conditional execution of a stage. Without it, we can branch quests, but we can’t control whether a branch will execute. It overrides the base class’s condition()
method, and allows a quest definition to specify which data from the quest datastructure to check:
class ConditionStage(Stage):""" For conditional branch execution """@property@abstractmethoddef variable(cls) -> str:""" Name of the variable to check """return NotImplemented# the variable to check againstcompare_variable: ClassVar[Optional[str]] = None# the value to check against, if compare_variable is Nonecompare_value: ClassVar[Any] = None# the operator to use comparison onoperator: ClassVar[Callable[..., bool]] = operator.eqdef condition(self) -> bool:value_left = getattr(self.quest.quest_data, self.variable)if self.compare_variable is not None:value_right = getattr(self.quest.quest_data, self.compare_variable)else:value_right = self.compare_valuereturn self.operator(value_left, value_right)class ConditionStage(Stage): """ For conditional branch execution """ @property @abstractmethod def variable(cls) -> str: """ Name of the variable to check """ return NotImplemented # the variable to check against compare_variable: ClassVar[Optional[str]] = None # the value to check against, if compare_variable is None compare_value: ClassVar[Any] = None # the operator to use comparison on operator: ClassVar[Callable[..., bool]] = operator.eq def condition(self) -> bool: value_left = getattr(self.quest.quest_data, self.variable) if self.compare_variable is not None: value_right = getattr(self.quest.quest_data, self.compare_variable) else: value_right = self.compare_value return self.operator(value_left, value_right)class ConditionStage(Stage): """ For conditional branch execution """ @property @abstractmethod def variable(cls) -> str: """ Name of the variable to check """ return NotImplemented # the variable to check against compare_variable: ClassVar[Optional[str]] = None # the value to check against, if compare_variable is None compare_value: ClassVar[Any] = None # the operator to use comparison on operator: ClassVar[Callable[..., bool]] = operator.eq def condition(self) -> bool: value_left = getattr(self.quest.quest_data, self.variable) if self.compare_variable is not None: value_right = getattr(self.quest.quest_data, self.compare_variable) else: value_right = self.compare_value return self.operator(value_left, value_right)
Enter fullscreen mode Exit fullscreen mode
A concrete implementation of the class looks like:
<span>class</span> <span>TestQuestBranching</span><span>(</span><span>Quest</span><span>):</span><span>class</span> <span>QuestDataModel</span><span>(</span><span>QuestBaseModel</span><span>):</span><span>value_a</span><span>:</span> <span>int</span> <span>=</span> <span>1</span><span>value_b</span><span>:</span> <span>int</span> <span>=</span> <span>2</span><span>version</span> <span>=</span> <span>VersionInfo</span><span>.</span><span>parse</span><span>(</span><span>"1.0.0"</span><span>)</span><span>difficulty</span> <span>=</span> <span>Difficulty</span><span>.</span><span>RESERVED</span><span>description</span> <span>=</span> <span>"This is a quest to test branching"</span><span>class</span> <span>Start</span><span>(</span><span>DebugStage</span><span>):</span><span>children</span> <span>=</span> <span>[</span><span>"BranchA"</span><span>,</span> <span>"BranchB"</span><span>]</span><span>class</span> <span>BranchA</span><span>(</span><span>ConditionStage</span><span>):</span><span>children</span> <span>=</span> <span>[</span><span>"EndingA"</span><span>]</span><span>variable</span> <span>=</span> <span>"value_a"</span><span>compare_variable</span> <span>=</span> <span>"value_b"</span><span>class</span> <span>BranchB</span><span>(</span><span>ConditionStage</span><span>):</span><span>children</span> <span>=</span> <span>[</span><span>"EndingB"</span><span>]</span><span>variable</span> <span>=</span> <span>"value_a"</span><span>operator</span> <span>=</span> <span>operator</span><span>.</span><span>gt</span><span>compare_value</span> <span>=</span> <span>10</span><span>class</span> <span>EndingA</span><span>(</span><span>FinalStage</span><span>):</span><span>children</span> <span>=</span> <span>[]</span><span>class</span> <span>EndingB</span><span>(</span><span>FinalStage</span><span>):</span><span>children</span> <span>=</span> <span>[]</span><span>class</span> <span>TestQuestBranching</span><span>(</span><span>Quest</span><span>):</span> <span>class</span> <span>QuestDataModel</span><span>(</span><span>QuestBaseModel</span><span>):</span> <span>value_a</span><span>:</span> <span>int</span> <span>=</span> <span>1</span> <span>value_b</span><span>:</span> <span>int</span> <span>=</span> <span>2</span> <span>version</span> <span>=</span> <span>VersionInfo</span><span>.</span><span>parse</span><span>(</span><span>"1.0.0"</span><span>)</span> <span>difficulty</span> <span>=</span> <span>Difficulty</span><span>.</span><span>RESERVED</span> <span>description</span> <span>=</span> <span>"This is a quest to test branching"</span> <span>class</span> <span>Start</span><span>(</span><span>DebugStage</span><span>):</span> <span>children</span> <span>=</span> <span>[</span><span>"BranchA"</span><span>,</span> <span>"BranchB"</span><span>]</span> <span>class</span> <span>BranchA</span><span>(</span><span>ConditionStage</span><span>):</span> <span>children</span> <span>=</span> <span>[</span><span>"EndingA"</span><span>]</span> <span>variable</span> <span>=</span> <span>"value_a"</span> <span>compare_variable</span> <span>=</span> <span>"value_b"</span> <span>class</span> <span>BranchB</span><span>(</span><span>ConditionStage</span><span>):</span> <span>children</span> <span>=</span> <span>[</span><span>"EndingB"</span><span>]</span> <span>variable</span> <span>=</span> <span>"value_a"</span> <span>operator</span> <span>=</span> <span>operator</span><span>.</span><span>gt</span> <span>compare_value</span> <span>=</span> <span>10</span> <span>class</span> <span>EndingA</span><span>(</span><span>FinalStage</span><span>):</span> <span>children</span> <span>=</span> <span>[]</span> <span>class</span> <span>EndingB</span><span>(</span><span>FinalStage</span><span>):</span> <span>children</span> <span>=</span> <span>[]</span>class TestQuestBranching(Quest): class QuestDataModel(QuestBaseModel): value_a: int = 1 value_b: int = 2 version = VersionInfo.parse("1.0.0") difficulty = Difficulty.RESERVED description = "This is a quest to test branching" class Start(DebugStage): children = ["BranchA", "BranchB"] class BranchA(ConditionStage): children = ["EndingA"] variable = "value_a" compare_variable = "value_b" class BranchB(ConditionStage): children = ["EndingB"] variable = "value_a" operator = operator.gt compare_value = 10 class EndingA(FinalStage): children = [] class EndingB(FinalStage): children = []
Enter fullscreen mode Exit fullscreen mode
Here, we have a quest that branches off into two, with two separate endings. The condition of the first branch is value_a
== value_b
, while the condition of the second is value_a
> 10, making use of python’s built-in operator
library to provide comparison methods like operator.gt(a, b)
.
FinalStage
Finally, we need a way to mark quests as complete. Nodes with no children
will simply not trigger a next stage, but might not signify the end of a quest (they may be a dead-end branch). So the FinalStage
concrete implementation of a Stage
simply sets the quests’s complete
boolean to True, (I’ve refactored quest storage to include this as part of the quest data model:
<span>class</span> <span>FinalStage</span><span>(</span><span>Stage</span><span>):</span><span>""" For ending the quest """</span><span>def</span> <span>__init_subclass__</span><span>(</span><span>cls</span><span>):</span><span>cls</span><span>.</span><span>children</span> <span>=</span> <span>[]</span><span>def</span> <span>execute</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>complete</span> <span>=</span> <span>True</span><span>class</span> <span>FinalStage</span><span>(</span><span>Stage</span><span>):</span> <span>""" For ending the quest """</span> <span>def</span> <span>__init_subclass__</span><span>(</span><span>cls</span><span>):</span> <span>cls</span><span>.</span><span>children</span> <span>=</span> <span>[]</span> <span>def</span> <span>execute</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span> <span>self</span><span>.</span><span>quest</span><span>.</span><span>complete</span> <span>=</span> <span>True</span>class FinalStage(Stage): """ For ending the quest """ def __init_subclass__(cls): cls.children = [] def execute(self) -> None: self.quest.complete = True
Enter fullscreen mode Exit fullscreen mode
With these concrete implementations of different loops, it’s now possible to have quests that require conditions to be fulfilled, and mark done.
LGTM (40 Part Series)
1 LGTM Devlog 0: Teaching Git through playing a game
2 LGTM Devlog 1: New Logo, and jumping the gun by buying a domain
… 36 more parts…
3 LGTM Devlog 2: Design Ideas for the game I
4 LGTM Devlog 3: Design Ideas for the game 2
5 LGTM Devlog 4: Technical Decisions – GitHub API and Python
6 LGTM Devlog 5: Technical Decisions – Serverless Architecture
7 LGTM Devlog 6: Sprint 1 plans
8 LGTM Devlog 7: Creating a Firebase project
9 LGTM Devlog 8: Starting a git repo for an open source project from scratch
10 LGTM Devlog 9: Python Google Cloud Functions with Unit Tests and Linting
11 LGTM Devlog 10: Capturing the GitHub webhook for fork requests
12 LGTM Devlog 11: Writing the Serverless Function for receiving GitHub webhooks with Pydantic validation
13 LGTM Devlog 12: CI/CD with GitHub Actions to run Unit Tests and deploy Firebase Functions
14 LGTM Devlog 13: GitHub Branch Protection and Security
15 LGTM Devlog 14: Sprint 1 Retrospective
16 LGTM Devlog 15: Sprint 2 plans
17 LGTM Devlog 16: A serverless data base access rule conundrum
18 LGTM Devlog 17: Website and GitHub OAuth
19 LGTM Devlog 18: Python Serverless functions using GitHub API to validate users
20 LGTM Devlog 19: Game data/quest storage
21 LGTM Devlog 20: Python Abstract Base Class-based data/quest storage
22 LGTM Devlog 21: Deploying Pub/Sub-triggered Python Google Cloud Functions
23 LGTM Devlog 22: Modularization
24 LGTM Devlog 23: Sprint 2 Retrospective
25 LGTM Devlog 24: Sprint 3 plans
26 LGTM Devlog 25: Some cleanup
27 LGTM Devlog 26: Python Graphlib DAGs for Quest Stages
28 LGTM Devlog 27: Branching quests
29 LGTM Devlog 28: Game event loop using Google Cloud Scheduler and PubSub
30 LGTM Devlog 29: ORM for Firestore and __init__subclass__ dunders and metaclasses
31 LGTM Devlog 30: Sprint 3 Retrospective
32 LGTM Devlog 31: Sprint 4 Plan
33 LGTM Devlog 32: Secrets Management to avoid storing API keys in services
34 LGTM Devlog 33: Using PyGithub to post GitHub issues and comments
35 LGTM Devlog 34: Characters Posting on GitHub Issues
36 LGTM Devlog 35: Responding to the player’s answers on GitHub Issues Comments
37 LGTM Devlog 36: Character profiles! On GitHub
38 LGTM Devlog 37: Sprint 4 Retrospective
39 LGTM Devlog 38: Sprint 5 Plan
40 LGTM Devlog 39: Planning the story
暂无评论内容