Skip to content

Instantly share code, notes, and snippets.

@RickCogley
Last active May 28, 2023 23:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RickCogley/d4246b9f2817a401f0943149c255697d to your computer and use it in GitHub Desktop.
Save RickCogley/d4246b9f2817a401f0943149c255697d to your computer and use it in GitHub Desktop.
Deploy Hugo to Deno Deploy via Github Actions

Background

Deno Deploy is an excellent, performant and cost-effective service geared toward hosting Deno apps at the edge. It can easily host a folder of static HTML files, if you provide an index.ts to launch something like "oak" to serve them (example index.ts below).

(It's important to note that it's still officially considered beta as of May 2023, and there have been some surprising periods of downtime over the past few months... just be sure to keep that in mind)

Hugo is a phenomenally fast-building and mature SSG, which can produce a folder of static files, but requires a build step like hugo --gc --minify --verbose --baseURL=$HUGOBASEURL --ignoreCache to generate them.

Below is a yaml file you would place in your project's .github/workflows folder. If you link your Deno Deploy project using Github Actions instead of specifying an index file, it will defer to what's in this. In this case, the Hugo files generated into public are being served by file_server.ts but I imagine you could use oak or other servers. You can see the build step for hugo is a bit involved. Your build might be simpler, but I need this due to the way I post new content to this microblog.

The action is using denoland/deployctl to deploy to Deno Deploy, and I wanted to have the action ping Slack at the end, and include the deployment url, to make it easy to just click that to share with a client. The id in that step is critical, as you refer to it in the "Ping Slack" step, referencing via steps.deploy.outputs.url.

Serving with Security Headers

The default entrypoint for deployctl is a remote file:

entrypoint: https://deno.land/std@0.140.0/http/file_server.ts # wraps the static output files

This serves up the site just fine, but if you want security headers or a 404, one option is to use a local file as follows:

  1. copy and save the sample index.ts to your Hugo project's static folder.
  2. in deploy.yml, set the entrypoint for deployctl to index.ts instead of the URI.

It has the same effect of wrapping and serving the site's files, as well as injecting whatever headers you need and providing a very simple text-only 404.

Acknowledgements

We ride on the backs of giants. Thanks to:

image

name: Deploy to Deno Deploy
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events for main or dev
# Deno Deploy project is linked to one branch, usually main, but will
# generate a non-production deploy URL for branches other than main
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
schedule:
# At 17:05 GMT every day, middle of the night in Japan.
- cron: '5 17 * * *'
# A workflow run is made up of one or more
# jobs that can run sequentially or in parallel
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
permissions:
id-token: write # Allows authentication with Deno Deploy.
contents: write # Allows cloning the repo. Need write this time, because we git push
steps:
- name: Clone repository
uses: actions/checkout@v3
with:
submodules: true # Fetch Hugo themes (true or recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest' # May fail if there's a breaking change, but just check logs for errors
extended: 'true' # Need extended for anything fancy
- name: Check
run: |
which hugo
hugo version
which jq
- name: Build site public with Hugo
env:
PRODBTOKEN15331: ${{ secrets.PRODBTOKEN15331 }}
run: |
DT=`date +'%Y%m%d-%H%M%S'`
HUGOBASEURL="https://logr.cogley.info"
npm install postcss postcss-cli autoprefixer
cd assets
git clone https://github.com/tachyons-css/tachyons-sass
cd ..
git config --global user.email "rick.cogley@esolia.co.jp"
git config --global user.name "Rick Cogley"
git add .
git diff-index --quiet HEAD || git commit -m "Logr update post $DT"
git push origin main
hugo --gc --minify --verbose --baseURL=$HUGOBASEURL --ignoreCache
- name: Deploy to Deno Deploy
id: deploy # needed to be able to get outputs from this step below
uses: denoland/deployctl@v1
with:
project: logr-cogley-info # the name of the project on Deno Deploy
entrypoint: https://deno.land/std@0.140.0/http/file_server.ts # wraps the static output files
root: public # Where the built HTML/CSS/JS files are located.
- name: Check folder contents and output
run: |
ls
echo "====== PUBLIC ======"
ls public
echo "====== STEPS ======"
echo '${{ toJSON(steps) }}'
- name: Ping Slack esolia-websites channel
uses: bryannice/gitactions-slack-notification@2.0.0
env:
SLACK_INCOMING_WEBHOOK: ${{ secrets.SLACK_INCOMING_WEBHOOK }}
SLACK_TITLE: 'Deno Deploy'
SLACK_MESSAGE: 'Commit: ${{ github.sha }} on ${{ github.ref }} from ${{ github.repository }}. Deployment URL: ${{ steps.deploy.outputs.url }}'
import { Application } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
// Logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.headers.get("X-Response-Time");
console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
});
// Timing
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.response.headers.set("X-Response-Time", `${ms}ms`);
ctx.response.headers.set("X-Custom-Header", "Rawr eSolia");
ctx.response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
ctx.response.headers.set("X-Frame-Options", "SAMEORIGIN");
ctx.response.headers.set("Referrer-Policy", "strict-origin");
ctx.response.headers.set("X-Content-Type-Options", "nosniff");
ctx.response.headers.set("X-Powered-By", "Blood Sweat Tears");
ctx.response.headers.set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=*, autoplay=(self), battery=(self), camera=(), cross-origin-isolated=*, fullscreen=*, geolocation=(self), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), usb=()");
});
app.use(async (ctx) => {
try {
await ctx.send({
root: `${Deno.cwd()}/`,
index: "index.html",
});
} catch {
ctx.response.status = 404;
ctx.response.body = "404 File not found";
}
});
await app.listen({ port: 8000 });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment