Skip to content
Open
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
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: smoke

on:
push:
pull_request:
schedule:
# Weekly. The point of a scheduled run is that a change in a published
# `@indiekit/*` package fails here, rather than the first time somebody
# clones this repository and finds it does not work.
- cron: "0 7 * * 1"

jobs:
smoke:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24]
mongodb-version: [8]
env:
PUBLICATION_URL: http://localhost:8090
SECRET: ci-secret-not-used-outside-this-job
# Placeholders. `test/indiekit.config.ci.js` points the content store at
# the stub in `test/github-stub.mjs`, so nothing reaches api.github.com
# and no repository is written to. No secrets are needed, which also
# means this workflow runs on pull requests from forks.
GITHUB_USER: example-user
GITHUB_REPO: example-repo
GITHUB_BRANCH: main
GITHUB_TOKEN: stub-token-not-real
GITHUB_API_PORT: 3001
MASTODON_URL: https://mastodon.example
MASTODON_USER: "@example"
MASTODON_ACCESS_TOKEN: stub-token-not-real
MONGO_URL: mongodb://localhost:27017/example-config
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
# Deleting a post needs the posts collection, so a database is required.
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.12.1
with:
mongodb-version: ${{ matrix.mongodb-version }}
# `npm ci` installs exactly what the lockfile pins, so a scheduled run
# using it would re-test the same versions for ever. The weekly run
# resolves the `^1.0.0-beta` ranges instead — what a new clone actually
# gets — so a change in a published package shows up here.
- name: Install dependencies
run: ${{ github.event_name == 'schedule' && 'npm install --no-audit' || 'npm ci' }}

# The test starts and stops the server itself, so there is no instance
# for it to publish into by mistake.
- run: npm run smoke
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,27 @@ docker compose up --build
Click the button to use this configuration as the basis of a new service deployed with Railway:

[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/gEboK6?referralCode=bCd1gL)

## Smoke test

A single test checks that this configuration still works: it publishes a note
through the Micropub API, verifies the file the content store received, and
deletes it again.

The GitHub content store is pointed at a local stub (`test/github-stub.mjs`)
through its `baseUrl` option, so the test needs no access token, makes no
network request, and writes to no repository — while still exercising the
plug-ins, preset, publication and syndicator options configured here.

It runs on every push and weekly, so a change in a published `@indiekit/*`
package is caught here rather than by the next person to clone this repository.

The test starts and stops its own server, and refuses to run if the ports it
needs are already in use, so it cannot publish into a server it did not start.
All it needs is a MongoDB — deleting a post requires one — and the `.env`
described above:

```sh
npm install
npm run smoke
```
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"url": "git+https://github.com/getindiekit/example-config.git"
},
"scripts": {
"start": "indiekit serve"
"start": "indiekit serve",
"smoke": "node test/smoke.mjs"
},
"dependencies": {
"@indiekit/indiekit": "^1.0.0-beta",
Expand All @@ -34,5 +35,8 @@
"npm": ">=11"
},
"type": "module",
"private": true
"private": true,
"devDependencies": {
"jsonwebtoken": "^9.0.2"
}
}
83 changes: 83 additions & 0 deletions test/github-stub.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import http from "node:http";

/**
* A stand-in for the GitHub contents API, holding files in memory.
*
* `@indiekit/store-github` takes a `baseUrl`, so pointing it here exercises the
* real store, preset and Micropub endpoint without a token, a network call, or
* a write to anybody’s repository.
* The port is fixed rather than ephemeral: Indiekit is started before the test
* runs and needs the URL up front.
* @see {@link https://docs.github.com/en/rest/repos/contents}
* @param {number} port - Port to listen on
* @returns {Promise<object>} Server, its base URL, and the files it holds
*/
export const startGithubStub = async (port) => {
/** @type {Map<string, {content: string, sha: string}>} */
const files = new Map();
/** @type {object[]} Bodies of every write, so the test can check the store
* sent what the GitHub API actually requires, not merely that it sent
* something this permissive stub was willing to accept. */
const writes = [];
let counter = 0;

const server = http.createServer((request, response) => {
// Everything after `/contents/`, minus the `?ref=` the store appends
const [pathname] = request.url.split("?");
const filePath = decodeURIComponent(
pathname.replace(/^\/repos\/[^/]+\/[^/]+\/contents\//, ""),
);

const send = (status, body) => {
response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify(body));
};

let body = "";
request.on("data", (chunk) => (body += chunk));
request.on("end", () => {
const sent = body ? JSON.parse(body) : {};

switch (request.method) {
case "GET": {
const file = files.get(filePath);
// A miss must not be 2xx: `createFile` reads this to decide whether
// the file already exists, and would skip the write if it were.
return file ? send(200, file) : send(404, { message: "Not Found" });
}

case "PUT": {
const sha = `sha${++counter}`;
writes.push({ filePath, ...sent });
files.set(filePath, { content: sent.content, sha });
return send(201, {
content: { html_url: `https://github.example/${filePath}`, sha },
});
}

case "DELETE": {
files.delete(filePath);
return send(200, { commit: { sha: `sha${++counter}` } });
}

default: {
return send(405, { message: "Method Not Allowed" });
}
}
});
});

await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));

return {
baseUrl: `http://127.0.0.1:${port}`,
/** @returns {string|undefined} Decoded file content */
read: (filePath) => {
const file = files.get(filePath);
return file && Buffer.from(file.content, "base64").toString("utf8");
},
paths: () => [...files.keys()],
writes: () => writes,
close: () => new Promise((resolve) => server.close(resolve)),
};
};
13 changes: 13 additions & 0 deletions test/indiekit.config.ci.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import process from "node:process";

import config from "../indiekit.config.js";

/**
* The published configuration, with only the GitHub API endpoint redirected at
* the stub in `test/github-stub.mjs`. Everything else — plug-ins, preset,
* publication and syndicator options — is exactly what this repository ships,
* so the smoke test exercises the real configuration rather than a copy of it.
*/
config["@indiekit/store-github"].baseUrl = process.env.GITHUB_API_URL;

export default config;
Loading