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
78 changes: 39 additions & 39 deletions .github/workflows/sitemap-parity.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
name: Sitemap Parity

# Ensures every URL currently listed in the production sitemap is still
# reachable on the deployment that will be produced by this branch. Runs on:
# - pull_request -> waits for the Netlify deploy-preview, then checks
# - push to main -> waits for the Netlify production deploy, then checks
# reachable on the Coolify PR preview built from this branch.
#
# In both cases the job blocks until Netlify posts a successful commit status
# for the exact commit under test (Netlify integrates via the GitHub Commit
# Status API, not the Deployments API), then runs the check against the URL
# from that status. If any prod URL is unreachable on the new deploy, the
# check fails and blocks the merge.
# Coolify reports preview deployments through a single PR comment per
# application (posted by the Coolify GitHub App and edited in place on every
# deploy), not through commit statuses or the Deployments API. The job polls
# that comment until it reports "ready" with an update newer than the event
# that triggered this run, then checks against the preview URL in the comment.
# If any prod URL is unreachable on the preview, the check fails and blocks
# the merge.

on:
pull_request:
Expand All @@ -34,61 +34,60 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0

- name: Wait for Netlify deployment
- name: Wait for Coolify preview deployment
id: wait
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# Only a comment edited after the push/open that triggered this run
# can describe this commit's deployment; older ones are stale.
SINCE: ${{ github.event.pull_request.updated_at }}
COOLIFY_BOT: openelements-coolify-integration[bot]
# Name of the Coolify application, as shown in bold in its comment.
COOLIFY_APP: Open Elements Website
# Total wait budget in seconds (default 15 min) and poll interval.
WAIT_TIMEOUT: '900'
WAIT_INTERVAL: '15'
run: |
set -euo pipefail
echo "Waiting for a Netlify commit status on commit $SHA ..."
echo "Waiting for the Coolify preview of '$COOLIFY_APP' on PR #$PR_NUMBER (updated after $SINCE) ..."

# Netlify reports build status via the GitHub Commit Status API
# (context typically `netlify/<site-name>/deploy-preview`) rather
# than the Deployments API, so we poll that endpoint.
deadline=$(( $(date +%s) + WAIT_TIMEOUT ))
preview_url=""

while [ "$(date +%s)" -lt "$deadline" ]; do
status_json=$(gh api "repos/${{ github.repository }}/commits/$SHA/status")
comment_json=$(gh api --paginate "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" \
| jq -s --arg bot "$COOLIFY_BOT" --arg app "$COOLIFY_APP" '
add
| map(select(.user.login == $bot and (.body | contains("**" + $app + "**"))))
| sort_by(.updated_at) | last // {}')
updated_at=$(printf '%s' "$comment_json" | jq -r '.updated_at // ""')
body=$(printf '%s' "$comment_json" | jq -r '.body // ""')

# Grab the first status whose context looks like a Netlify one.
# `map(select(...)) | .[0]` normalises the jq stream into a single
# (possibly null) object so downstream extraction is unambiguous.
netlify_state=$(printf '%s' "$status_json" | jq -r '
.statuses
| map(select(.context | ascii_downcase | startswith("netlify/")))
| .[0].state // ""')
netlify_target=$(printf '%s' "$status_json" | jq -r '
.statuses
| map(select(.context | ascii_downcase | startswith("netlify/")))
| .[0].target_url // ""')

if [ "$netlify_state" = "success" ]; then
if [ -z "$netlify_target" ] || [ "$netlify_target" = "null" ]; then
echo "::error::Netlify status is success but has no target_url."
if [ -z "$updated_at" ]; then
echo "No Coolify comment yet. Waiting ${WAIT_INTERVAL}s ..."
elif [[ ! "$updated_at" > "$SINCE" ]]; then
echo "Coolify comment is from an earlier deployment ($updated_at). Waiting ${WAIT_INTERVAL}s ..."
elif [[ "$body" == *"is ready."* ]]; then
preview_url=$(printf '%s' "$body" | sed -n 's/.*\[Open Preview\](\([^)]*\)).*/\1/p' | head -n1)
if [ -z "$preview_url" ]; then
echo "::error::Coolify reports ready but the comment has no preview link."
exit 1
fi
preview_url="$netlify_target"
echo "Found successful Netlify deployment -> $preview_url"
echo "Found ready Coolify preview -> $preview_url"
break
elif [ "$netlify_state" = "failure" ] || [ "$netlify_state" = "error" ]; then
echo "::error::Netlify build reported state=$netlify_state for commit $SHA."
elif [[ "$body" == *"failed."* ]]; then
echo "::error::Coolify preview deployment failed for PR #$PR_NUMBER. See the build logs linked in the PR comment."
exit 1
elif [ -n "$netlify_state" ]; then
echo "Netlify status is currently: $netlify_state. Waiting ${WAIT_INTERVAL}s ..."
else
echo "No Netlify status posted yet. Waiting ${WAIT_INTERVAL}s ..."
echo "Coolify preview is still deploying. Waiting ${WAIT_INTERVAL}s ..."
fi

sleep "$WAIT_INTERVAL"
done

if [ -z "$preview_url" ]; then
echo "::error::Timed out after ${WAIT_TIMEOUT}s waiting for a Netlify deployment on $SHA."
echo "::error::Timed out after ${WAIT_TIMEOUT}s waiting for the Coolify preview of PR #$PR_NUMBER."
exit 1
fi

Expand All @@ -103,4 +102,5 @@ jobs:
--prod-base=https://open-elements.com \
--preview-base="$PREVIEW_URL" \
--concurrency=4 \
--retries=4
--retries=4 \
--ignore=/en/newsletter
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ test-results/

next-env.d.ts
.claude/settings.local.json

# TypeScript incremental build info
*.tsbuildinfo
4 changes: 4 additions & 0 deletions nixpacks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ cmds = ["pnpm install --frozen-lockfile"]

[phases.build]
cmds = ["pnpm run build"]
# Explicit list replaces the provider defaults, which include a cache mount on
# tsconfig.tsbuildinfo. Cache mounts are directories, so mounting one over that
# file breaks the build ("not a directory").
cacheDirectories = [".next/cache", "node_modules/.cache"]

# Runs `next start` (SSR, middleware, API routes) on $PORT, which Coolify injects.
[start]
Expand Down
19 changes: 0 additions & 19 deletions pnpm-lock.yaml

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

25 changes: 18 additions & 7 deletions scripts/check-sitemap-parity.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Verify that every URL currently listed in the production sitemap is still
* reachable on a preview deployment (typically a Netlify PR preview).
* reachable on a preview deployment (typically a Coolify PR preview).
*
* How it works:
* 1. Downloads `${prodBase}/en/sitemap.xml` and `${prodBase}/de/sitemap.xml`
Expand All @@ -17,10 +17,11 @@
*
* Usage:
* node scripts/check-sitemap-parity.mjs \
* --preview-base=https://deploy-preview-42--my-site.netlify.app \
* --preview-base=https://42.open-elements.cloud \
* [--prod-base=https://open-elements.com] \
* [--concurrency=8] \
* [--timeout=15000]
* [--timeout=15000] \
* [--ignore=/en/some-path] (repeatable; exact path, skipped in the check)
*
* Exits 0 if every prod URL is reachable on the preview, 1 otherwise.
*/
Expand All @@ -38,6 +39,7 @@ function parseArgs(argv) {
concurrency: 4,
timeoutMs: 15000,
retries: 3,
ignore: new Set(),
};
for (const arg of argv.slice(2)) {
if (arg.startsWith('--prod-base=')) {
Expand All @@ -50,6 +52,8 @@ function parseArgs(argv) {
out.timeoutMs = Number(arg.slice('--timeout='.length));
} else if (arg.startsWith('--retries=')) {
out.retries = Number(arg.slice('--retries='.length));
} else if (arg.startsWith('--ignore=')) {
out.ignore.add(arg.slice('--ignore='.length));
} else if (arg === '--help' || arg === '-h') {
out.help = true;
} else {
Expand Down Expand Up @@ -179,7 +183,7 @@ async function main() {
const args = parseArgs(process.argv);
if (args.help || !args.previewBase) {
console.error(
'Usage: node scripts/check-sitemap-parity.mjs --preview-base=<url> [--prod-base=<url>] [--concurrency=N] [--timeout=ms]',
'Usage: node scripts/check-sitemap-parity.mjs --preview-base=<url> [--prod-base=<url>] [--concurrency=N] [--timeout=ms] [--ignore=<path>]...',
);
process.exit(args.help ? 0 : 2);
}
Expand Down Expand Up @@ -219,10 +223,17 @@ async function main() {
for (const p of missingFromPreviewSitemap) console.log(` - ${p}`);
}

console.log('');
console.log(`Checking reachability of ${prodPaths.size} paths on preview...`);
const ignored = [...prodPaths].filter(p => args.ignore.has(p)).sort();
if (ignored.length > 0) {
console.log('');
console.log(`Ignoring ${ignored.length} path(s) passed via --ignore:`);
for (const p of ignored) console.log(` ~ ${p}`);
}

const pathList = [...prodPaths].sort();
const pathList = [...prodPaths].filter(p => !args.ignore.has(p)).sort();

console.log('');
console.log(`Checking reachability of ${pathList.length} paths on preview...`);
const results = await runWithConcurrency(
pathList,
args.concurrency,
Expand Down
2 changes: 1 addition & 1 deletion src/data/de/team.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
"firstName": "Daniel",
"lastName": "Ntege",
"link": "/employees/daniel",
"bio": "Daniel ist ein leidenschaftlicher Softwareentwickler mit einem starken Interesse an Open-Source-Technologien. Er arbeitet gerne an Projekten, die Innovation und das Wachstum der Gemeinschaft fördern. Mit einem Hintergrund in Informatik widmet sich Daniel dem kontinuierlichen Lernen und dem Beitrag zum Technologie-Ökosystem.",
"bio": "Daniel ist Software Engineer bei Open Elements und arbeitet remote aus Uganda mit einem Schwerpunkt auf Open Source und Decentralized Trust. Er ist Maintainer im Hiero-Projekt der Linux Foundation Decentralized Trust, wo er am Hiero Python SDK und an Community-Tooling arbeitet und neue Contributors beim Einstieg unterstützt. Außerdem ist er Maintainer von Record Store, dem selbst gehosteten, S3-kompatiblen Object-Storage-Service von Open Elements.",
"picture": "/team/daniel.jpg",
"role": "Software Engineer",
"socials": [
Expand Down
2 changes: 1 addition & 1 deletion src/data/en/team.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
"firstName": "Daniel",
"lastName": "Ntege",
"link": "/employees/daniel",
"bio": "Daniel is a passionate software developer with a keen interest in open source technologies. He enjoys collaborating on projects that drive innovation and community growth. With a background in computer science, Daniel is dedicated to continuous learning and contributing to the tech ecosystem.",
"bio": "Daniel is a software engineer at Open Elements, working remotely from Uganda with a focus on open source and decentralized trust. He is a maintainer in the Hiero project under the Linux Foundation Decentralized Trust, where he works on the Hiero Python SDK and community tooling, and helps new contributors get started. He is also a maintainer of Record Store, Open Elements' self-hosted, S3-compatible object storage service.",
"picture": "/team/daniel.jpg",
"role": "Software Engineer",
"socials": [
Expand Down
1 change: 0 additions & 1 deletion tsconfig.tsbuildinfo

This file was deleted.

Loading