Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secure Secrets for Generalized Continuous Deploys #1

Closed
0x4007 opened this issue Feb 24, 2024 · 26 comments · Fixed by ubiquity/ts-template#32, #3, ubiquity/ts-template#35, ubiquity/onboard.ubq.fi#15 or ubiquity/.github#101

Comments

@0x4007
Copy link
Member

0x4007 commented Feb 24, 2024

Context

I spent quite a bit of time cleaning up the code and trying to make the deploys more general purpose so that we can include in our @ubiquity/ts-template to make it seamless for contributors to open pull requests and for our reviewers to be able to review their work. I severely underestimated the amount of time that this would take. However:

  • I finally got it stable and fairly fast (I removed unnecessary steps) over the past few days.
  • Here's a test on a pull request: ci: test ci/cd work.ubq.fi#16 (comment).
    • I also fixed the details around commit hashes posting in chronological order, which used to be an issue.
  • Specifically the work is done in build.yml and then its passed along to this current repository (@ubiquity/cloudflare-deploy-action) starting with action.yml in the root.
  • From there, it goes to one script for dealing with cloudflare: cloudflare-deploy.sh
  • And then finally it passes the output (the deployed URL) to the node script in src/index.ts which posts the comment as the bot to the pull request (or commit - but this is commented out now.)

As you can see I was struggling with passing secrets around. I decided to review the old implementation and realized that the clever workaround to expose the secrets was to invoke the action "internally" from the successful completion of a previous action1, as seen here.

I spent a lot more time than I anticipated on this project, and unfortunately I'm starting to run out of development time leading up to 1 March and I still have several other matters I need to tend to. Unfortunately I did not follow best practices and exposed all of what should be secrets as plaintext and hardcoded in the scripts.

Notes

  • I created a dedicated Cloudflare account specifically for our automatic deployments across all Ubiquity repositories. The API key that is used is tightly scoped just to create and edit Workers deployments. This isn't really a security concern because my updated deploy logic will only deploy to production if its merged to the default branch (for most of our repositories its development but sometimes main - its simple to change in the repository settings) otherwise it will just make a preview on a unique cloudflare URL.
  • I created a dedicated GitHub App (Ubiquibot Continuous Deploys) with minimal permissions to just be able to add comments on pull requests and commits for posting the deploys.

I don't think its fatal that these credentials are leaked, but it obviously is not following best practices.

Objectives

  1. Update @ubiquity/ts-template with the correct action initiators (init.yml, then build.yml so that it can have access to secrets)
    • I think init.yml would be a dummy action; or to be smart we could make it handle conventional-commits and then run the build. All secrets should be available in build.yml so that it can apply our i.e. SUPABASE_ANON_KEY and SUPABASE_URL in the build. That way the reviewers can see the built (and functional) app easily.
  2. Update @ubiquity/cloudflare-deploy-action to handle the event coming in from build.yml. Now it should be privleged and be able to read our secrets normally. Change my code to use the secrets instead of the hardcoded plaintext values.
  3. Rotate the credentials.

Reference

Here is the order of the old setup.

  1. https://github.com/ubiquity/pay.ubq.fi/blob/ca90b265210fe953d942387791a52bca879b538c/.github/workflows/build.yml
  2. https://github.com/ubiquity/pay.ubq.fi/blob/a57f08925a2fa10c756d036b0e5aeb7b54cf81b0/.github/workflows/continuous-deploy.yml
  3. https://github.com/ubiquity/.github/blob/main/.github/workflows/deploy.yml

Final Remarks

I was able to make it stable without using download/upload artifact; and without using checkout actions. We should try not using these because if I recall correctly, they were the biggest bottlenecks for the slowdowns in the runs. Right now the biggest bottleneck that I couldn't' really get around is the yarn install for running node. I think its fine to leave in.

There was some bug with the wrangler CLI that I couldn't verify if the project already existed remotely. I must have been doing something wrong but I spent so many hours on it that I just decided to replace it with a curl.

Footnotes

  1. As explained plainly by ChatGPT: https://chat.openai.com/share/0455a2fe-1dd4-483b-86e5-b05e025ad485

@0x4007
Copy link
Member Author

0x4007 commented Feb 24, 2024

