# Development workflow: GitHub source and Gitea mirror [Documentation](README.md) GitHub is the authoritative repository. Self-hosted Gitea is a fast-forward-only mirror for redundancy and local visibility. Pull requests and Dependabot updates land on GitHub first. Every default-branch change is squash-merged through a pull request; direct pushes and protection bypasses are disabled. Use **placeholder** URLs in docs you commit publicly; set real URLs in your local git remotes and in Gitea/Woodpecker settings. --- ## 1. Remotes: GitHub authoritative, Gitea mirror After creating the repository on GitHub and Gitea: ```bash git clone https://github.com/youruser/homelab.git cd homelab # Preserve the repository's established remote names. git remote rename origin github git remote add origin https://gitea.example.com/youruser/homelab.git git branch --set-upstream-to=github/main main git config remote.pushDefault github git remote -v # github -> GitHub # origin -> Gitea ``` Daily synchronization: ```bash git pull --ff-only ``` Synchronize Gitea manually: ```bash cp scripts/sync-gitea-mirrors.sh scripts/sync-gitea-mirrors.local.sh # Edit the ignored script with this host's repositories and remotes. make mirror-sync ``` --- ## 2. Automatic GitHub-to-Gitea mirror Gitea cannot convert an existing normal repository into a pull mirror. This repo therefore includes a user-level systemd timer that provides equivalent safe behavior without recreating the Gitea repository: ```bash mkdir -p ~/.config/systemd/user install -m 0644 scripts/systemd/github-gitea-mirrors.* \ ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now github-gitea-mirrors.timer ``` The timer runs every 15 minutes. It synchronizes the repositories configured in the ignored, host-specific `scripts/sync-gitea-mirrors.local.sh`; start from `scripts/sync-gitea-mirrors.sh`. For each repository, it fetches GitHub `main`, confirms Gitea can fast-forward, pushes that exact commit to the Gitea backup, and mirrors tags. It never force-pushes or deletes refs. Direct commits to the configured backup refs are unsupported; if a backup diverges, synchronization fails for that repository while the remaining repositories are still checked. Each entry has `name|path|source-remote|backup-remote`, optionally followed by `|destination-branch|tag-prefix`. The defaults are `main` and no tag prefix. For a Gitea repository with its own maintained history or protected branches, use `|github-main|github/`. This backs up GitHub's published `main` and tags to separate refs while preserving Gitea's existing branches and tags. It does not merge the two development histories or change Gitea's default branch. The helper supports normal, linked, and bare repositories. It resolves configured remote URLs, then fetches and pushes in a disposable bare repository. Local edits, unpublished local tags, and working-copy hooks do not participate; the source is the published GitHub ref. Source development still follows its normal PR and CI requirements. Destination protections are respected, and a lookup failure stops before publication. Existing destination branches must be ancestors of the source; conflicting tags are refused. Branch and tag pushes are separate operations, so a tag failure can follow a successful branch update; inspect both before retrying. After changing the installed service definition, run `systemctl --user daemon-reload` and verify its `ExecStart` points to `sync-gitea-mirrors.local.sh`. Keep the local repository list ignored. Verify remote refs and the service result after a sync. --- ## 3. Local CI before pushing `make ci-local` runs Mylar's portable publication controls through `make mylar-source-check`: private-copy reader recovery, retained API and worker handoffs, authenticated launch, workflow intake, archive adoption and NFS checks. Set `ARCHIVING_UTILS_ROOT` to the checked archive SDK directory containing `lib/archive_backend.py`; the complete gate refuses a missing SDK rather than skipping its integration tests. `PUBLICATION_AUTH_BACKEND` selects the existing fixture backend: `host` on the supported Arch host, or `target` in the Ubuntu verification image. They use disposable local files and SQLite, require no Docker access, and preserve tracked source files. `MYLAR_PYTHON` selects the interpreter (default `python3`); use Python 3.10 to match the native runtime. These source checks do not replace the installed-image gate or protected NFS/reader integration acceptance. Install the hooks once: ```bash npm ci make hooks-install ``` Each commit checks staged secrets, whitespace, dependency configuration, GitHub workflow syntax/security, changed Dockerfiles, Compose rendering, metadata, preparation scripts, and generated documentation. Run the full all-files and Git-history suite on demand. Prepare a separate checkout of `bolens/.github` at the exact `tooling-ref` in `.github/workflows/source-lint.yml`, install its locked tools with `npm ci --ignore-scripts --prefix /path/to/shared/tools/source-lint`, and provide Ruff 0.15.20. The shared source lint runs first, using the hosted configuration and rules. Missing tools, a different checkout revision or changed tooling stop the gate; validation does not clone or install tools automatically. ```bash make ci-local JAVA_HOME_21=/path/to/jdk-21 SOURCE_LINT_TOOLING=/path/to/shared ``` After the Mylar controls, `ci-local` runs the Komga source/compile gate through `make komga-source-check`, the same target used by hosted validation. Set `JAVA_HOME_21` to a JDK 21 installation. It defaults to the hosted runner's `JAVA_HOME_21_X64` when available. A missing JDK or failed gate stops the command; the gate runs without Docker and does not observe or change service state. The final secret scan checks every commit reachable from candidate `HEAD`. It does not include unrelated local branch or snapshot histories. The staged-file secret hook still checks new changes before commit. `make secrets` retains the repository-wide scan across all refs. Repository validation ShellChecks every tracked `*.sh` file, including stack-local entrypoints and preparation helpers. GitHub pull requests render only changed stacks when all changes are under `stacks/` or `portainer/`; global configuration or tooling changes automatically trigger rendering of every stack. The equivalent local command is: ```bash make validate-changed BASE=origin/main ``` The hooks require Python with PyYAML, pre-commit, ShellCheck, Gitleaks, actionlint, and Hadolint. The zizmor hook uses a native installation when available and otherwise uses its pinned Docker image. Install the locked Python validation dependency into the virtual environment or managed Python interpreter used by `python3`: ```bash python3 -m pip install --require-hashes -r .github/requirements-validation.txt ``` The lock includes the PyYAML source archive for interpreters without an allowed wheel. Run `python3 -c 'import yaml; print(yaml.__version__)'` before validation to confirm that the selected interpreter can import it. Run the Pages browser suite separately because it starts Chrome and is slower than the commit-time checks: ```bash make site-test ``` To opt in to the same browser suite before every push, install the locked Node dependencies and Playwright's Chrome browser, then install the pre-push hook: ```bash npm ci npx --no-install playwright install chrome make hooks-install-site ``` The opt-in hook uses two workers and CI mode, so it starts its own local server and fails if port 4173 is already occupied. A failed test blocks the push. `make hooks-install` and `make ci-local` do not enable or run this browser hook. Hook installation is shared by linked worktrees; each checkout used for pushing needs its own installed Node dependencies. Run the hook directly with `pre-commit run pre-push-site-browser-tests --hook-stage pre-push --all-files`. To remove the optional push hook, run `pre-commit uninstall --hook-type pre-push`. The commit hook remains installed. CI runs the same locked Playwright and Axe suite before it publishes GitHub Pages. The tests cover serious accessibility violations, catalog search and filters, keyboard skip navigation, JavaScript errors, and mobile overflow. --- ## CI organization Use the [shared CI vocabulary](https://github.com/bolens/.github/blob/main/docs/woodpecker-transition.md#ci-organization): `validate`, `test`, `site` and `publish` describe purpose; existing workflow names and required status identities stay unchanged. Run commands from the repository root with the documented toolchain. | Phase / group | Canonical local entrypoint | Native mapping / retained owner | | --- | --- | --- | | `validate` / repository and secrets | `make validate-changed BASE=origin/main`; full `make ci-local JAVA_HOME_21=/path/to/jdk-21 SOURCE_LINT_TOOLING=/path/to/shared` after the setup above | `woodpecker` / `repository-validation`, `gitleaks`; strict validation checks all portable contracts and generated content. | | `validate` / source and workflow policy | `make source-lint SOURCE_LINT_TOOLING=/path/to/shared`; `actionlint`; `zizmor --offline --min-severity medium --min-confidence medium .github .pre-commit-config.yaml` | `source-and-workflow-policy` also owns pinned Spec Kit integrity and Hadolint. | | `test` / source and compiler controls | Existing focused Make targets documented above and in the owning stack guides | `komga-source-controls`, `kavita-dotnet-controls`, and `publisher-broker-tests`; private or installed-runtime controls remain separate from portable source proof. | | `site` / generated site and browser checks | `make pages-check`; `make site-test` | `site-quality`; Pages retains its own publication-coupled browser acceptance and protected OIDC deployment. | | `validate` / scheduled links | `python3 scripts/ci/check-native-links.py` with the pinned Lychee tool | `weekly-documentation-links` on cron/manual; internal failures are fatal, external reporting is explicitly nonblocking. | | `publish` / custom images | The reviewed image contexts and selectors in `.github/custom-images.json` and [RELEASING.md](../RELEASING.md#custom-image-ci-selection) | Retained hosted builds/runtime checks publish only selected trusted-main images. Native validation has no publisher credentials or host Docker socket. | Duplicate hosted Source lint, Dockerfile Lint, Workflow Security and Weekly Maintenance workflows are retired. The retained repository-validation workflow still owns its nonportable checks; managed security, actual image builds, metadata and deployment remain hosted. The recovery rules below preserve replacement-check proof and operator authority. Repository validation does not deploy any stack. ## 4. Woodpecker CI Enable the intended repository on the native GitHub or Gitea forge and use the root `.woodpecker.yml`. It retains the single `woodpecker` result on every push, PR, manual run and cron, without path filters that could omit a required result. The Linux Docker backend needs no published port, host mount, Docker socket, privileged step, registry login or pipeline secret for these checks. The pipeline runs: - full-history Gitleaks and `make validate-strict`, including all portable stack contracts, generated content and the existing Python/Node suites; - pinned shared source lint, actionlint, offline zizmor, Spec Kit integrity and Hadolint for tracked files named exactly `Dockerfile`; - the existing Komga source/compile controls with JDK 21; - the default-CI Kavita compiler controls with .NET SDK 10.0.403 and Node 26.7.0, requiring at least nine executed tests and no skips; - the locked Playwright/Axe site suite using the existing Chrome channel; - publisher-broker race tests and vet; and - internal documentation links on cron/manual runs, with external URLs remaining a report-only check under the existing weekly workflow's exact arguments. Tool downloads retain a 15-second socket inactivity timeout, a 256 MiB byte bound and the pinned SHA-256 check. EOF and verified content determine completion; a progressing download is not cut off by a second total bootstrap timer. The existing pipeline timeout still bounds the whole CI job. The general repository suite can still report optional compiler probes skipped in its Node image. The dedicated .NET lane closes the ordinary hosted compiler coverage; optional installed-assembly/source probes still require their explicit private inputs and are not claimed as executed. Neither lane runs application services or accesses live data. Each link scan has a 180-second reporting ceiling. An external timeout prints an explicit incomplete-report warning and establishes no link success; it remains nonblocking. Internal errors/timeouts and missing tools remain fatal, as do checksum or tool-install failures before a scan. The wrapper-based validation lanes check the exact committed revision in separate full disposable clones. Their Python environments, npm modules, downloads and caches live under a unique `/tmp` directory and are removed on exit. The existing Gitleaks and Go lanes use the initial pipeline checkout. Docker/Compose are checksum-bound CLI tools used only to render configurations; their socket points to an absent task-local path. Compose is registered in the disposable container's standard system plugin directory so fixtures can use their own empty `HOME`/`DOCKER_CONFIG` without losing the plugin. Registration refuses host execution, checksum changes and existing unrelated plugins. No host plugin directory is changed. No Alpine/glibc Python environment or browser cache is shared across images. Public executable pins and SHA-256 checksums are in [`scripts/ci/native-tools.json`](../scripts/ci/native-tools.json). The bootstrap checks HTTPS origins, archive paths, entry types, size limits and hashes before executing tools. Chrome for Testing is the fixed Google artifact selected there; Playwright's Chrome channel and locked test dependencies remain unchanged. Native Python tools use the hashed [requirements](../.github/requirements-native-ci.txt). Refresh a tool's version, primary-source checksum and coupled tests together; rerun its actual image lane. Renovate monitors Woodpecker image references, and Dependabot monitors the Python requirements. The raw executable manifest requires a reviewed coordinated update. ### Hosted retirement and recovery `WOODPECKER_LINUX_CI` defaults off. With it absent or false, existing hosted pure validation still runs. An operator may set it to `true` only after the same native candidate passes a PR, a genuine merged-main push and a configured weekly cron (the existing Monday 08:41 UTC schedule), and the protected branch requires the actual native result on the latest SHA. Keep native trust/fork approval enabled. There is no publishing-flag dependency for homelab. This source change does not set the variable, create a cron or change protection. To recover, clear `WOODPECKER_LINUX_CI` and verify the hosted required producers on a new run before changing protection. Preserve the native check until its replacement is proven. GitHub retains actual Docker image builds/runtime tests and publication, Mylar and converter fixtures, CodeQL, dependency review, Scorecard, Pages OIDC deployment and deployment-bound browser evidence, metadata automation and Spec Kit update PRs. The Pages job's own pre-deployment browser check remains coupled to its publication rather than trusting an unrelated run. To skip a run for a commit message, use `[CI SKIP]` or `[SKIP CI]` (Woodpecker convention). If **gitleaks** reports findings in **tracked** files, fix or allowlist via [Gitleaks config](https://github.com/gitleaks/gitleaks) (e.g. repo-root `.gitleaks.toml`). Local-only files such as `stack.env` are not in git and are not part of the default `gitleaks detect` scan of committed history, avoid committing them. --- ## 5. GitHub Actions GitHub retains the same portable validation as the default fallback described above, plus the Docker, deployment and managed security exceptions. Validation uses read-only repository permissions. Additional narrowly scoped workflows provide: - advanced CodeQL analysis for Python and GitHub Actions with the `security-extended` query suite; - pull-request dependency review for newly introduced high or critical vulnerabilities; - actionlint and zizmor checks for workflow correctness and security; - Hadolint checks for maintained Dockerfiles; - weekly repository, secret, and documentation-link checks; and - weekly OpenSSF Scorecard results in GitHub code scanning. Actions are pinned to immutable commit SHAs. Dependabot proposes grouped, reviewed updates for Actions, Dockerfiles, Compose images, and the root npm manifest. Those managers are disabled in Renovate to prevent duplicate pull requests; Renovate remains available for other supported dependency types. Dockerfile update directories are listed explicitly and checked for complete, non-overlapping coverage. Acquire and Privotron stay on Python 3.13 until their application locks support a newer runtime; their separate update group still receives digest refreshes. AIL's native builder stays on Ubuntu 22.04, enforced by the custom-image contract test. The interim Ubuntu 25.10 release is explicitly ignored; this does not suppress other stacks' selected Ubuntu 26.04 digest updates. Docker ignore conditions use numeric Bundler requirements such as `0.15.6`, rather than image tags such as `v0.15.6`; local validation checks this syntax. Treat runtime migrations as reviewed application changes. Database major upgrades require a verified restore/migration and matching volume contracts. PostgreSQL, PostGIS and MongoDB major proposals are held for that work; current-major security and digest updates remain enabled. Inspect the published engine version for floating database tags before accepting a digest refresh. Security-reporting workflows grant `security-events: write` only to jobs that upload results; other workflows and jobs retain read-only or empty default permissions. CodeQL uses the committed advanced workflow in `.github/workflows/codeql.yml`. Do not also enable GitHub's default CodeQL setup, because that would duplicate analysis. --- ## 6. Related docs - [stacks/woodpecker-ci/README.md](../stacks/woodpecker-ci/README.md), deploy Woodpecker server/agent and Gitea OAuth. - [scripts/README.md](../scripts/README.md), `scan-secrets-gitleaks.sh` for local scans (including `--no-git` for the working tree).