-
Notifications
You must be signed in to change notification settings - Fork 0
docs: hosting documentation #2
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
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f986b44
docs: hosting documentation
KyleKing 0468945
Merge branch 'yak-shears-py' into hetzner-py
KyleKing 7a90739
Apply suggestions from code review
KyleKing 063dca4
Apply suggestion from @coderabbitai[bot]
KyleKing c21da5e
docs: implement recommendations from Code Rabbit
KyleKing File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| Several options exist for web-based file editors in Go, ranging from simple solutions to more feature-rich applications. Here are a few examples: | ||
|
|
||
| • Simple Text Editor: | ||
| • A basic implementation involves using Go's html/template package to serve an HTML form with a textarea for editing. The server can then handle saving the content back to a file. | ||
| • This approach provides fundamental editing capabilities but lacks advanced features like syntax highlighting or real-time collaboration. | ||
|
|
||
| • File Browser with Built-in Editor: | ||
| • Filebrowser is a web file manager written in Go that includes a built-in code editor with syntax highlighting for various languages. | ||
| • It offers functionalities like file management (upload, download, rename, delete), user authentication, and customization options. | ||
|
|
||
| • WebEdit: | ||
| • WebEdit is an HTML5-based text editor designed for editing local files on a server. It aims to provide a more responsive editing experience compared to using SSH and command-line editors. | ||
|
|
||
| • Go Playground: | ||
| • The Go Playground is a web service that allows users to run Go code in a sandboxed environment. While primarily for running code snippets, it can also be used for basic file editing. | ||
|
|
||
| • GitHub.dev: | ||
| • GitHub.dev is a web-based editor that runs entirely in the browser. It allows users to navigate and edit files in GitHub repositories, offering features like syntax highlighting and source control integration. | ||
|
|
||
| These options cater to different needs, from quick edits to comprehensive file management and code editing within a web environment. The choice of editor depends on the specific requirements of the project. | ||
|
|
||
| --- | ||
|
|
||
|
|
||
| To implement a file editor using Go and HTMX, consider the following approach: | ||
|
|
||
| • Backend (Go): | ||
| • File Handling: Implement functions to read, write, and update files on the server. | ||
| • Routing: Use a Go web framework (like net/http or chi) to define routes for handling file operations. For example: | ||
| • GET /edit/{filename}: Retrieve file content for editing. | ||
| • POST /save/{filename}: Save updated file content. | ||
|
|
||
| • Templating: Employ Go's html/template package or a templating engine like Templ to render HTML fragments for HTMX responses. | ||
|
|
||
| • Frontend (HTMX and HTML): | ||
| • Display File Content: Create an HTML form with a <textarea> element to display and edit the file content. | ||
| • HTMX Integration: Use HTMX attributes to handle user interactions: | ||
| • hx-get on page load to fetch initial file content. | ||
| • hx-post on form submission to save changes. | ||
| • hx-target and hx-swap to update the UI after saving. | ||
|
|
||
| • Markdown Editor (Optional): Integrate a client-side Markdown editor like EasyMDE for enhanced editing capabilities. | ||
|
|
||
| • Workflow: | ||
| • The user requests to edit a file (e.g., /edit/my-file.txt). | ||
| • The Go server reads the file and renders it within an HTML form. | ||
| • HTMX loads the form content into the page. | ||
| • The user edits the content and submits the form. | ||
| • HTMX sends a POST request to the /save endpoint. | ||
| • The Go server saves the changes and returns an updated HTML fragment. | ||
| • HTMX updates the UI with the response. | ||
|
|
||
| • Code Example (Conceptual): | ||
|
|
||
| // Go (Backend) | ||
| func handleEditFile(w http.ResponseWriter, r *http.Request) { | ||
| filename := mux.Vars(r)["filename"] | ||
| content, err := os.ReadFile(filename) | ||
| if err != nil { /* handle error */ } | ||
| tmpl.ExecuteTemplate(w, "edit_form.html", map[string]string{"Filename": filename, "Content": string(content)}) | ||
| } | ||
|
|
||
| func handleSaveFile(w http.ResponseWriter, r *http.Request) { | ||
| filename := mux.Vars(r)["filename"] | ||
| content := r.FormValue("content") | ||
| err := os.WriteFile(filename, []byte(content), 0644) | ||
| if err != nil { /* handle error */ } | ||
| // Return updated HTML or success message | ||
| fmt.Fprint(w, "<div class='success'>File saved successfully!</div>") | ||
| } | ||
|
|
||
| <!-- HTML (Frontend - edit_form.html) --> | ||
| <form hx-post="/save/{{.Filename}}" hx-target="#file-editor" hx-swap="outerHTML"> | ||
| <textarea name="content">{{.Content}}</textarea> | ||
| <button type="submit">Save</button> | ||
| </form> | ||
| <div id="file-editor"></div> | ||
|
|
||
| • Templ vs standard templates: | ||
| • Templ offers better type safety, but it introduces extra steps of generating templ files and then compiling the program before checking template changes. [1] | ||
| • Standard templates allow to check template changes just by saving the template file and reloading the page. | ||
|
|
||
| [1] https://www.reddit.com/r/htmx/comments/1ams8xi/gohtmx_templ_vs_templates/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # Hosting | ||
|
|
||
| Selected VPS for similarity to local usage and Hetzner because of cost and IaC support (<https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs/resources/server>). See notes on deployment saved in 1Password for Hetzner (and more info on SSH Keys if needed: <https://community.hetzner.com/tutorials/howto-ssh-key>) | ||
|
|
||
| *Hetzner Web Console requires Rescue>Reset to get a root password when created with SSH: <https://docs.hetzner.com/cloud/servers/getting-started/vnc-console>* | ||
|
|
||
| ## Install SyncThing | ||
|
|
||
| Following: <https://idroot.us/install-syncthing-ubuntu-24-04> | ||
|
|
||
| ```sh | ||
| sudo apt update -y && sudo apt upgrade -y && sudo apt autoremove | ||
|
|
||
| sudo apt install gnupg2 curl apt-transport-https -y | ||
|
|
||
| # Follow instructions from: https://apt.syncthing.net | ||
| sudo mkdir -p /etc/apt/keyrings | ||
| sudo curl -L -o /etc/apt/keyrings/syncthing-archive-keyring.gpg https://syncthing.net/release-key.gpg | ||
| # Add the "stable" channel to your APT sources: | ||
| echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] https://apt.syncthing.net/ syncthing stable" | sudo tee /etc/apt/sources.list.d/syncthing.list | ||
| sudo apt update | ||
| sudo apt install syncthing | ||
| syncthing --version | ||
|
|
||
| # Warning: consider making a non-root user for the application in a future iteration | ||
| sudo systemctl enable [email protected] | ||
| sudo systemctl start [email protected] | ||
| sudo systemctl status [email protected] | ||
|
|
||
| # Keep SSH, turn on web, and allow ports for Syncthing | ||
| sudo ufw allow ssh && sudo ufw allow http && sudo ufw allow https && | ||
| sudo ufw allow 22000/tcp && sudo ufw allow 22000/udp && sudo ufw allow 21027/udp && sudo ufw enable && sudo ufw status | ||
| ``` | ||
|
|
||
| ```sh | ||
| # Port-Forward the UI to Sync (run on Laptop) | ||
| ssh -L 9998:localhost:8384 ubuntu-4gb-hel1-1 | ||
KyleKing marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # Copy Laptop Device ID, accept from laptop, then edit the connection to check all three options (introducer, share, etc.), and confirm one more time from laptop | ||
| # <https://docs.syncthing.net/intro/getting-started.html#configuring> | ||
| ``` | ||
|
|
||
| ## Install Caddy | ||
|
|
||
| Following: <https://caddyserver.com/docs/install#debian-ubuntu-raspbian> | ||
|
|
||
| ```sh | ||
| sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl | ||
| curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg | ||
| curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list | ||
| sudo apt update | ||
| sudo apt install caddy | ||
| ``` | ||
|
|
||
| Following: <https://caddyserver.com/docs/quick-starts/https> and Gemini | ||
|
|
||
| ```sh | ||
| sudo ufw allow OpenSSH && sudo ufw allow http && sudo ufw allow https && sudo ufw enable && sudo ufw status | ||
| # Check that the domain is configured | ||
| curl "https://cloudflare-dns.com/dns-query?name=yak-shears.kyleking.me&type=A" \ | ||
| -H "accept: application/dns-json" | ||
|
|
||
| tee "Caddyfile" >/dev/null <<'EOF' | ||
| { | ||
| email [email protected] # Recommended for Let's Encrypt notifications | ||
| } | ||
|
|
||
| yak-shears.kyleking.me { | ||
| # 8384 for Syncthing fails because of host check errors as designed | ||
| reverse_proxy localhost:8084 { | ||
| header_up Host {host} | ||
| header_up X-Real-IP {remote_host} | ||
| header_up X-Forwarded-For {remote_host} | ||
| header_up X-Forwarded-Proto {scheme} | ||
| } | ||
| header { | ||
| # (HSTS): Forces browsers to always use HTTPS. | ||
| Strict-Transport-Security "max-age=31536000; includeSubDomains" | ||
| # Prevents browsers from MIME-sniffing | ||
| X-Content-Type-Options "nosniff" | ||
| # Helps prevent clickjacking attacks. | ||
| X-Frame-Options "DENY" | ||
| # Controls how much referrer information is sent with requests. | ||
| Referrer-Policy "same-origin" | ||
| # Content-Security-Policy "default-src 'self';" # Customize as needed | ||
| } | ||
| } | ||
| EOF | ||
|
|
||
| # # Example reviewing logs: | ||
| # sudo journalctl -u caddy --no-pager | ||
| ``` | ||
|
|
||
| ## TODO | ||
|
|
||
| 1. The ufw rules appear to reset on VPS boot. I may need to edit the defaults? | ||
| 1. And keep Caddy running: <https://caddyserver.com/docs/running> | ||
| 1. Create script that copies all the manually managed files into a single location for version control (e.g. traefik config, sshd_config, maybe output of ufw, apt versions, Linux version, systemctl, etc.) | ||
| 1. Create a basic HTMX app with authentication | ||
| 1. Add list all files (show `<header> (<dir>/<filename>)` in future version) | ||
| 1. Then per file, shows the raw text and then allows edits with HTMX submit (in future, default view is a preview where switching to edit would warn other users -- maybe locally is also git to track changes? How to use different users when editing the files from the go server?) | ||
| 1. Further in the future, have GitOps where a cron-scheduled service checks for git changes, pulls, and then updates the service (how to handle downtime - maybe have flag in UI that current users can delay while working on changes?) | ||
| - 10-min golang+systemctl deploy: https://jonathanmh.com/p/deploying-go-apps-systemd-10-minutes-without-docker/ | ||
| - Other options: https://www.ecosia.org/search?q=running%20golang+on+vps&addon=firefox&addonversion=5.2.0&method=topbar | ||
| - https://reintech.io/blog/writing-web-based-code-editor-go | ||
| - https://www.magicbell.com/blog/setting-up-htmx-and-templ-for-go | ||
| - https://gist.github.com/peterhellberg/60dcccab932f8446bacd2ceb57ba603d | ||
| - https://www.youtube.com/watch?v=x7v6SNIgJpE (Primeagen Golang+HTMX) | ||
| - Structure: https://www.youtube.com/watch?v=lVyIQV-op5I | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| Okay, here's a step-by-step guide for a peer software engineer on configuring Caddy 2 as a reverse proxy for `localhost:8311` on an Ubuntu VPS with a domain managed by CloudFlare, focusing on security and performance: | ||
|
|
||
| **Assumptions:** | ||
|
|
||
| * You have an Ubuntu VPS with SSH access. | ||
| * You have a domain name managed through CloudFlare (e.g., `yourdomain.com`). | ||
| * Your application is running on the VPS and accessible at `localhost:8311`. | ||
| * You have basic familiarity with the Linux command line. | ||
|
|
||
| **Step 1: Install Caddy 2 on your Ubuntu VPS** | ||
|
|
||
| Caddy is known for its ease of use and automatic TLS certificate management. We'll install it using the official methods. | ||
|
|
||
| 1. **SSH into your Ubuntu VPS:** | ||
| ```bash | ||
| ssh your_username@your_vps_ip_address | ||
| ``` | ||
|
|
||
| 2. **Install Caddy using the official repository:** | ||
| ```bash | ||
| sudo apt update | ||
| sudo apt install -y debian-keyring | ||
| curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg | ||
| curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.sources | ||
| sudo apt update | ||
| sudo apt install caddy | ||
| ``` | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 3. **Verify the installation:** | ||
| ```bash | ||
| caddy version | ||
| ``` | ||
| You should see the installed Caddy version. | ||
|
|
||
| **Step 2: Configure Caddy for Reverse Proxying** | ||
|
|
||
| Caddy's configuration is done through a file called `Caddyfile`. We'll create and configure this file. | ||
|
|
||
| 1. **Create the `Caddyfile`:** | ||
| ```bash | ||
| sudo nano /etc/caddy/Caddyfile | ||
| ``` | ||
|
|
||
| 2. **Add the following configuration to the `Caddyfile`, replacing `yourdomain.com` with your actual domain:** | ||
|
|
||
| ```caddyfile | ||
| { | ||
| email [email protected] # Recommended for Let's Encrypt notifications | ||
| } | ||
|
|
||
| yourdomain.com { | ||
| reverse_proxy localhost:8311 { | ||
| header_up Host {host} | ||
| header_up X-Real-IP {remote_host} | ||
| header_up X-Forwarded-For {remote_host} | ||
| header_up X-Forwarded-Proto {scheme} | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| **Explanation of the configuration:** | ||
|
|
||
| * `{ email [email protected] }`: This is optional but highly recommended. Caddy uses this email to register with Let's Encrypt for TLS certificates and will send notifications about certificate renewals. Replace `[email protected]` with your actual email address. | ||
| * `yourdomain.com { ... }`: This block defines the configuration for your domain. Caddy will automatically handle TLS for this domain because it's a recognized public domain. | ||
| * `reverse_proxy localhost:8311 { ... }`: This directive tells Caddy to forward incoming requests for `yourdomain.com` to your application running on `localhost:8311`. | ||
| * `header_up Host {host}`: Passes the original Host header from the client to your backend application. This is often necessary for applications that rely on the Host header. | ||
| * `header_up X-Real-IP {remote_host}`: Passes the client's real IP address. | ||
| * `header_up X-Forwarded-For {remote_host}`: Appends the client's IP address to the `X-Forwarded-For` header, which might already contain proxy IPs. | ||
| * `header_up X-Forwarded-Proto {scheme}`: Indicates whether the original request was made over HTTP or HTTPS. | ||
|
|
||
| 3. **Save and close the `Caddyfile`:** Press `Ctrl+X`, then `Y`, then `Enter`. | ||
|
|
||
| **Step 3: Ensure Caddy Service is Running and Enabled** | ||
|
|
||
| Caddy should be configured to run as a service so it starts automatically on boot. | ||
|
|
||
| 1. **Start the Caddy service:** | ||
| ```bash | ||
| sudo systemctl start caddy | ||
| ``` | ||
|
|
||
| 2. **Check the status of the Caddy service:** | ||
| ```bash | ||
| sudo systemctl status caddy | ||
| ``` | ||
| You should see that the service is active and running. If there are errors, check the Caddy logs using `sudo journalctl -u caddy --no-pager`. | ||
|
|
||
| 3. **Enable the Caddy service to start on boot:** | ||
| ```bash | ||
| sudo systemctl enable caddy | ||
| ``` | ||
|
|
||
| **Step 4: Configure CloudFlare DNS** | ||
|
|
||
| To ensure traffic is routed to your VPS, you need to configure the DNS records in CloudFlare. | ||
|
|
||
| 1. **Log in to your CloudFlare account.** | ||
| 2. **Select your domain (e.g., `yourdomain.com`).** | ||
| 3. **Go to the "DNS" section.** | ||
| 4. **Ensure you have an A record (or AAAA record for IPv6) pointing your domain (or the subdomain you want to use) to the public IP address of your Ubuntu VPS.** | ||
|
|
||
| * **For the root domain (`yourdomain.com`):** | ||
| * Type: `A` | ||
| * Name: `@` or leave it blank | ||
| * Value: Your VPS public IP address | ||
| * TTL: Automatic or your preference | ||
| * **Important: Ensure the "Proxy status" (the cloud icon) is set to "Proxied" (orange cloud).** This is crucial for CloudFlare to handle the TLS termination and provide its security benefits. | ||
|
|
||
| * **For a subdomain (e.g., `app.yourdomain.com`):** | ||
| * Type: `A` | ||
| * Name: `app` | ||
| * Value: Your VPS public IP address | ||
| * TTL: Automatic or your preference | ||
| * **Important: Ensure the "Proxy status" (the cloud icon) is set to "Proxied" (orange cloud).** | ||
|
|
||
| **Step 5: Verify the Setup** | ||
|
|
||
| 1. **Access your domain in your web browser (e.g., `https://yourdomain.com`).** | ||
| 2. **You should see your application running.** | ||
| 3. **Verify that the connection is secure (HTTPS).** You should see a padlock icon in your browser's address bar. This indicates that Caddy automatically obtained and is serving a TLS certificate. | ||
|
|
||
| **Security Considerations:** | ||
|
|
||
| * **CloudFlare Proxy:** By using CloudFlare's proxy, you benefit from: | ||
| * **DDoS protection:** CloudFlare helps mitigate distributed denial-of-service attacks. | ||
| * **Basic firewall rules:** CloudFlare offers options to block malicious traffic. | ||
| * **TLS termination at the edge:** This can improve performance for users geographically distant from your server. | ||
| * **Hiding your origin server's IP address:** This makes it harder for attackers to target your VPS directly. | ||
| * **Caddy's Automatic TLS:** Caddy automatically obtains and renews TLS certificates from Let's Encrypt, ensuring your connection is always secure without manual intervention. | ||
| * **Secure Headers (Optional but Recommended):** You can add security headers to your Caddyfile for enhanced security: | ||
|
|
||
| ```caddyfile | ||
| yourdomain.com { | ||
| reverse_proxy localhost:8311 { | ||
| header_up Host {host} | ||
| header_up X-Real-IP {remote_host} | ||
| header_up X-Forwarded-For {remote_host} | ||
| header_up X-Forwarded-Proto {scheme} | ||
| } | ||
|
|
||
| header { | ||
| Strict-Transport-Security "max-age=31536000; includeSubDomains" | ||
| X-Content-Type-Options "nosniff" | ||
| X-Frame-Options "DENY" | ||
| Referrer-Policy "same-origin" | ||
| # Content-Security-Policy "default-src 'self';" # Customize as needed | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| * `Strict-Transport-Security` (HSTS): Forces browsers to always use HTTPS. | ||
| * `X-Content-Type-Options`: Prevents browsers from MIME-sniffing. | ||
| * `X-Frame-Options`: Helps prevent clickjacking attacks. | ||
| * `Referrer-Policy`: Controls how much referrer information is sent with requests. | ||
| * `Content-Security-Policy` (CSP): A powerful header to control resources the browser is allowed to load. **Customize this based on your application's needs.** | ||
|
|
||
| * **Firewall on the VPS:** Ensure your VPS firewall (like `ufw`) is configured to only allow necessary inbound traffic (e.g., SSH, HTTP, HTTPS). Caddy will handle the HTTPS traffic on port 443. | ||
| ```bash | ||
| sudo ufw allow OpenSSH | ||
| sudo ufw allow http | ||
| sudo ufw allow https | ||
| sudo ufw enable | ||
| sudo ufw status | ||
| ``` | ||
| * **Regular Updates:** Keep your Ubuntu system and Caddy package updated to patch security vulnerabilities. | ||
|
|
||
| **Performance Considerations:** | ||
|
|
||
| * **CloudFlare CDN (Optional):** If your application serves static assets, consider enabling CloudFlare's Content Delivery Network (CDN) to cache these assets closer to your users, improving load times. This is usually enabled by default when the proxy is active. | ||
| * **Keep-Alive Connections:** Caddy and most modern browsers use keep-alive connections (HTTP persistent connections) by default, which reduces the overhead of establishing new connections for each request. | ||
| * **Gzip Compression:** Ensure your backend application is configured to use gzip or Brotli compression for responses. Caddy can also handle compression, but it's generally recommended to do it at the application level if possible. | ||
| * **HTTP/2 and HTTP/3:** Caddy automatically supports HTTP/2 and HTTP/3, which can improve performance by allowing multiple requests over a single connection and reducing latency. CloudFlare also supports these protocols. | ||
| * **Resource Limits:** Monitor the resource usage of your VPS to ensure it can handle the traffic. | ||
|
|
||
| **Troubleshooting:** | ||
|
|
||
| * **Check Caddy Logs:** If you encounter issues, the Caddy logs are your first place to look: `sudo journalctl -u caddy --no-pager`. | ||
| * **Verify DNS Propagation:** It might take some time for DNS changes in CloudFlare to propagate. You can use online tools to check DNS records. | ||
| * **CloudFlare SSL/TLS Settings:** Review the SSL/TLS settings in your CloudFlare dashboard to ensure they are compatible with Caddy (e.g., "Full" or "Full (strict)" mode is usually recommended). | ||
| * **Firewall Issues:** Double-check your VPS firewall rules to ensure they are not blocking traffic to Caddy. | ||
| * **Application Errors:** If you can access Caddy but not your application, check your application logs for errors. | ||
|
|
||
| This comprehensive guide should help you configure Caddy 2 as a secure and performant reverse proxy for your application on your Ubuntu VPS with a domain managed by CloudFlare. Remember to adapt the configurations to your specific needs and always prioritize security best practices. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.