@rndquu please spend some time on this so that we can standardize deploys across all of our future projects via ts-template. I would appreciate if you can address this within the next couple of weeks.

Copy link

ubiquibot bot commented Feb 24, 2024

@rndquu the deadline is at 2024-02-25T20:26:19.445Z

@0x4007
Copy link
Member Author

0x4007 commented Feb 25, 2024

Related: ubiquity/ts-template#10

@rndquu
Copy link
Member

rndquu commented Feb 28, 2024

I created a dedicated Cloudflare account specifically for our automatic deployments across all Ubiquity repositories.

I have access to the Ubiquity DAO Workers cloudflare account. Is it the one you're talking about?

I created a dedicated GitHub App (Ubiquibot Continuous Deploys) with minimal permissions to just be able to add comments on pull requests and commits for posting the deploys.

Can't find it here https://github.com/marketplace?category=&type=&verification=&query=Ubiquibot+Continuous+Deploys

@0x4007
Copy link
Member Author

0x4007 commented Feb 28, 2024

I created a dedicated Cloudflare account specifically for our automatic deployments across all Ubiquity repositories.

I have access to the Ubiquity DAO Workers cloudflare account. Is it the one you're talking about?

Yes

I created a dedicated GitHub App (Ubiquibot Continuous Deploys) with minimal permissions to just be able to add comments on pull requests and commits for posting the deploys.

Can't find it here https://github.com/marketplace?category=&type=&verification=&query=Ubiquibot+Continuous+Deploys

It's not public. It's the one posting all the continuous deploys now. It's called ubiquibot-continuous-deploys

@0x4007
Copy link
Member Author

0x4007 commented Mar 2, 2024

Hey just following up here.

I made some special logic for if we create a new repository named *.ubq.fi, leveraging our continuous deploys, it will automatically map the subdomain live on to *.ubq.fi

It would be nice for the keys to be secured or else there will be some manual cleanup required in the Ubiquity DAO Workers account for any pollution of the namespace.

@rndquu
Copy link
Member

rndquu commented Mar 3, 2024

Hey just following up here.

I made some special logic for if we create a new repository named *.ubq.fi, leveraging our continuous deploys, it will automatically map the subdomain live on to *.ubq.fi

It would be nice for the keys to be secured or else there will be some manual cleanup required in the Ubiquity DAO Workers account for any pollution of the namespace.

Hey, I remember about this task, I'll check it this week

@0x4007
Copy link
Member Author

0x4007 commented Mar 3, 2024

I just realized that these will deploy to our account even from forks that are not related to Ubiquity. https://github.com/pavlovcik/ubiquity-dollar/actions/runs/8129516826/job/22216713105

I added the template CI in my fork of the Dollar and it ended up on Cloudflare. In the specified process, when its a fork opening up to our organization that should naturally fix this issue.

image

@rndquu
Copy link
Member

rndquu commented Mar 4, 2024

I just realized that these will deploy to our account even from forks that are not related to Ubiquity. https://github.com/pavlovcik/ubiquity-dollar/actions/runs/8129516826/job/22216713105

I added the template CI in my fork of the Dollar and it ended up on Cloudflare. In the specified process, when its a fork opening up to our organization that should naturally fix this issue.

image

So what's the expected behaviour? When PR is from a fork then we deploy to our own account, right?

@0x4007
Copy link
Member Author

0x4007 commented Mar 4, 2024

This is intended to facilitate our reviewers by posting the deployments on the pull request. So it should only run in the context of a pull request opened against the Ubiquity organization's repositories.

Copy link

ubiquibot bot commented Mar 10, 2024

+ Evaluating results. Please wait...

Copy link

ubiquibot bot commented Mar 10, 2024

[ 243.8 WXDAI ]

@pavlovcik
Contributions Overview
ViewContributionCountReward
IssueSpecification1190.4
IssueComment653.4
Conversation Incentives
CommentFormattingRelevanceReward
### Context

I spent quite a bit of time cleaning up the code ...

190.4

h3:
  count: 5
  score: "5"
  words: 6
a:
  count: 6
  score: "6"
  words: 16
li:
  count: 15
  score: "15"
  words: 450
code:
  count: 17
  score: "17"
  words: 34
1190.4
@rndquu please spend some time on this so that we can standardiz...
8.2
code:
  count: 1
  score: "1"
  words: 2
