CI pipelines, lint gates, environments (local/QA/production), zero-downtime deploy, release docs, footer version, the docs site build.
Part of the requirements index. Section IDs (02-§N.M) are stable and cited from code; they do not encode the file path.
§23.1–§23.13 are superseded — see Archived (superseded). §23.14 still applies to
ci.yml.
main to detect changed files must check out
with sufficient git history for the three-dot diff (origin/main...HEAD) to find a merge
base. A shallow checkout (depth 1) is not sufficient. The build generates HTML pages. Invalid HTML must be caught automatically
before merge. This closes the CL-§5.1 gap.
html-validate — a standard, offline HTML
validator with configurable rules. .html files in public/ after the
build step completes. lint:html npm script runs html-validate on public/*.html. ci.yml) runs lint:html after the build step. .htmlvalidate.json at the project root. CSS source files must be linted automatically before merge. This closes
the CL-§5.2 gap.
stylelint-config-standard as the
base configuration. .css files in source/assets/cs/. lint:css npm script runs Stylelint on the CSS source files. ci.yml) runs lint:css alongside the existing
lint steps. .stylelintrc.json at the project root. The static site deploy must use a staging-and-swap strategy that limits
downtime to milliseconds. The build output is uploaded to a staging directory
via SCP, then swapped into the live web root with server-side mv operations.
scp over SSH, not via FTP. public_html). domains/ directory
inside public_html by moving it into the new release before the
swap. mv operations on the same filesystem (milliseconds, not seconds). public_html_old)
and from any previous failed deploy (release_new) must be cleaned
up automatically. tar.gz archive
before upload, to avoid per-file transfer overhead. SERVER_HOST,
SERVER_USER, SERVER_SSH_KEY, SERVER_SSH_PORT. DEPLOY_DIR must be added, pointing to the domain
directory on the server (the parent of public_html). FTP_TARGET_DIR validation
step must be removed from the workflow. main, paths-ignore data files) must
remain unchanged. set -e so any failing command aborts
the deploy immediately. The project uses three environments — Local, QA, and Production — deployed
from a single main branch. Code changes are promoted to Production manually;
event data reaches both environments immediately.
main. workflow_dispatch trigger. main branch; no separate
production branch exists. main, regardless
of which environment the API runs in. qa. production. SITE_URL, API_URL, SERVER_HOST, SERVER_USER,
SERVER_SSH_KEY, SERVER_SSH_PORT, DEPLOY_DIR.
Production additionally requires: FTP_HOST, FTP_USERNAME,
FTP_PASSWORD, FTP_APP_DIR, FTP_TARGET_DIR.
QA no longer uses FTP secrets (see §43). .github/workflows/deploy-reusable.yml) must
contain the shared build-and-deploy logic. deploy-qa.yml must call the reusable workflow with environment
qa. deploy-prod.yml must call the reusable workflow with environment
production. deploy.yml must be removed after the split is
complete. event-data-deploy.yml must deploy the
event data pages to both QA and Production in
parallel. SITE_URL and API_URL so that per-event page links point to the
correct domain. build.js must use the SITE_URL environment
variable instead of a hardcoded domain. ci.yml does not need environment-scoped secrets; its SITE_URL
remains a repository-level secret. .env for all environment
variables. .env.example must document the environment management
setup. FTP transmits credentials in cleartext and requires a separate set of secrets
(FTP_HOST, FTP_USERNAME, FTP_PASSWORD). The static site deploy already
uses SCP/SSH. This section migrates the remaining FTP-based deploy steps to
SSH for the QA environment, reducing the attack surface and the number of
secrets to manage. Production remains on FTP until QA is validated.
deploy-qa job in event-data-deploy.yml must upload event data
pages via SCP over SSH instead of FTP. SERVER_HOST,
SERVER_USER, SERVER_SSH_KEY, SERVER_SSH_PORT. DEPLOY_DIR (the same
secret the full site deploy uses), with /public_html/ appended,
instead of requiring a separate FTP_TARGET_DIR secret. schema.html,
idag.html, live.html, dagens-schema.html, events.json,
schema.rss, and per-event detail pages under schema/. FTP_TARGET_DIR validation step must be removed from the QA
job. deploy-reusable.yml must
be removed. Deploy API via SSH) must remain unchanged —
it already performs git pull and npm install, which is sufficient
to deploy the API server. §43.9–§43.10 are superseded — see Archived (superseded).
docs/08-ENVIRONMENTS.md must be updated to reflect that QA no longer
requires FTP secrets for event data deploy. docs/04-OPERATIONS.md must be updated to describe the new QA event
data deploy method (SCP instead of FTP). 08-ENVIRONMENTS.md must note which FTP secrets
are production-only and which are shared. FTP_HOST,
FTP_USERNAME, FTP_PASSWORD, FTP_APP_DIR, FTP_TARGET_DIR) should
be removed from the qa GitHub Environment. The Node.js API server (app.js) requires a Node.js-capable host with Passenger
or a similar process manager. Loopia (the target webhotell) supports PHP and
Apache but not Node.js. This section adds a PHP implementation of the same API
so that the entire site — static files and API — can be served from a single
shared hosting account.
The Node.js API is kept intact for local development and for any future host that supports Node.js.
POST /api/add-event with the same request body,
validation rules, and response format as the Node.js POST /add-event. POST /api/edit-event with the same request body,
validation rules, and response format as the Node.js POST /edit-event. Content-Type: application/json. GET /api/health endpoint must return {"status":"API running"} for
monitoring and deploy verification. start_date..end_date) must be enforced. id field. opens_for_editing..end_date + 1 day. patchEventInYaml. source/data/camps.yaml from
GitHub (not from a local file), using the same derivation rules as
resolveActiveCamp. sb_session cookie using the same
format (JSON-encoded array of event IDs, URL-encoded) as the Node.js
implementation. Path=/, Max-Age=604800, Secure,
SameSite=Strict, and optional Domain when COOKIE_DOMAIN is set. cookieConsent: true in the request body). Access-Control-Allow-Origin,
Access-Control-Allow-Methods, Access-Control-Allow-Headers,
Access-Control-Allow-Credentials) for origins listed in
ALLOWED_ORIGIN and QA_ORIGIN environment variables. OPTIONS preflight requests must return HTTP 204 with the correct
CORS headers. GITHUB_OWNER, GITHUB_REPO,
GITHUB_BRANCH, GITHUB_TOKEN, ALLOWED_ORIGIN, QA_ORIGIN,
COOKIE_DOMAIN, BUILD_ENV. .env file in the
API directory. The PHP API must load this file at startup if it
exists. GITHUB_TOKEN and other secrets must never appear in error responses
or logs visible to end users. api/ directory at the project root,
separate from the Node.js source. api/composer.json). api/index.php (router/entry point),
api/src/ (modules), api/composer.json, api/.env (not committed,
git-ignored). .htaccess file in the api/ directory must route all requests
to index.php (front-controller pattern). .htaccess must work on Loopia’s Apache 2.4 with mod_rewrite
enabled. api/ directory (including
vendor/) to the server alongside the static site. composer install --no-dev must run either locally in CI or the
vendor/ directory must be included in the deploy archive. .env file on the server is managed manually — it is not part
of the deploy archive. API_URL to point to the PHP API path
(e.g. https://sbsommar.se/api/add-event) when building for
environments that use the PHP API. API_URL format remains valid for environments
that still use the Node.js API. app.js, source/api/) must remain fully functional
and unmodified. npm start (Node.js). API_URL
environment variable in each GitHub Environment. docs/04-OPERATIONS.md must document the PHP API: directory structure,
configuration, and how to set it up on a new host. docs/08-ENVIRONMENTS.md must document the qa GitHub Environment
(PHP on Loopia) and its secrets. docs/03-architecture/ must note the dual API architecture (Node.js
for local dev and Node.js hosts, PHP for shared hosting). Event data validation (injection patterns, link protocol, length limits) runs in the API layer at submission time. Data that reaches git is already validated. The CI pipeline for event-data PRs provides a branch-protection gate, and a post-merge workflow builds and deploys event-data files.
§50.1–§50.7 (Docker image and build workflow) are superseded by §52 — see Archived (superseded).
event-data-deploy.yml must contain a single job that logs “Validated at API
layer” and passes. main with path source/data/**.yaml and
only for branches matching event/ and event-edit/ prefixes. .github/workflows/event-data-deploy-post-merge.yml) must trigger
on push to main with path filter source/data/**.yaml. setup-node + npm ci. node source/build/build.js. schema.html,
idag.html, live.html, dagens-schema.html, events.json, schema.rss,
schema.ics, kalender.html, and per-event pages under schema/. SERVER_HOST, SERVER_USER,
SERVER_SSH_KEY, SERVER_SSH_PORT, DEPLOY_DIR. FTP_HOST, FTP_USERNAME, FTP_PASSWORD,
FTP_APP_DIR, FTP_TARGET_DIR) should be removed from the production GitHub
Environment. This is a manual step. has_code == false), ci.yml must skip
npm ci and npm run build, letting the job pass after the detect
step. The post-merge event-data deploy workflow (§50.4) currently runs a serial
detect job before starting the three parallel deploy jobs. This adds
~15 seconds to the critical path because the deploy jobs must wait for
detect to finish before they can start. Each environment requires its
own build (different SITE_URL and BUILD_ENV), so sharing build
artifacts is not possible. Eliminating the serial dependency is the
primary lever for reducing deploy latency.
detect job that other jobs depend on. fetch-depth: 2 to support
HEAD~1..HEAD comparison. git diff logic previously in the detect job: filter for
source/data/*.yaml, exclude camps.yaml and local.yaml. camps.yaml lookup
as the previous detect job. 02-§50.13 (detect via HEAD~1..HEAD in a dedicated job) is
superseded by 02-§51.2 and 02-§51.5 (inline detection per
job). 02-§50.14 (QA detection sets is_qa output) is superseded by
02-§51.7 (inline QA check in production job only). The post-merge event-data deploy workflow (§50.4) previously used a
pre-built Docker image from GHCR to avoid running npm ci on every
deploy. While this eliminated npm ci time, pulling the Docker image
itself added ~20 seconds per job. Replacing the Docker container with
actions/setup-node and the built-in npm cache achieves the same
dependency-availability goal with lower overhead: cache restore takes
~2–3 seconds on cache hit, and npm ci --omit=dev installs four small
production packages in ~3 seconds.
actions/setup-node@v4 with node-version: '20' and cache: 'npm'
instead of a Docker container. npm ci --omit=dev to install only production
dependencies. container: key must be
absent from all jobs). packages: read permission (no GHCR
access needed). setup-node and npm ci must be
conditional on the gate step output — skipped when no event data file
changed. setup-node and npm ci must run
unconditionally (before the gate step), because the gate step itself
uses node -e with js-yaml to check QA camp status. 02-§50.1–02-§50.7 (Docker image and Docker build workflow) are
superseded — the event-data deploy workflow no longer uses a Docker
image. 02-§50.12 (workflow must use Docker image from GHCR) is superseded
by 02-§52.1 (setup-node + npm cache). The project has a working CI/CD pipeline (QA auto-deploy, production manual trigger) but no contributor-facing documentation explaining the deploy flow, how to release to production, or who is authorized to do so. A new contributor who is not familiar with the GitHub Actions setup cannot determine how their changes reach users.
01-CONTRIBUTORS.md) must include a section
explaining what happens after a PR is merged: QA auto-deploy for code,
dual auto-deploy for event data, and manual trigger for
production. 08-ENVIRONMENTS.md) must document the
required reviewers configuration for the production
environment. 09-RELEASING.md) must exist with
step-by-step instructions for deploying to production. QA testers and administrators need to confirm which version of the site is deployed. Without a visible version indicator, there is no way to know whether a deploy has completed or which build is currently live. A version string in the footer solves this with minimal visual impact.
VERSION file with the major.minor version
(e.g. 1.0). This file is the single source of truth for the base
version. <p class="site-footer__version"> element at the bottom of the
footer. v1.0.4. v1.0.4 – QA PR212. If no PR number can be extracted,
the short commit SHA is used as fallback. v1.0 – Lokal 2026-03-02 14:30. BUILD_ENV is set but BUILD_VERSION is
not (event-data deploys), no version string is rendered in the
footer. v1.0.4). --generate-notes. BUILD_VERSION environment variable.
When set, it is used as the version string. BUILD_VERSION is not set and BUILD_ENV is not set (local
development), the build must read the VERSION file and generate a
local timestamp version. v1.0.1, QA must also show
v1.0.1). This makes it visible that QA is running the
production release with no additional changes. main, the normal QA deploy
(deploy-qa.yml) restores the QA-suffixed version string
(e.g. v1.0.1 – QA PR217). The project documentation in docs/ is structured Markdown covering
contribution guidelines, requirements, architecture, data contract,
operations, design, environments, releasing, and the traceability
matrix. Reading it on GitHub’s file viewer works, but cross-file links
render as raw .md file downloads in some contexts and searching
requires opening files one at a time.
Publishing the same content as a small, read-only documentation site
lets contributors and stakeholders browse the docs in a regular
browser, follow internal links without friction, and link directly to
specific sections when reporting issues or discussing changes. The
documentation site is entirely separate from the main sbsommar.se
site — it does not affect the camp website, the PHP API, or any
existing deployment workflow.
docs/ folder is published as a static website at a
public URL hosted by GitHub Pages. main branch, mapped to
the /docs folder, using GitHub Pages’ “Deploy from a branch”
source. main that changes any file under docs/ causes GitHub
Pages to rebuild and republish the documentation site automatically,
without running the project’s own CI workflows and without manual
intervention. docs/ folder of the repository; it
does not surface files from outside docs/, repository secrets, or
environment values. docs/ are rendered as HTML by GitHub Pages’
built-in Jekyll toolchain, with no custom build step or project-owned
workflow. [text](other-file.md) or [text](other-file.md#section) resolve
correctly on the published site, so that navigation between docs
files works without editing each link manually. <!-- 02-§1.1 -->) are preserved as HTML
comments in the rendered output and are not visible to readers. docs/index.md as the
landing page. The landing page lists every other Markdown file under
docs/ (for example 01-CONTRIBUTORS.md, the 02-requirements/
index and its topic files, and the rest of the numbered docs) with a
one-line description and a link to the rendered HTML page.
package.json or
api/composer.json. .github/workflows/ publishes the
documentation site; GitHub Pages’ built-in deployment is the only
mechanism that builds it. deploy-qa.yml, deploy-prod.yml, deploy-reusable.yml,
event-data-deploy.yml, event-data-deploy-post-merge.yml) do not
interact with the documentation site or GitHub Pages; they target
shared hosting only. *.github.io URL assigned by
GitHub Pages; no docs/CNAME file is present. README.md (the file GitHub renders on the repository home page)
links to the documentation site’s public URL near the top of the
file, before the developer setup section, so that a first-time
visitor sees the link without scrolling past the marketing copy. README.md lists every file currently
published under docs/ (01-CONTRIBUTORS.md, the 02-requirements/
index plus its topic files pages-navigation.md,
schedule-and-detail.md, add-edit-forms.md, event-data.md,
build-deploy.md, caching-performance.md, platform-security.md,
design-and-content.md, archive.md, then
03-architecture/, 04-OPERATIONS.md, 05-DATA_CONTRACT.md,
06-EVENT_DATA_MODEL.md, 07-DESIGN.md, 08-ENVIRONMENTS.md,
09-RELEASING.md, and 99-traceability.md), each with a one-line
description and a link that works both on GitHub’s file viewer and
on the rendered documentation site. docs/index.md carries a banner near the top
(above the documentation index) that links back to the source
repository on GitHub, the rendered README.md on GitHub, and the
GitHub issue tracker. The links target absolute
https://github.com/moggleif/sbsommar URLs so they resolve
identically whether the page is viewed on the published Pages site
or on GitHub’s file viewer. https://sbsommar.se (the
participant-facing camp site). The §97.8 search-engine policy
prohibits actively pointing search engines at the camp site from a
publicly hosted Pages page. README.md for additional context.
It does not describe the camp’s purpose, audience, schedule, or
any other content that the §97.8 search-engine policy keeps off
search-engine result pages. sbsommar.se) but applies to the documentation site
(https://moggleif.github.io/sbsommar/). docs/robots.txt file at the documentation site root disallows
every user agent from every path (User-agent: * /
Disallow: /). <meta name="robots" content="noindex, nofollow"> tag in the
<head> section. The tag is injected via Jekyll’s head-custom
include under docs/_includes/. Both filename conventions are
shipped (head-custom.html for Primer/Minima themes,
head_custom.html for Cayman) so that the meta tag lands in
<head> regardless of which default theme GitHub Pages selects.