DEV Community

Cover image for LGTM Devlog 34: Characters Posting on GitHub Issues
Yuan Gao
Yuan Gao

Posted on

LGTM Devlog 34: Characters Posting on GitHub Issues

Some big changes today as multiple things are coming together! Most of the work is now in the stages module, the code for this pots is at commit 0e67f0d

Delay stages

I've decided to instead of using a task queue system for timed events, simply add a DelayStage quest stage that enacts a time delay. This reduces the complexity by knocking out a piece of infrastructure, and was surprisingly easy to write given the existing infrastructure:

class DelayStage(Stage):
    """ For enacting a time delay """

    @property
    @abstractmethod
    def delay(cls) -> timedelta:
        """ how much time to delay """
        return NotImplemented

    def prepare(self) -> None:
        """ On first run, insert datetime """
        if self.get_stage_data() is None:
            now = datetime.now().timestamp()
            logger.info(f"Delay stage prepare", now=now)
            self.set_stage_data(now)

    def condition(self) -> bool:
        """ Calculate whether that has elapsed """
        target = datetime.utcfromtimestamp(self.get_stage_data(0)) + self.delay
        now = datetime.now()
        logger.info(f"Delay stage prepare", now=now, target=target)
        return now > target
Enter fullscreen mode Exit fullscreen mode

The DelayStage uses the prepare() event to write into its stage storage (a new free-form dict value that I've added to the quest data storage model to store stage-specific data) the target timestamp, and when the condition runs next, it tests if this time has been reached or exceeded! Very simple.

Creating Issues

Now that we have the code for characters to post github messages, I can add a CreateIssueStage() that will create a new issue in a user's fork, it looks like this:


class CreateIssueStage(Stage):
    """ This stage posts a new issue to a user's fork """

    @property
    @abstractmethod
    def character(cls) -> Character:
        """ Which character will post the issue """
        return NotImplemented

    @property
    @abstractmethod
    def issue_title(cls) -> str:
        """ Issue title """
        return NotImplemented

    @property
    @abstractmethod
    def issue_body(cls) -> str:
        """ Issue main body """
        return NotImplemented

    # variable to store resulting issue ID in for later
    issue_id_variable: Optional[int] = None

    def execute(self) -> None:
        """ Post the issue to the fork """
        game = self.quest.quest_page.game
        game.load()
        fork_url = game.data.fork_url

        logger.info("Creating issue", title=self.issue_title, fork_url=fork_url)
        issue_id = self.character.issue_create(
            fork_url, self.issue_title, self.issue_body
        )

        if self.issue_id_variable:
            logger.info(
                "Storing issue Id in variable",
                issue_id=issue_id,
                variable=self.issue_id_variable,
            )
            setattr(self.quest.quest_data, self.issue_id_variable, issue_id)
Enter fullscreen mode Exit fullscreen mode

It has some abstract properties for the character, issue title, and so on; and the business part of the class simply calls character.isseu_create() that's part of the package defined last time. The rest of the execute() event deals with fetching the fork_url, and saving the results in a location of your choosing, so that a stage can output the issue ID created so that other stages can use it.

In usage, it looks like this:

class IntroQuest(Quest):
    class QuestDataModel(QuestBaseModel):
        issue_id: Optional[str] = None
        last_comment: Optional[datetime] = None

    version = VersionInfo.parse("0.1.0")
    difficulty = Difficulty.BEGINNER
    description = "The intro quest"

    class Start(CreateIssueStage):
        children = ["CheckNumber1"]
        character = character_garry
        issue_id_variable = "issue_id"
        issue_title = "Welcome, can you give me a hand?"
        issue_body = """\
            Hello therre!
        """
Enter fullscreen mode Exit fullscreen mode

The reason for the """\ is due to the use of pythons dedent method which keeps intedentation nicer in the source code for multi-line strings.

When this Start event fires, it will use the character defined by character_garry to send an issue to the player's fork, with the given issue title and body. It will also store the issue_id in the quest data model's issue_id key, so that next stages can re-use it.

Testing

In order to test, to avoid spamming data somewhere public, I've over-written the test files to fake a fork to a private github repo instead. This usually isn't possible on GitHub during a normal flow, but our system doesn't make a distinction, so happily accepts it as part of a test.

Test issues

I can run the tests already in the github_webhook_listener to test the creation of this first issue since it already starts by deleting the test user's game and starting a new one. I can also test follow-on stages using the tests already defined in test_main_tick

Discussion (0)