0.8558.2
Related: https://github.com/ubiquity/ts-template/issues/10...
1.80.7651.8
> > I created a dedicated Cloudflare account specifically for ou...
5.2
code:
  count: 1
  score: "1"
  words: 3
0.615.2
Hey just following up here.

I made some special logic for if...

14.2

code:
  count: 1
  score: "1"
  words: 3
0.7114.2
I just realized that these will deploy to our account even from ...
17.20.82517.2
This is intended to facilitate our reviewers by posting the depl...
6.80.796.8

[ 608.6 WXDAI ]

@rndquu
Contributions Overview
ViewContributionCountReward
IssueTask1600
IssueComment30
IssueComment38.6
Conversation Incentives
CommentFormattingRelevanceReward
> I created a dedicated Cloudflare account specifically for our ...
-
code:
  count: 1
  score: "0"
  words: 3
0.79-
> Hey just following up here. > > I made some special logic f...
-
code:
  count: 1
  score: "0"
  words: 3
0.765-
> I just realized that these will deploy to our account even fro...
-0.68-
> I created a dedicated Cloudflare account specifically for our ...
4.4
code:
  count: 1
  score: "1"
  words: 3
0.794.4
> Hey just following up here. > > I made some special logic f...
2.2
code:
  count: 1
  score: "1"
  words: 3
0.7652.2
> I just realized that these will deploy to our account even fro...
20.682

@rndquu rndquu reopened this Mar 10, 2024
@ubiquibot ubiquibot bot unassigned rndquu Mar 10, 2024
@rndquu rndquu self-assigned this Mar 10, 2024
Copy link

ubiquibot bot commented Mar 10, 2024

@rndquu the deadline is at 2024-03-11T20:24:50.917Z

Copy link

ubiquibot bot commented Mar 13, 2024

+ Evaluating results. Please wait...

Copy link

ubiquibot bot commented Mar 13, 2024

[ 250.4 WXDAI ]

@pavlovcik
Contributions Overview
ViewContributionCountReward
IssueSpecification1190.4
IssueComment653.4
ReviewComment16.6
Conversation Incentives
CommentFormattingRelevanceReward
### Context

I spent quite a bit of time cleaning up the code ...

190.4

h3:
  count: 5
  score: "5"
  words: 6
a:
  count: 6
  score: "6"
  words: 16
li:
  count: 15
  score: "15"
  words: 450
code:
  count: 17
  score: "17"
  words: 34
1190.4
@rndquu please spend some time on this so that we can standardiz...
8.2
code:
  count: 1
  score: "1"
  words: 2
0.868.2
Related: https://github.com/ubiquity/ts-template/issues/10...
1.80.771.8
> > I created a dedicated Cloudflare account specifically for ou...
5.2
code:
  count: 1
  score: "1"
  words: 3
0.745.2
Hey just following up here.

I made some special logic for if...

14.2

code:
  count: 1
  score: "1"
  words: 3
0.6714.2
I just realized that these will deploy to our account even from ...
17.20.7217.2
This is intended to facilitate our reviewers by posting the depl...
6.80.836.8
Can you also update `ts-template` with any necessary changes, pr...
6.6
code:
  count: 2
  score: "4"
  words: 4
0.766.6

[ 614.2 WXDAI ]

@rndquu
Contributions Overview
ViewContributionCountReward
IssueTask1600
IssueComment30
IssueComment38.6
ReviewComment12.8
ReviewComment12.8
Conversation Incentives
CommentFormattingRelevanceReward
> I created a dedicated Cloudflare account specifically for our ...
-
code:
  count: 1
  score: "0"
  words: 3
0.84-
> Hey just following up here. > > I made some special logic f...
-
code:
  count: 1
  score: "0"
  words: 3
0.68-
> I just realized that these will deploy to our account even fro...
-0.69-
> I created a dedicated Cloudflare account specifically for our ...
4.4
code:
  count: 1
  score: "1"
  words: 3
0.844.4
> Hey just following up here. > > I made some special logic f...
2.2
code:
  count: 1
  score: "1"
  words: 3
0.682.2
> I just realized that these will deploy to our account even fro...
20.692
> Can you also update `ts-template` with any necessary changes, ...
2.8
code:
  count: 2
  score: "2"
  words: 4
