Skip to content

Commit ca7c86a

Browse files
committed
optimize CI workflow and add prepare-build script
1 parent ed7d654 commit ca7c86a

5 files changed

Lines changed: 227 additions & 47 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ jobs:
5050
run: pnpm check
5151

5252
build:
53-
name: Build
53+
name: Build & Validate
5454
runs-on: ubuntu-22.04
55-
needs: [lint]
5655

5756
steps:
5857
- uses: actions/checkout@v6
@@ -66,44 +65,23 @@ jobs:
6665
node-version: "22"
6766
cache: "pnpm"
6867

69-
- name: Install production dependencies
70-
run: pnpm install --frozen-lockfile --prod
68+
- name: Install dependencies
69+
run: pnpm install --frozen-lockfile
70+
71+
- name: Prepare source for CI build
72+
env:
73+
BASE_BRANCH: ${{ github.event.pull_request.base.ref }} # PR target branch
74+
BASE_REF: ${{ github.event_name == 'push' && github.event.before || '' }} # previous commit before the push event
75+
run: |
76+
ARGS="--limit-posts 10 --skip-og --limit-locales"
77+
if [ -n "$BASE_BRANCH" ]; then ARGS="$ARGS --base-branch $BASE_BRANCH"; fi
78+
if [ -n "$BASE_REF" ]; then ARGS="$ARGS --base-ref $BASE_REF"; fi
79+
pnpm prepare-build $ARGS
7180
7281
- name: Build site
82+
env:
83+
SKIP_IMAGE_OPTIMIZATION: "true"
7384
run: pnpm build
7485

75-
- name: Upload build artifacts
76-
uses: actions/upload-artifact@v7
77-
with:
78-
name: site-build
79-
path: dist/
80-
retention-days: 1
81-
82-
validate:
83-
name: Validate HTML
84-
runs-on: ubuntu-22.04
85-
needs: [build]
86-
87-
steps:
88-
- uses: actions/checkout@v6
89-
90-
- name: Setup pnpm
91-
uses: pnpm/action-setup@v4
92-
93-
- name: Setup Node.js
94-
uses: actions/setup-node@v6
95-
with:
96-
node-version: "22"
97-
cache: "pnpm"
98-
99-
- name: Download build artifacts
100-
uses: actions/download-artifact@v8
101-
with:
102-
name: site-build
103-
path: dist/
104-
105-
- name: Install dependencies
106-
run: pnpm install --frozen-lockfile
107-
10886
- name: Validate built HTML
10987
run: pnpm exec htmlhint 'dist/**/*.html'

Dockerfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ RUN pnpm install --frozen-lockfile
1616

1717
# Prepare source
1818
FROM base AS src
19-
ARG LIMIT_POSTS
20-
COPY --from=deps /app/node_modules ./node_modules
19+
ARG PREPARE_BUILD_ARGS
20+
COPY --from=deps-dev /app/node_modules ./node_modules
2121
COPY . .
22-
RUN if [ -n "$LIMIT_POSTS" ]; then \
23-
find src/content/blog -maxdepth 1 -name '[^_]*.md' | sort -r | tail -n +$((LIMIT_POSTS + 1)) | while IFS= read -r f; do rm -f "$f"; done; \
22+
RUN if [ -n "$PREPARE_BUILD_ARGS" ]; then \
23+
pnpm prepare-build $PREPARE_BUILD_ARGS; \
2424
fi
25+
COPY --from=deps /app/node_modules ./node_modules
2526

2627
# Build the site (static)
2728
FROM src AS build

README.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ All commands are run from the root of the project:
4343
| :--------------------- | :----------------------------------------------- |
4444
| `pnpm install` | Installs dependencies |
4545
| `pnpm dev` | Starts local dev server at `localhost:4321` |
46+
| `pnpm prepare-build` | Trim sources before `pnpm build` (see below) |
4647
| `pnpm build` | Build your production site to `./dist/` |
4748
| `pnpm preview` | Preview your build locally, before deploying |
4849
| `pnpm lint` | Run ESLint to check for code issues |
@@ -51,6 +52,31 @@ All commands are run from the root of the project:
5152
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
5253
| `pnpm astro -- --help` | Get help using the Astro CLI |
5354

55+
## Prepare build
56+
57+
It is possible to trim the source code before `pnpm build` to speed up preview deployments (fewer blog posts, fewer locales, no OG image generation).
58+
59+
```bash
60+
pnpm prepare-build [flags]
61+
```
62+
63+
Each flag has an env var equivalent, for build environments that don't accept CLI arguments.
64+
65+
| Flag | Env var | Description |
66+
| :---------------- | :------------------- | :-------------------------------------------------------- |
67+
| `--limit-posts N` | `LIMIT_POSTS=N` | Keep only the N most recent blog posts |
68+
| `--skip-og` | `SKIP_OG=true` | Remove OpenGraph image generation |
69+
| `--limit-locales` | `LIMIT_LOCALES=true` | Build only the default locale (+ locales with PR changes) |
70+
71+
Changed-file detection (pick one):
72+
73+
| Flag | Env var | Description |
74+
| :------------------- | :----------------- | :------------------------------------------------------- |
75+
| `--base-branch NAME` | `BASE_BRANCH=NAME` | Branch to diff against via merge-base (for PRs/previews) |
76+
| `--base-ref SHA` | `BASE_REF=SHA` | SHA to diff against directly (for push events) |
77+
78+
`SKIP_PREPARE_BUILD=true` early-exits the script. Useful if you can't set a per-environment build command in your platform, and have to skip the script in production.
79+
5480
## Docker
5581

