Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches:
- master
schedule:
# daily check; builds only if a scheduled post came due or the last deploy is 30+ days old
- cron: "0 2 * * *"
workflow_dispatch:

permissions:
Expand All @@ -17,7 +20,23 @@ concurrency:
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
outputs:
publish: ${{ steps.due.outputs.publish }}
steps:
- uses: actions/checkout@v7
- id: due
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
run: node scripts/posts-due.mjs
build:
needs: check
if: needs.check.outputs.publish == 'true'
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.MODULE_FETCH_GITHUB_TOKEN }}
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ THe lower part is your blog post content. You'll also see an example of how to i
- `description` is a short one or two line summary of your blog post
- `author` is your name / nickname / alias - we recommend using either your GitHub or Discord name
- `date` is the date you want your blog post to be published aka. go online (in ISO form: `YYYY-MM-DD`)
(this feature is no longer supported, your blog post will always be visible irrespective of the date you set)
A post dated in the future stays hidden until the first build on or after that date. A daily check at 02:00 UTC starts that build on the day a post comes due; to publish sooner, run the "Publish website on GitHub Pages" workflow from the Actions tab.
`npm run dev` shows scheduled posts so you can preview them.
- `tags` should always be an array, multiple values are possible
(please tag TeraSaturday posts with "TeraSaturday")

Expand Down
2 changes: 1 addition & 1 deletion docs/project-deployment.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Deployment

The website is deployed to [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages) by the `deploy.yml` workflow on every push to `master` (or manually from the Actions tab).
The website is deployed to [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages) by the `deploy.yml` workflow on every push to `master`, or manually from the Actions tab. A daily check at 02:00 UTC (`scripts/posts-due.mjs`) also starts a build when a blog post's date has passed since the last successful deploy, or when that deploy is over 30 days old (which refreshes the module and issue data), and does nothing otherwise.

The workflow installs dependencies with `npm ci`, runs `npm run build` with the `MODULE_FETCH_GITHUB_TOKEN` secret exposed as `GITHUB_TOKEN`, uploads `dist/` with `actions/upload-pages-artifact`, and publishes it with `actions/deploy-pages`.
The site is served from the domain root, so no base path is configured.
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default [
...tseslint.configs.recommended,
...astro.configs.recommended,
{
files: ["**/*.{js,jsx,ts}"],
files: ["**/*.{js,jsx,mjs,ts}"],
languageOptions: {
parserOptions: { ecmaFeatures: { jsx: true } },
globals: { ...globals.browser, ...globals.node },
Expand Down
46 changes: 46 additions & 0 deletions scripts/posts-due.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Decides whether a deploy run has anything to publish. Scheduled runs only build
// when a post's date has passed since the last successful deploy, or when that
// deploy is over REFRESH_DAYS old (to refresh module and issue data); every other
// trigger (push, manual) always builds. Writes `publish=true|false` to $GITHUB_OUTPUT.
import { execFileSync } from "node:child_process";
import { appendFileSync, readdirSync, readFileSync } from "node:fs";

const REFRESH_DAYS = Number(process.env.REFRESH_DAYS ?? 30);

function lastSuccessfulDeploy() {
const repo = process.env.GITHUB_REPOSITORY;
const json = execFileSync(
"gh",
[
...["run", "list", "--workflow", "deploy.yml", "--status", "success"],
...["--limit", "1", "--json", "createdAt"],
...(repo ? ["--repo", repo] : []),
],
{ encoding: "utf8" },
);
const [run] = JSON.parse(json);
return run ? new Date(run.createdAt) : null;
}

function postDates() {
return readdirSync("blog").flatMap((dir) => {
try {
const text = readFileSync(`blog/${dir}/index.md`, "utf8");
const date = /^date:\s*["']?(.*?)["']?\s*$/m.exec(text)?.[1];
return date ? [new Date(date)] : [];
} catch {
return [];
}
});
}

let publish = true;
if (process.env.EVENT === "schedule") {
const since = lastSuccessfulDeploy();
const now = new Date();
const stale = since !== null && now - since > REFRESH_DAYS * 24 * 3600e3;
publish = since === null || stale || postDates().some((d) => d > since && d <= now);
}

console.log(`publish=${publish}`);
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `publish=${publish}\n`);
15 changes: 11 additions & 4 deletions src/integrations/blog-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import type { AstroIntegration } from "astro";
import kebabCase from "lodash.kebabcase";
import { isPublished } from "../lib/published";

/** Map of post slug -> post directory; slug = kebab-cased frontmatter title. */
async function postDirs(blogDir: string): Promise<Map<string, string>> {
/**
* Map of post slug -> post directory; slug = kebab-cased frontmatter title.
* With `publishedOnly`, posts dated in the future are left out.
*/
async function postDirs(blogDir: string, publishedOnly = false): Promise<Map<string, string>> {
const dirs = new Map<string, string>();
for (const dir of await readdir(blogDir)) {
const md = path.join(blogDir, dir, "index.md");
if (!existsSync(md)) continue;
const title = /^title:\s*["']?(.*?)["']?\s*$/m.exec(await readFile(md, "utf8"))?.[1];
const text = await readFile(md, "utf8");
const title = /^title:\s*["']?(.*?)["']?\s*$/m.exec(text)?.[1];
const date = /^date:\s*["']?(.*?)["']?\s*$/m.exec(text)?.[1];
if (publishedOnly && date && !isPublished(new Date(date))) continue;
if (title) dirs.set(kebabCase(title), path.join(blogDir, dir));
}
return dirs;
Expand Down Expand Up @@ -44,7 +51,7 @@ export default function blogAssets(): AstroIntegration {
},
"astro:build:done": async ({ dir, logger }) => {
const out = fileURLToPath(dir);
const dirs = await postDirs(path.join(root, "blog"));
const dirs = await postDirs(path.join(root, "blog"), true);
for (const [slug, src] of dirs) {
await cp(src, path.join(out, "blog", slug), {
recursive: true,
Expand Down
4 changes: 3 additions & 1 deletion src/lib/blog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getImage } from "astro:assets";
import { getCollection, type CollectionEntry } from "astro:content";
import { excerpt } from "./format";
import { isPublished } from "./published";

export type Post = CollectionEntry<"blog">;

Expand All @@ -17,7 +18,8 @@ export interface CardData {
}

export async function getPosts(): Promise<Post[]> {
const posts = await getCollection("blog");
// `astro dev` also lists scheduled posts, so authors can preview them.
const posts = await getCollection("blog", (p) => import.meta.env.DEV || isPublished(p.data.date));
return posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/published.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** A post is live once its date has arrived; later-dated posts wait for a build on or after that date. */
export const isPublished = (date: Date, now: Date = new Date()): boolean =>
date.getTime() <= now.getTime();
Loading