Skip to content

Commit 54f292c

Browse files
authored
Build Caddyfile from snapshot versions provided by the homepage (#13)
https://delpa.org/snapshot_versions.json
1 parent 1be0e0e commit 54f292c

11 files changed

+211
-60
lines changed

.github/workflows/lint.yml

-8
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,5 @@ jobs:
4040
- name: Install Dependencies
4141
run: npm ci
4242

43-
- name: Install Caddy
44-
run: |
45-
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
46-
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
47-
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
48-
sudo apt update
49-
sudo apt install caddy
50-
5143
- name: Lint
5244
run: npm run lint
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# @license AGPL-3.0-or-later
2+
#
3+
# Copyright(C) 2025 Hong Xu <[email protected]>
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU Affero General Public License as
7+
# published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Affero General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License
16+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
name: Production Container Image Builds and Brief Tests
19+
20+
on:
21+
push:
22+
branches: ["master"]
23+
pull_request:
24+
branches: ["master"]
25+
26+
jobs:
27+
test:
28+
name: Production Container Image
29+
runs-on: ubuntu-24.04
30+
31+
steps:
32+
- uses: actions/[email protected]
33+
34+
- name: Use Node.js
35+
uses: actions/[email protected]
36+
with:
37+
node-version: 22
38+
cache: "npm"
39+
40+
- name: Install podman
41+
run: sudo apt-get update && sudo apt-get install -y podman
42+
43+
- name: Build
44+
run: npm run build_prod
45+
46+
- name: Start the container
47+
run: podman run --detach -p 3000:80 -p 3001:443 delpa-redirection-server
48+
49+
- name: Install npm dependencies
50+
run: npm ci
51+
52+
- name: Run test
53+
run: npm run test_prod

.github/workflows/runtime.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
run: npm run build
4545

4646
- name: Start the container
47-
run: podman run --detach -p 3000:80 -p 3001:443 app
47+
run: podman run --detach -p 3000:80 -p 3001:443 delpa-redirection-test-server
4848

4949
- name: Install npm dependencies
5050
run: npm ci

Caddyfile.base

-32
This file was deleted.

Dockerfile

+28-3
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,42 @@
1515
# You should have received a copy of the GNU Affero General Public License
1616
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1717

18-
FROM docker.io/node:22.12.0-alpine@sha256:6e80991f69cc7722c561e5d14d5e72ab47c0d6b6cfb3ae50fb9cf9a7b30fdf97 as builder
18+
# snapshot versions, either prod or test
19+
ARG snapshot_versions_type=prod
20+
21+
FROM docker.io/busybox:1.37.0@sha256:2919d0172f7524b2d8df9e50066a682669e6d170ac0f6a49676d54358fe970b5 as snapshot-versions-getter-base
22+
23+
# Production snapshot versions
24+
FROM snapshot-versions-getter-base as snapshot-versions-getter-prod
25+
26+
RUN wget https://delpa.org/snapshot_versions.json && \
27+
wget https://delpa.org/snapshot_versions.json.sha256
28+
29+
# Verify that snapshot_versions.json is in the sha256 checksum file and verify
30+
# checksum
31+
RUN grep snapshot_versions.json snapshot_versions.json.sha256 && \
32+
sha256sum -c snapshot_versions.json.sha256
33+
34+
# Test snapshot versions
35+
FROM snapshot-versions-getter-base as snapshot-versions-getter-test
36+
37+
COPY ./snapshot_versions.json .
38+
39+
FROM snapshot-versions-getter-${snapshot_versions_type} as snapshot-versions-getter
40+
41+
FROM docker.io/node:22.12.0-alpine@sha256:6e80991f69cc7722c561e5d14d5e72ab47c0d6b6cfb3ae50fb9cf9a7b30fdf97 as caddyfile-builder
42+
1943

2044
COPY package.json package-lock.json ./
2145
RUN npm install -g npm && npm install
2246
COPY . .
23-
RUN npx tsx gen_caddy.ts < Caddyfile.base > Caddyfile
47+
COPY --from=snapshot-versions-getter ./snapshot_versions.json .
48+
RUN npx tsx gen_caddy.ts > Caddyfile
2449

2550
FROM docker.io/caddy:2.8.4-alpine@sha256:e97e0e3f8f51be708a9d5fadbbd75e3398c22fc0eecd4b26d48561e3f7daa9eb
2651

2752
LABEL org.opencontainers.image.authors="Hong Xu <[email protected]>"
2853
LABEL org.opencontainers.image.title="Delpa Redirection Server"
2954
LABEL org.opencontainers.image.licenses=AGPL-3.0-or-later
3055

31-
COPY --from=builder Caddyfile /etc/caddy/
56+
COPY --from=caddyfile-builder Caddyfile /etc/caddy/

README.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,32 @@ https://github.com/delpa-org/melpa-snapshot-*.
66

77
For more information about Delpa, see https://delpa.org.
88

9+
## Build
10+
11+
You need [podman][] installed.
12+
13+
To build the server for production, run:
14+
15+
npm run build_prod
16+
917
## Development
1018

1119
You need [podman][] installed.
1220

13-
To build the server, run:
21+
To build the server for test, run:
1422

1523
npm run build
1624

1725
To start the server, run:
1826

1927
npm run start
2028

29+
After starting the test server, to run test:
30+
31+
npm run test
32+
33+
The `snapshot_versions.json` file in this repo is deliberately set wrong and for
34+
test purpose only. In production build, it will be replaced by the file grabbed
35+
from https://delpa.org/snapshot_versions.json.
36+
2137
[podman]: https://podman.io/

gen_caddy.ts

+23-6
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@
1616
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

19-
/** Generate caddy file.
20-
* Currently simply redirect stdin to stdout.
21-
*/
19+
/** Generate the caddyfile. */
20+
21+
import snapshotVersions from "./snapshot_versions.json" with { type: "json" };
22+
23+
const snapVersionsRegexp = snapshotVersions.join("|");
24+
25+
const caddyfile = `
26+
{
27+
admin off
28+
}
29+
30+
{$HOST_ADDRESS:localhost} {
31+
redir / https://delpa.org permanent
32+
33+
respond /health-check "OK"
2234
23-
import fs from "fs";
35+
@snapshot path_regexp ^/snapshot/(${snapVersionsRegexp})/(.*)$
36+
redir @snapshot https://raw.githubusercontent.com/delpa-org/melpa-snapshot-{re.1}/refs/heads/master/packages/{re.2} permanent
2437
25-
const data = fs.readFileSync(0, "utf-8");
38+
respond "404 Not Found" 404 {
39+
close
40+
}
41+
}
42+
`;
2643

27-
console.log(data);
44+
console.log(caddyfile);

index.test.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ test("/health-check returns 200 with OK", async () => {
3939
describe("/snapshot", () => {
4040
const snapshotFirstPathComp = "snapshot" as const;
4141
for (const [name, path] of [
42-
["valid with a one-level subdir", "2025-01-02/a"],
43-
["valid with a one-level subdir with a trailing slash", "2025-01-02/a/"],
44-
["valid with a two-level subdir", "2025-01-02/a/b"],
45-
["valid with a two-level subdir with a trailing slash", "2025-01-02/a/b/"],
42+
["valid with a one-level subdir", "2024-01-01/a"],
43+
["valid with a one-level subdir with a trailing slash", "2024-01-01/a/"],
44+
["valid with a two-level subdir", "2024-01-01/a/b"],
45+
["valid with a two-level subdir with a trailing slash", "2024-01-01/a/b/"],
4646
] as const) {
4747
test(`Redirect with valid URL under /shapshot: ${name}`, async () => {
4848
const response = await fetch(
@@ -55,7 +55,7 @@ describe("/snapshot", () => {
5555
expect(response.status).toBe(301);
5656
expect(response.headers.get("location")).toBe(
5757
delpaGitHubRawBaseUrl +
58-
"/melpa-snapshot-2025-01-02/refs/heads/master/packages/" +
58+
"/melpa-snapshot-2024-01-01/refs/heads/master/packages/" +
5959
path.slice(
6060
path.indexOf("/") + 1, // Remove the top-level folder in path
6161
),
@@ -64,6 +64,8 @@ describe("/snapshot", () => {
6464
}
6565

6666
for (const [name, path] of [
67+
["non-existing snapshot", "2025-01-01"],
68+
["non-existing partially-matched snapshot", "2024-01"],
6769
["non-existing with no subdir", "non-existing"],
6870
["non-existing with no subdir but with a trailing slash", "non-existing/"],
6971
["non-existing with a one-level subdir", "non-existing/a"],

package.json

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
"author": "Hong Xu <[email protected]>",
88
"type": "module",
99
"scripts": {
10-
"lint": "npm run compile && eslint *.ts *.mjs && prettier . --check && caddy fmt --diff Caddyfile.base",
11-
"build": "podman build . -t app",
12-
"start": "podman run -it --rm -p 3000:80 -p 3001:443 app",
10+
"lint": "npm run compile && eslint *.ts *.mjs && prettier . --check",
11+
"build": "podman build --build-arg snapshot_versions_type=test . -t delpa-redirection-test-server",
12+
"build_prod": "podman build . -t delpa-redirection-server",
13+
"start": "podman run -it --rm -p 3000:80 -p 3001:443 delpa-redirection-test-server",
1314
"test": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest --run index.test.ts",
14-
"format": "eslint --fix *.ts *.mjs && prettier . --write && caddy fmt --overwrite Caddyfile.base",
15+
"test_prod": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest --run prod.test.ts",
16+
"format": "eslint --fix *.ts *.mjs && prettier . --write",
1517
"compile": "tsc -p tsconfig.json"
1618
},
1719
"devDependencies": {

prod.test.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/** @license AGPL-3.0-or-later
2+
*
3+
* Copyright(C) 2025 Hong Xu <[email protected]>
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
/** Briefly test a production image. Due to the changing snapshot versions and
20+
* netowrk dependencies when building an image, this isn't as thorough as the
21+
* test image.
22+
*/
23+
24+
/** Host address. */
25+
const hostAddress = process.env.HOST_ADDRESS ?? "https://localhost:3001";
26+
27+
const delpaGitHubRawBaseUrl =
28+
"https://raw.githubusercontent.com/delpa-org" as const;
29+
30+
test("/ redirects to Delpa homepage", async () => {
31+
const response = await fetch(`${hostAddress}/`, {
32+
redirect: "manual",
33+
});
34+
expect(response.status).toBe(301);
35+
expect(response.headers.get("location")).toBe("https://delpa.org");
36+
});
37+
38+
test("/health-check returns 200 with OK", async () => {
39+
const response = await fetch(`${hostAddress}/health-check`, {
40+
redirect: "manual",
41+
});
42+
expect(response.status).toBe(200);
43+
expect(await response.text()).toBe("OK");
44+
});
45+
46+
describe("/snapshot", () => {
47+
const snapshotFirstPathComp = "snapshot" as const;
48+
test("Redirect with valid URL under /shapshot", async () => {
49+
const response = await fetch(
50+
`${hostAddress}/${snapshotFirstPathComp}/2025-01-02/subpath`,
51+
{
52+
redirect: "manual",
53+
},
54+
);
55+
56+
expect(response.status).toBe(301);
57+
expect(response.headers.get("location")).toBe(
58+
delpaGitHubRawBaseUrl +
59+
"/melpa-snapshot-2025-01-02/refs/heads/master/packages/subpath",
60+
);
61+
});
62+
63+
test("Return 404 with invalid URL under /shapshot", async () => {
64+
const response = await fetch(
65+
`${hostAddress}/${snapshotFirstPathComp}/non-existing`,
66+
{
67+
redirect: "manual",
68+
},
69+
);
70+
71+
expect(response.status).toBe(404);
72+
expect(response.headers.get("content-type")).toContain("text/plain");
73+
expect(await response.text()).toBe("404 Not Found");
74+
});
75+
});

snapshot_versions.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["2024-01-01", "2024-02-02", "2024-03-03"]

0 commit comments

Comments
 (0)