Get Netlify build time down to 0 hours with GitHub Actions and get rid of the month-end hijinks!

tubone24

tubone24

Posted on February 28, 2021

Get Netlify build time down to 0 hours with GitHub Actions and get rid of the month-end hijinks!

Netlify

Netlify is a very useful and appreciated service that you all know and love, but for the poor people who use it for free**, there is a certain problem that arises every month.

This month's build time is XX minutes left!

img

Netlify works with GitHub repositories to run front-end builds and deploy them, but there is a time limit on how long it takes to run these builds.

The free version is limited to 300 minutes per month. (If you want to use it more than that, you can charge $19/month for the Pro version. I've already paid for it.)

It's not that I don't think 300 minutes will be enough, but

If you're using Netlify across multiple repositories, if you're using a lot of images in Gatsby.js and it's taking a long time for Sharp to resize them, or if Dependabot is getting PRs and Preview deploys regularly
You may be surprised to find that you're on the edge.

img

So, at the end of the month, poor folks like me are so worried about Netlify build time that they stop writing this blog post** and slow down the pace of Site Refactor.

So at the end of the month, poor folks like me are so worried about Netlify's build time that they don't write any more posts on this blog or slow down the pace of Site Refactor.

For example, the article I'm writing right now is written from my phone on the train to work, so I'd like to hit commit and save it in detail, but if I hit commit and push it, the build will run, so I'm too lazy to commit with WIP, and as a result, I don't have a work space like at home where I can push it all together. As a result, I don't blog anymore unless I'm at home where I have a work space where I can push it all together.
As a result, I don't write blogs anymore unless I am at home where I have a work space where I can push them all together.

Leave this problem to GitHub Actions!

So, I'd like to solve this problem with GitHub Actions.

Find out what Netlify is doing when it builds and try to do it yourself.

Basically, what Netlify does when building, for example, Gatsby.js, is to run the gatsby build command and deploy the built JS in a specific directory (usually ./public) and deploys the pre-built JS placed in a specific directory.
However, there is a pattern that Netlify performs post-processing (PostProcess) on the built JS.

In my case, Asset optimization, which optimizes JS and images, and Form detection, which creates forms automatically by adding attributes to Form tags, were set.

img

img

This will no longer be done by Netlify, so you will need to reimplement it here.

gatsby-plugin-minify

Among asset optimization, JS and CSS minifers can use gatsby-plugin-minify to minify html, JS, and CSS can be minified.

You can install it from NPM (yarn) as usual.

npm install gatsby-plugin-minify
Enter fullscreen mode Exit fullscreen mode

You can use it by setting the plugins in gatsby-config.js as follows.

    {
      resolve: 'gatsby-plugin-minify',
      options: {
        caseSensitive: false,
        collapseBooleanAttributes: true,
        useShortDoctype: false,
        removeEmptyElements: false,
        removeComments: true,
        removeAttributeQuotes: false,
        minifyCSS: true,
        minifyJS: true,
      },
    },
Enter fullscreen mode Exit fullscreen mode

By setting minifyCSS and minifyJS to true, the CSS will be minified together using clean-css and the JS will be minified together using UglifyJS to minify the JS together. Also, the other side of gatsby-plugin-minify is just multiplying html-minifier by postbuild in gatsby-node.js. You can set detailed options in html-minifier.

By the way, you have to be careful to set the removeAttributeQuotes option to false.

If you set this option to true, double-quotes will be removed from the attributes in the HTML tags, and the file will be a little lighter, but it will not load well in systems like berss.com that retrieve RSS links from sites. I've been using it all day.

Be careful if you are planting RSS links as page links.

By using imgur, you can do image hosting and resizing at the same time.

There is a service called imgur.

It is mainly known as an image hosting service for Reddit and Gifs, but I use imgur on this blog because it allows you to easily resize and host images.

This can be done by inserting the keyword that matches the image size after the image URL.

For example, if you have an image with this URL

https://i.imgur.com/Wfz9G0B.png
Enter fullscreen mode Exit fullscreen mode

To resize to 160x160, attach b to the back.

https://i.imgur.com/Wfz9G0Bb.png
Enter fullscreen mode Exit fullscreen mode

This completes the image optimization.

getform.io

Getform.io](https://getform.io/) is a great service that provides a backend for forms.

You need to pay for the paid version to use the useful integrations, but if you just want to send a notification email to a specified email address when a form is submitted, you can do that for free.

We will now replace Netlify's Form detection.

First of all, when you create a new form, you can issue an Action URL for the form.

The following blog explains how to create a Form in an easy-to-understand manner.

https://blog.nakamu.life/posts/getform-io

Now, once the Form is created, you can follow the tutorial and set this URL to the action of the Form tag, but the free version of GetForm does not allow you to set the Thanks page after submitting the Form.

<!--
* Add your getform endpoint into "action" attribute
* Set a unique "name" field
* Start accepting submissions
-->
<form action="{getform-endpoint}" method="POST">

  <input type="text" name="name">
  <input type="email" name="email">
  <button type="submit">Send</button>

</form>
Enter fullscreen mode Exit fullscreen mode

img

Well, this is good enough, but since we are using React, let's specify that the URL of getform.io should be POST fetched behind the scenes and sent to your own Thanks URL defined in actions.

First, we need to set onSubmit on the Form.

        <form
              name="contact"
              method="post"
              action="/thanks/"
              onSubmit={this.handleSubmit}
            >
                <label>
                  <span className="icon-user" />&nbsp;Your name<br />
                  <input
                    type="text"
                    name="name"
                    className="form-control"
                    maxLength="30"
                    minLength="2"
                    required
                    placeholder="Enter your name"
                    onChange={this.handleChange}
                  />
                </label>
              </p>
Enter fullscreen mode Exit fullscreen mode

Then, separately define a function that will fire onSubmit.

  handleSubmit(e) {
    e.preventDefault();
    const form = e.target;
    fetch('https://getform.io/f/xxxxxxxxxxxxxxxxxxxxxxxxx', {
      method: 'POST',
      body: Contact.encode({
        'form-name': form.getAttribute('name'),
        ...this.state,
      }),
    })
  }
Enter fullscreen mode Exit fullscreen mode

Since we are sending a Form, we need to send the elements appended to FormData in the fetch.

  static encode(data) {
    const formData = new FormData();
    // eslint-disable-next-line no-restricted-syntax
    for (const key of Object.keys(data)) {
      formData.append(key, data[key]);
    }
    return formData;
  }
Enter fullscreen mode Exit fullscreen mode

Again, in React, in addition to actions, onSubmit can be a function in a Form.

However, when onSubmit is pressed, the input items of the Form must be passed via POST Fetch, so for each changeEvent that occurs on input of the Form, the value of the Form should be saved as a state.

  handleChange(e) {
    this.setState({ [e.target.name]: e.target.value });
  }

  handleAttachment(e) {
    this.setState({ [e.target.name]: e.target.files[0] });
  }

  (δΈ­η•₯)

                <label>
                  <span className="icon-user" />&nbsp;Your name<br />
                  <input
                    type="text"
                    name="name"
                    className="form-control"
                    maxLength="30"
                    minLength="2"
                    required
                    placeholder="Enter your name"
                    onChange={this.handleChange}
                  />

                </label>
              </p>
Enter fullscreen mode Exit fullscreen mode

Also, if you use onSubmit, you will not be able to jump to the Thanks page after the Post process is finished using Gatsby's navigate function.

  handleSubmit(e) {
    e.preventDefault();
    const form = e.target;
    fetch('https://getform.io/f/897f187e-876d-42a7-b300-7c235af72e6d', {
      method: 'POST',
      body: Contact.encode({
        'form-name': form.getAttribute('name'),
        ...this.state,
      }),
    })
      .then(() => navigateTo(form.getAttribute('action')))
      .catch((error) => alert(error));
  }
Enter fullscreen mode Exit fullscreen mode

Now you can create your own Thanks page even with GetForm free version.

img

Build and Deploy with GitHub Actions

Once you've done this, all you need to do is build and deploy with GitHub Actions.

We'll create two actions, one for preview deployment with a PR to the master branch, and another for production deployment with a commit to master.

First, let's do Preview deployment.

name: DeployToNetlifyPreview
on:
  pull_request:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v2
      - name: Cache node_modules
        uses: actions/cache@v1
        with:
          path: node_modules
          key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: npm install and build
        env:
          GATSBY_GITHUB_CLIENT_SECRET: ${{secrets.GATSBY_GITHUB_CLIENT_SECRET}}
          GATSBY_GITHUB_CLIENT_ID: ${{secrets.GATSBY_GITHUB_CLIENT_ID}}
          GATSBY_ALGOLIA_SEARCH_API_KEY: ${{secrets.GATSBY_ALGOLIA_SEARCH_API_KEY}}
          GATSBY_ALGOLIA_INDEX_NAME: ${{secrets.GATSBY_ALGOLIA_INDEX_NAME}}
          GATSBY_ALGOLIA_APP_ID: ${{secrets.GATSBY_ALGOLIA_APP_ID}}
          GATSBY_ALGOLIA_ADMIN_API_KEY: ${{secrets.GATSBY_ALGOLIA_ADMIN_API_KEY}}
          FAUNADB_SERVER_SECRET: ${{secrets.FAUNADB_SERVER_SECRET}}
        run: |
          npm install
          npm run build
      - name: Deploy to netlify
        run: npx netlify-cli deploy --dir=./public > cli.txt
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
      - name: Cat cli.txt
        run: |
          cat cli.txt
          sed -i -z 's/\n/\\n/g' cli.txt
      - name: Post Netlify CLI Comment
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.comments_url }}
        run: |
          curl -X POST \
               -H "Authorization: token ${GITHUB_TOKEN}" \
               -d "{\"body\": \"$(cat cli.txt)\"}" \
               ${URL}
Enter fullscreen mode Exit fullscreen mode

The node setup, npm install, and build are the same as usual.

In GitHub Actions, you can specify a secret, so API keys for Algolia search and FaunaDB are passed as secrets in the environment variable at build time.

Incidentally, if you set GATSBY_XXXX in the environment variable, the environment variable will also be entered in the built JS. (Don't forget to do this when using environment variables from JS. This is a point that gets stuck quite a bit.

To deploy, use netlify-cli.

The required environment variables are site ID and AUTH TOKEN.

One of the features of netlify-cli is that when the deployment is successful, the deploy URL will appear in the standard output**.

I also send the URL to PR comments.

The great thing about GitHub Actions is that you can retrieve the GITHUB TOKEN by using secrets.GITHUB_TOKEN without setting it, so you can easily send it to PR comments.

      - name: Deploy to netlify
        run: npx netlify-cli deploy --dir=./public > cli.txt
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
      - name: Cat cli.txt
        run: |
          cat cli.txt
          sed -i -z 's/\n/\\n/g' cli.txt
      - name: Post Netlify CLI Comment
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.comments_url }}
        run: |
          curl -X POST \
               -H "Authorization: token ${GITHUB_TOKEN}" \
               -d "{\"body\": \"$(cat cli.txt)\"}" \
               ${URL}