0.642.8
> Can you also update `ts-template` with any necessary changes, ...
2.8
code:
  count: 2
  score: "2"
  words: 4
0.642.8

@rndquu rndquu reopened this Mar 13, 2024
@0x4007
Copy link
Member Author

0x4007 commented Mar 13, 2024

Is there a problem? Why did you reopen?

@rndquu
Copy link
Member

rndquu commented Mar 13, 2024

Is there a problem? Why did you reopen?

I will:

  1. Pass cloudflare API token and account id here
  2. Rotate cloudflare API token in github secrets

@0x4007
Copy link
Member Author

0x4007 commented Mar 13, 2024

I suppose it would also make sense to update the active repo actions as well?

@rndquu
Copy link
Member

rndquu commented Mar 13, 2024

I suppose it would also make sense to update the active repo actions as well?

yes

Copy link

ubiquibot bot commented Mar 13, 2024

+ Evaluating results. Please wait...

Copy link

ubiquibot bot commented Mar 13, 2024

[ 255 WXDAI ]

@pavlovcik
Contributions Overview
ViewContributionCountReward
IssueSpecification1190.4
IssueComment858
ReviewComment16.6
Conversation Incentives
CommentFormattingRelevanceReward
### Context

I spent quite a bit of time cleaning up the code ...

190.4

h3:
  count: 5
  score: "5"
  words: 6
a:
  count: 6
  score: "6"
  words: 16
li:
  count: 15
  score: "15"
  words: 450
code:
  count: 17
  score: "17"
  words: 34
1190.4
@rndquu please spend some time on this so that we can standardiz...
8.2
code:
  count: 1
  score: "1"
  words: 2
0.8158.2
Related: https://github.com/ubiquity/ts-template/issues/10...
1.80.591.8
> > I created a dedicated Cloudflare account specifically for ou...
5.2
code:
  count: 1
  score: "1"
  words: 3
0.6255.2
Hey just following up here.

I made some special logic for if...

14.2

code:
  count: 1
  score: "1"
  words: 3
0.614.2
I just realized that these will deploy to our account even from ...
17.20.6317.2
This is intended to facilitate our reviewers by posting the depl...
6.80.6056.8
Is there a problem? Why did you reopen?...
1.60.6751.6
I suppose it would also make sense to update the active repo act...
30.753
Can you also update `ts-template` with any necessary changes, pr...
6.6
code:
  count: 2
  score: "4"
  words: 4
0.76.6

[ 619 WXDAI ]

@rndquu
Contributions Overview
ViewContributionCountReward
IssueTask1600
IssueComment50
IssueComment513.4
ReviewComment12.8
ReviewComment12.8
Conversation Incentives
CommentFormattingRelevanceReward
> I created a dedicated Cloudflare account specifically for our ...
-
code:
  count: 1
  score: "0"
  words: 3
0.635-
> Hey just following up here. > > I made some special logic f...
-
code:
  count: 1
  score: "0"
  words: 3
0.51-
> I just realized that these will deploy to our account even fro...
-0.54-
> Is there a problem? Why did you reopen?

I will:

  1. Pass cl...
-
a:
  count: 1
  score: "0"
  words: 1
li:
  count: 2
  score: "0"
  words: 31
0.7-
> I suppose it would also make sense to update the active repo a...
-0.76-
> I created a dedicated Cloudflare account specifically for our ...
4.4
code:
  count: 1
  score: "1"
  words: 3
0.6354.4
> Hey just following up here. > > I made some special logic f...
2.2
code:
  count: 1
  score: "1"
  words: 3
0.512.2
> I just realized that these will deploy to our account even fro...
20.542
> Is there a problem? Why did you reopen?

I will:

  1. Pass cl...
4.7
a:
  count: 1
  score: "1"
  words: 1
li:
  count: 2
  score: "2"
  words: 31
0.74.7
> I suppose it would also make sense to update the active repo a...
0.10.760.1
> Can you also update `ts-template` with any necessary changes, ...
2.8
code:
  count: 2
  score: "2"
  words: 4
0.682.8
> Can you also update `ts-template` with any necessary changes, ...
2.8
code:
  count: 2
  score: "2"
  words: 4
0.682.8

@rndquu
Copy link
Member