5682
You can build and run the site without installing Node.js or pnpm. Five targets are available:
@@ -110,13 +136,13 @@ Then open [http://127.0.0.1:4321](http://127.0.0.1:4321).
110136

111137
### Build args
112138

113-
| Arg | Description | Example |
114-
| :------------------------- | :-------------------------------------------------- | :------ |
115-
| `LIMIT_POSTS` | Keep only the N most recent blog posts for previews | `6` |
116-
| `SKIP_IMAGE_OPTIMIZATION` | Disable image processing for faster builds | `true` |
139+
| Arg | Description | Example |
140+
| :------------------------- | :--------------------------------------------------- | :-------------------------- |
141+
| `PREPARE_BUILD_ARGS` | Flags for the [prepare-build](#prepare-build) script | `--limit-posts 6 --skip-og` |
142+
| `SKIP_IMAGE_OPTIMIZATION` | Disable image processing for faster builds | `true` |
117143

118144
```bash
119-
docker build --build-arg LIMIT_POSTS=6 --build-arg SKIP_IMAGE_OPTIMIZATION=true --target serve-ssr -t monero-site-ssr .
145+
docker build --build-arg PREPARE_BUILD_ARGS="--limit-posts 6 --skip-og" --build-arg SKIP_IMAGE_OPTIMIZATION=true --target serve-ssr -t monero-site-ssr .
120146
```
121147

122148
## More

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"lint:css": "stylelint \"src/**/*.{css,astro}\"",
1616
"lint:fix": "eslint . --fix",
1717
"format": "prettier --write .",
18-
"format:check": "prettier --check ."
18+
"format:check": "prettier --check .",
19+
"prepare-build": "tsx scripts/prepare-build.ts"
1920
},
2021
"dependencies": {
2122
"@astrojs/node": "^9.5.4",

scripts/prepare-build.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { execFileSync } from "node:child_process";
2+
import { readdirSync, rmSync, writeFileSync } from "node:fs";
3+
import { basename, join, relative } from "node:path";
4+
import { parseArgs } from "node:util";
5+
import { defaultLocale, locales, rtlLocales } from "../src/i18n/config";
6+
7+
if (process.env.SKIP_PREPARE_BUILD === "true") {
8+
console.log("Skipping: SKIP_PREPARE_BUILD is set");
9+
process.exit(0);
10+
}
11+
12+
const { values: args } = parseArgs({
13+
options: {
14+
"limit-posts": { type: "string" },
15+
"skip-og": { type: "boolean", default: false },
16+
"limit-locales": { type: "boolean", default: false },
17+
"base-branch": { type: "string" },
18+
"base-ref": { type: "string" },
19+
},
20+
});
21+
22+
const limitPosts = args["limit-posts"] ?? process.env.LIMIT_POSTS;
23+
const skipOg = args["skip-og"] || process.env.SKIP_OG === "true";
24+
const limitLocales =
25+
args["limit-locales"] || process.env.LIMIT_LOCALES === "true";
26+
const baseBranch = args["base-branch"] ?? process.env.BASE_BRANCH;
27+
const baseRefArg = args["base-ref"] ?? process.env.BASE_REF;
28+
29+
if (baseBranch && baseRefArg) {
30+
console.error("Cannot use both --base-branch and --base-ref");
31+
process.exit(1);
32+
}
33+
34+
const BLOG_DIR = "src/content/blog";
35+
const OG_ROUTE = "src/pages/open-graph/[...route].ts";
36+
const I18N_CONFIG = "src/i18n/config.ts";
37+
const I18N_DIR = "src/i18n/translations";
38+
39+
function git(...gitArgs: string[]): string {
40+
return execFileSync("git", gitArgs, { encoding: "utf-8" }).trim();
41+
}
42+
43+
function resolveRef(ref: string): string | undefined {
44+
try {
45+
return git("rev-parse", "--verify", ref);
46+
} catch {
47+
// ref not found, try fetching
48+
}
49+
50+
const remote = git("remote").split("\n").filter(Boolean)[0];
51+
if (!remote) return undefined;
52+
53+
try {
54+
if (git("rev-parse", "--is-shallow-repository") === "true") {
55+
git("fetch", "--unshallow", remote);
56+
}
57+
58+
git("fetch", remote, ref);
59+
return "FETCH_HEAD";
60+
} catch {
61+
return undefined;
62+
}
63+
}
64+
65+
function getChangedFiles(
66+
ref: string,
67+
useMergeBase: boolean,
68+
...dirs: string[]
69+
): string[] {
70+
const resolved = resolveRef(ref);
71+
if (!resolved) return [];
72+
73+
try {
74+
return git(
75+
"diff",
76+
"--name-only",
77+
...(useMergeBase ? ["--merge-base"] : []),
78+
resolved,
79+
"HEAD",
80+
"--",
81+
...dirs,
82+
)
83+
.split("\n")
84+
.filter(Boolean);
85+
} catch (e) {
86+
console.warn("Could not detect changed files");
87+
console.warn(e instanceof Error ? e.message : e);
88+
return [];
89+
}
90+
}
91+
92+
function changedFilesUnder(dir: string): string[] {
93+
return changed.filter((f) => f.startsWith(dir + "/"));
94+
}
95+
96+
function serializeI18nConfig(activeLocales: Set<string>): string {
97+
const entries = Object.entries(locales)
98+
.filter(([key]) => activeLocales.has(key))
99+
.map(([key, val]) => ` ${key}: "${val}",`);
100+
101+
const rtl = rtlLocales.map((l) => `"${l}"`).join(", ");
102+
103+
return [
104+
`export const defaultLocale = "${defaultLocale}";`,
105+
"export const locales = {",
106+
...entries,
107+
"};",
108+
`export const rtlLocales = [${rtl}];`,
109+
"",
110+
].join("\n");
111+
}
112+
113+
const baseRef = baseBranch ?? baseRefArg;
114+
const useMergeBase = Boolean(baseBranch);
115+
116+
let changed: string[] = [];
117+
if (baseRef) {
118+
const mode = useMergeBase ? "merge-base" : "direct";
119+
console.log(`Diff: ${mode} against ${baseRef}`);
120+
changed = getChangedFiles(baseRef, useMergeBase, BLOG_DIR, I18N_DIR);
121+
console.log(
122+
`Diff: ${changed.length} file${changed.length !== 1 ? "s" : ""} changed`,
123+
);
124+
} else {
125+
console.log("Diff: skipped");
126+
}
127+
128+
// Limit blog posts, keeping edited ones
129+
if (limitPosts) {
130+
const limit = Number(limitPosts);
131+
if (!Number.isFinite(limit) || limit < 1) {
132+
console.error(`Invalid --limit-posts value: ${limitPosts}`);
133+
process.exit(1);
134+
}
135+
136+
const isPost = (f: string) => /^\d/.test(f) && f.endsWith(".md");
137+
138+
const allPosts = readdirSync(BLOG_DIR).filter(isPost).sort().reverse();
139+
const keep = new Set(allPosts.slice(0, limit));
140+
141+
for (const file of changedFilesUnder(BLOG_DIR)) {
142+
if (isPost(basename(file))) keep.add(basename(file));
143+
}
144+
145+
let removed = 0;
146+
for (const file of allPosts) {
147+
if (keep.has(file)) continue;
148+
rmSync(join(BLOG_DIR, file));
149+
removed++;
150+
}
151+
152+
console.log(
153+
`Blog: kept ${allPosts.length - removed} of ${allPosts.length} posts`,
154+
);
155+
}
156+
157+
// Limit locales
158+
if (limitLocales) {
159+
const keepLocales = new Set([defaultLocale]);
160+
161+
for (const file of changedFilesUnder(I18N_DIR)) {
162+
const locale = relative(I18N_DIR, file).split("/")[0];
163+
if (locale && locale !== defaultLocale) keepLocales.add(locale);
164+
}
165+
166+
writeFileSync(I18N_CONFIG, serializeI18nConfig(keepLocales));
167+
console.log(`Locales: building ${Array.from(keepLocales).join(" ")}`);
168+
}
169+
170+
// OpenGraph removal
171+
if (skipOg) {
172+
rmSync(OG_ROUTE, { force: true });
173+
console.log(`OpenGraph: removed ${OG_ROUTE}`);
174+
}

0 commit comments

Comments
 (0)