Enter fullscreen mode Exit fullscreen mode

The next step is to deploy to production.

name: DeployToNetlifyPRD
on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v2
      - name: Cache node_modules
        uses: actions/cache@v1
        with:
          path: node_modules
          key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: npm install and build
        env:
          GATSBY_GITHUB_CLIENT_SECRET: ${{secrets.GATSBY_GITHUB_CLIENT_SECRET}}
          GATSBY_GITHUB_CLIENT_ID: ${{secrets.GATSBY_GITHUB_CLIENT_ID}}
          GATSBY_ALGOLIA_SEARCH_API_KEY: ${{secrets.GATSBY_ALGOLIA_SEARCH_API_KEY}}
          GATSBY_ALGOLIA_INDEX_NAME: ${{secrets.GATSBY_ALGOLIA_INDEX_NAME}}
          GATSBY_ALGOLIA_APP_ID: ${{secrets.GATSBY_ALGOLIA_APP_ID}}
          GATSBY_ALGOLIA_ADMIN_API_KEY: ${{secrets.GATSBY_ALGOLIA_ADMIN_API_KEY}}
          FAUNADB_SERVER_SECRET: ${{secrets.FAUNADB_SERVER_SECRET}}
        run: |
          npm install
          npm run build
      - name: Deploy to netlify
        run: npx netlify-cli deploy --prod --dir=./public
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
Enter fullscreen mode Exit fullscreen mode

It is almost the same, but by including the --prod option in the deploy command in netlify-cli, it will be deployed to the production environment.

      - name: Deploy to netlify
        run: npx netlify-cli deploy --prod --dir=./public
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Now the build time of Netlify is zero, which is a mental relief.

img

And I can get on with refactoring and writing articles!

πŸ’– πŸ’ͺ πŸ™… 🚩
tubone24
tubone24

Posted on February 28, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related