rndquu commented Mar 15, 2024

@pavlovcik

TLDR; I want to revert all of the changes (one, two, three) and keep your initial approach with hardcoding cloudflare API credentials for now

Long story

Github CI has 2 contexts:

  1. Target org/repo (for example https://github.com/ubiquity organization)
  2. Base repo (where PR is originated)

Github secrets are available in workflows triggered by PRs in the following cases:

  1. PR is opened in a branch within the target organization (i.e. contributor is a member of an organization hence he is trusted to create branches)
  2. PR is opened in a branch from a forked repository. By default such PRs don't have access to organization secrets but there are options:
    a) Use pull_request_target
    b) Use workflow_run
    c) Use pull_request_target + "deploy" label

All of the options above are a security risk (together with the checkout action) since they run untrusted code in a PR with access to org/repo secrets and github API token with write access.

I've tried it myself and was able to exfiltrate env variables:

  1. Example with pull_request_target (check the "Cypress run" step, you may find GITHUB_TOKEN there)
  2. Example with workflow_run (check the "Build" step)

It doesn't really matter if we use a github composite action (like https://github.com/ubiquity/cloudflare-deploy-action) or a reusable workflow:

  • If we use a github composite action we can only pass secrets in action input params since github composite actions don't have access to org/repo secrets
  • If we use a reusable workflow then we can access secrets there only if the caller has access to secrets (Docs, TLDR; When a reusable workflow is triggered by a caller workflow, the github context is always associated with the caller workflow.). It means that we must use either pull_request_target either workflow_run which is again a security risk.

Overall this issue (unable to run deployment preview from forked repos) is common:

Our main goal is to make contributors UX as seamless as possible. We could:

  1. Create 2 cloudflare accounts: one for deployment previews and another one for production builds (like https://pay.ubq.fi/, https://work.ubq.fi/, etc...)
  2. Update "build and deploy" workflow this way (rough code):
name: Build & Deploy

on:
  push:
    branches:
      - development
  pull_request:

  - name: Deploy to Cloudflare
    uses: ubiquity/cloudflare-deploy-action@main
    with:
      repository: ${{ github.repository }}
      production_branch: ${{ github.event.repository.default_branch }}
      output_directory: "static"
      current_branch: ${{ github.ref_name }}
      pull_request_number: ${{ github.event.pull_request.number }}
      cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
      cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
      commit_sha: ${{ github.event.pull_request.head.sha }}

The main idea is that on the push event (to production branch) secrets are available and will be passed while on pull_request event secrets will be empty.
3. Update https://github.com/ubiquity/cloudflare-deploy-action so that:

  • if cloudflare credentials are passed then use them (which means production deploy)
  • if cloudflare credentials are not passed then use hardcoded values for deployment preview

So with the described approach:

  • we use hardcoded cloudflare credentials for deployment previews
  • we use github secrets to store cloudflare credentials for production builds
  • we don't have access to org/repo secrets in the build step because it requires using pull_request_target/workflow_run + running untrusted code in a PR with access to org/repo secrets

@rndquu rndquu reopened this Mar 15, 2024
@rndquu
Copy link
Member

rndquu commented Mar 15, 2024

Or use the approach we used earlier:

  1. Build workflow creates static files to publish and saves to github artifacts (here the "build workflow" runs in an unprivileged context so we'll have to hardcode env variables required for build)
  2. Cloudflare deploy workflow deploys the artifact (this workflow runs in privileged context with access to github secrets so we don't need to maintain 2 accounts)

@0x4007
Copy link
Member Author

0x4007 commented Mar 15, 2024

Well currently the domain is managed on my private account, and has a worker thats forwarding the subdomains to other workers on the "Ubiquity DAO Workers" account.

The other "Ubiquity DAO Workers" account only deploys workers that are forwarded to. So because of this, especially during our current rapid development phase, our security downsides should be fairly limited (we don't really have end users using our UIs now.)

The worst case scenario is that a bad pull is merged and one of our production UIs are compromised. But aside from that, I am unsure that the keys with the current minimum permissions are enough to cause real harm on the dedicated deploys account. It might be worth an audit from you.

Copy link

ubiquibot bot commented Mar 18, 2024

+ Evaluating results. Please wait...

Copy link

ubiquibot bot commented Mar 18, 2024

[ 279.4 WXDAI ]

@pavlovcik
Contributions Overview
ViewContributionCountReward
IssueSpecification1190.4
IssueComment982.4
ReviewComment16.6
Conversation Incentives
CommentFormattingRelevanceReward
### Context

I spent quite a bit of time cleaning up the code ...

190.4

h3:
  count: 5
  score: "5"
  words: 6
a:
  count: 6
  score: "6"
  words: 16
li:
  count: 15
  score: "15"
  words: 450
code:
  count: 17
  score: "17"
  words: 34
1190.4
@rndquu please spend some time on this so that we can standardiz...
8.2
code:
  count: 1
  score: "1"
  words: 2
0.858.2
Related: https://github.com/ubiquity/ts-template/issues/10...
1.80.6751.8
> > I created a dedicated Cloudflare account specifically for ou...
5.2
code:
  count: 1
  score: "1"
  words: 3
0.685.2
Hey just following up here.

I made some special logic for if...

14.2

code:
  count: 1
  score: "1"
  words: 3
0.70514.2
I just realized that these will deploy to our account even from ...
17.20.6917.2
This is intended to facilitate our reviewers by posting the depl...
6.80.746.8
Is there a problem? Why did you reopen?...
1.60.831.6
I suppose it would also make sense to update the active repo act...
30.7653
Well currently the domain is managed on my private account, and ...
24.40.77524.4
Can you also update `ts-template` with any necessary changes, pr...
6.6
code:
  count: 2
  score: "4"
  words: 4
0.7356.6

[ 725.9 WXDAI ]

@rndquu
Contributions Overview
ViewContributionCountReward
IssueTask1600
IssueComment70
IssueComment7120.3
ReviewComment12.8
ReviewComment12.8
Conversation Incentives
CommentFormattingRelevanceReward
> I created a dedicated Cloudflare account specifically for our ...
-
code:
  count: 1
  score: "0"
  words: 3
0.625-
> Hey just following up here. > > I made some special logic f...
-
code:
  count: 1
  score: "0"
  words: 3
0.78-
> I just realized that these will deploy to our account even fro...
-0.76-
> Is there a problem? Why did you reopen?

I will:

  1. Pass cl...
-
a:
  count: 1
  score: "0"
  words: 1
li:
  count: 2
  score: "0"
  words: 31
0.805-
> I suppose it would also make sense to update the active repo a...
-0.72-
@pavlovcik

TLDR; I want to revert all of the changes ([one](...

-

a:
  count: 14
  score: "0"
  words: 21
li:
  count: 17
  score: "0"
  words: 377
code:
  count: 11
  score: "0"
  words: 30
0.765-
Or use the approach we used earlier: 1. [Build workflow](https:...
-
a:
  count: 2
  score: "0"
  words: 4
li:
  count: 2
  score: "0"
  words: 93
0.825-
> I created a dedicated Cloudflare account specifically for our ...
4.4
code:
  count: 1
  score: "1"
  words: 3
0.6254.4
> Hey just following up here. > > I made some special logic f...
2.2
code:
  count: 1
  score: "1"
  words: 3
0.782.2
> I just realized that these will deploy to our account even fro...
20.762
> Is there a problem? Why did you reopen?

I will:

  1. Pass cl...
4.7
a:
  count: 1
  score: "1"
  words: 1
li:
  count: 2
  score: "2"
  words: 31
0.8054.7
> I suppose it would also make sense to update the active repo a...
0.10.720.1
@pavlovcik

TLDR; I want to revert all of the changes ([one](...

96.4

a:
  count: 14
  score: "14"
  words: 21
li:
  count: 17
  score: "17"
  words: 377
code:
  count: 11
  score: "11"
  words: 30
0.76596.4
Or use the approach we used earlier: 1. [Build workflow](https:...
10.5
a:
  count: 2
  score: "2"
  words: 4
li:
  count: 2
  score: "2"
  words: 93
0.82510.5
> Can you also update `ts-template` with any necessary changes, ...
2.8
code:
  count: 2
  score: "2"
  words: 4
0.652.8
> Can you also update `ts-template` with any necessary changes, ...
2.8
code:
  count: 2
  score: "2"
  words: 4
0.652.8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment