Organization-wide GitHub configuration for Forum Virium Helsinki.
| Path | Purpose |
|---|---|
profile/README.md |
Org profile shown on github.com/ForumViriumHelsinki |
CODE_OF_CONDUCT.md |
Default code of conduct for all repos |
CONTRIBUTING.md |
Default contribution guidelines for all repos |
SECURITY.md |
Security vulnerability reporting policy |
SUPPORT.md |
Where to get help |
.github/ISSUE_TEMPLATE/ |
Default issue templates for all repos |
.github/PULL_REQUEST_TEMPLATE.md |
Default PR template for all repos |
.github/workflows/reusable-*.yml |
Reusable workflows callable from any FVH repo |
workflow-templates/ |
Starter workflows shown in Actions tab for new repos |
.rulesync/rules/ |
Canonical AI coding rules for FVH repos; the only place to edit them (how to sync) |
rulesync.jsonc |
rulesync targets and features for regenerating this repo; reusable-sync-ai-rules.yml passes its features as flags and accepts a subset of its targets through the targets input, so a consuming repo needs no copy |
.claude/rules/, .agents/rules/, .cursor/rules/, .github/instructions/ |
Tool-specific rule copies generated by rulesync generate; do not edit |
Call these from any FVH repo using uses: ForumViriumHelsinki/.github/.github/workflows/<name>.yml@main.
| Workflow | Description |
|---|---|
reusable-container-build.yml |
PR phase: build and push :next-{version} pre-release image to GHCR |
reusable-container-release.yml |
Release phase: promote pre-built image to semver tags (or fallback rebuild) + Trivy scan |
reusable-release-please.yml |
Automated releases via release-please |
reusable-auto-merge-image-updater.yml |
Auto-merge ArgoCD Image Updater PRs (single-job, push-triggered) |
reusable-fix-release-conflicts.yml |
Auto-resolve release-please merge conflicts |
reusable-clear-autorelease-labels.yml |
Manual escape hatch: strip a stale autorelease: pending label from closed PRs to unclog release-please |
reusable-renovate.yml |
Dependency updates via Renovate — called centrally from the infrastructure repo only; application repos must not call it |
reusable-enforce-conventional-commits.yml |
Auto-fix PR titles to conventional-commit format (github-script, no AI) |
reusable-npm-publish.yml |
Publish an npm package via OIDC trusted publishing (no NPM_TOKEN; the caller's job grants id-token: write) |
reusable-bun-ci.yml |
Pull-request gate for bun/TypeScript repos: bun install --frozen-lockfile, then the repo's own typecheck (opt-in), test and build commands; optional coverage artifact; no secrets |
reusable-gitleaks.yml |
For public repos: gitleaks over the checked-out tree with the default rules plus the FVH GCP-identifier rule from .gitleaks.toml (embedded, drift-tested); enforcement: block|warn; release binary, no secrets |
reusable-claude.yml |
Claude Code @-mention support in issues and PRs |
| Workflow | Description |
|---|---|
reusable-claude-review.yml |
Automated PR review |
reusable-auto-fix.yml |
Analyze failed workflow runs, then auto-fix (commit + push) or open an issue |
reusable-auto-resolve-conflicts.yml |
Resolve PR merge conflicts with Claude Code; stale automated PRs are closed instead |
| Workflow | Description |
|---|---|
reusable-sync-ai-rules.yml |
Open a PR in the calling repo that syncs rules from .rulesync/rules/ via rulesync (the caller sets the trigger) |
| Workflow | Description |
|---|---|
reusable-security-secrets.yml |
Detect leaked secrets and credentials |
reusable-security-deps.yml |
Dependency vulnerability audit |
reusable-security-owasp.yml |
OWASP Top 10 static analysis |
| Workflow | Description |
|---|---|
reusable-quality-code-smell.yml |
Code smell and anti-pattern detection |
reusable-quality-async.yml |
Async pattern validation |
reusable-quality-typescript.yml |
TypeScript strictness analysis |
| Workflow | Description |
|---|---|
reusable-a11y-aria.yml |
ARIA pattern correctness |
reusable-a11y-wcag.yml |
WCAG 2.1 compliance check |
These eight workflows never post a PR comment; Claude is granted no GitHub write tool. Each finding goes to the run's job summary and to a file annotation on the PR's Files changed tab (error level for the workflow's blocking severities, warning otherwise). GitHub shows at most 10 annotations per level per step, so the job summary is the complete list. A run that analysed nothing (no changed file matched, or the diff exceeded max-diff-lines) says so in its summary, so its green check does not read as a clean scan.
A run in which Claude finishes but returns no structured output ends green with a warning that there is no verdict; re-run it to get one. The job goes red when the analysis itself failed (an API error, the max-turns limit, or the max-budget-usd ceiling, which defaults to 5 USD), and, when enabled, on blocking findings: reusable-security-owasp.yml with fail-on-critical (default true) and reusable-security-deps.yml with fail-on-high (default false) fail when a blocking finding is itemised or the reported count of them is above zero.
The container workflows use a build-once/promote pattern: images are built during the release-please PR and promoted (manifest-only retag) on tag push — no redundant rebuilds.
# In your repo's .github/workflows/container-build.yml
name: Build and release container
on:
push:
tags: ['my-app-v*.*.*']
pull_request:
branches: [main]
jobs:
# PR phase: build :next-{version} image for later promotion
build:
if: startsWith(github.head_ref, 'release-please--branches--main')
uses: ForumViriumHelsinki/.github/.github/workflows/reusable-container-build.yml@main
with:
image-name: forumviriumhelsinki/my-app
# Release phase: promote pre-built image to semver tags (seconds, not minutes)
release:
if: github.event_name == 'push'
uses: ForumViriumHelsinki/.github/.github/workflows/reusable-container-release.yml@main
with:
image-name: forumviriumhelsinki/my-app
tag-prefix: my-app-v
# Security scan on PRs
security:
if: github.event_name == 'pull_request'
uses: ForumViriumHelsinki/.github/.github/workflows/reusable-security-owasp.yml@main
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}CLAUDE_CODE_OAUTH_TOKEN is an organization secret granted only to repositories that carry the claude topic. A Claude-powered caller in a repository without that topic gets no token and fails.
tag-prefix must prefix every release tag the caller triggers on: the release workflow strips it and derives all image tags from what remains (my-app-v1.2.3 → 1.2.3, 1.2, 1, latest; a pre-release such as my-app-v1.2.3-rc.1 → 1.2.3-rc.1 only). A tag the prefix does not match fails the run with an ::error::.
Every FVH repo calls these workflows at @main, so .github/workflows/lint.yml checks each PR and push to main: every workflow and template parses as YAML, actionlint and gitleaks pass, the generated rule copies match .rulesync/rules/, the Python blocks in .rulesync/rules/ are in ruff 0.16.9 style (--isolated --line-length 88, as the consuming repos that run ruff format --check see them), the shipped fetch and generate steps of reusable-sync-ai-rules.yml produce one file per rule for every target in a scratch repo (the path consumers run, which reads no rulesync.jsonc), and every test under .github/tests/ passes. just lint and just test run the same commands locally, and just sync-smoke (network and a GitHub token) the consumer path.
The tools that gate fetches run at exact versions (yq and Node come with the runner image): rulesync in lint.yml, reusable-sync-ai-rules.yml, the justfile and the commands in this README, and actionlint, gitleaks and ruff in lint.yml (with a sha256; gitleaks also in reusable-gitleaks.yml) and the justfile. renovate.json bumps each one across its files in a single PR, and the tool-pins test fails when any of them disagree or a Renovate regex stops matching. Renovate cannot recompute a checksum, so an actionlint, gitleaks or ruff bump stays red at sha256sum -c until the matching *_SHA256 is updated by hand; the PR body says how. The test convention is in .github/tests/README.md.
The org's coding rules for AI assistants live in .rulesync/rules/*.md in this repository, one Markdown file per topic with YAML frontmatter naming its targets and path globs. rulesync compiles those sources into the per-tool formats each assistant reads. Every other directory of rules — here and in consuming repos — is generated output and must not be edited by hand.
| Source | Generated for | Output |
|---|---|---|
.rulesync/rules/*.md |
Claude Code | .claude/rules/*.md |
| GitHub Copilot | .github/instructions/*.instructions.md |
|
Antigravity (agy), via rulesync's antigravity-ide target |
.agents/rules/*.md with trigger: frontmatter |
|
| Cursor | .cursor/rules/*.mdc |
|
.rulesync/.aiignore |
Claude Code, Cursor | a permissions.deny block in .claude/settings.json, plus .cursorignore |
Edit the file under .rulesync/rules/, regenerate, and commit source and output together:
npx rulesync@40.2.0 generate
To check generated files are current without writing anything (this is what CI asserts — it exits 1 when regeneration would change something):
npx rulesync@40.2.0 generate --check
A consuming repo needs one file, committed to the consuming repo: .github/workflows/sync-ai-rules.yml, a caller for reusable-sync-ai-rules.yml. The reusable workflow declares only workflow_call, so the caller owns the trigger. The starter workflow in workflow-templates/sync-ai-rules.yml (also offered in the repo's Actions → New workflow page) runs weekly and on demand:
name: Sync AI Rules
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1" # Weekly Monday 6am UTC
jobs:
sync:
uses: ForumViriumHelsinki/.github/.github/workflows/reusable-sync-ai-rules.yml@mainEach run fetches .rulesync/ from this repository into the calling repo, regenerates the per-tool files, and opens a PR on branch chore/sync-ai-rules when anything changed. The fetched .rulesync/ copy is committed in the consuming repo too — rulesync generates from a local source tree, so the rules have to land there before generate can read them.
By default the PR is opened with GITHUB_TOKEN. In a repo where "Allow GitHub Actions to create and approve pull requests" is off (GitHub's default), that fails with GitHub Actions is not permitted to create or approve pull requests, and a PR GITHUB_TOKEN does open triggers no CI. Pass a GitHub App to open it with an App token instead:
jobs:
sync:
uses: ForumViriumHelsinki/.github/.github/workflows/reusable-sync-ai-rules.yml@main
with:
app-id: ${{ vars.CI_APP_ID }}
secrets:
APP_PRIVATE_KEY: ${{ secrets.CI_APP_PRIVATE_KEY }}Only Claude Code files are generated by default. The targets input takes a comma-separated subset of claudecode, copilot, antigravity-ide and cursor, and defaults to claudecode. Add a target only for a tool the repo actually uses; any other value fails the run before generation. In the #111 pilot, all four targets wrote 78 files into each of google-chat-agent and podio-mcp, and 46 of them landed in Copilot, Cursor and Antigravity trees that neither repo used.
with:
targets: claudecode,copilotThe workflow does not read the consuming repo's rulesync.jsonc for targets or features. fetch pulls only .rulesync/, never the config, and a bare generate with no config resolves to the agentsmd target alone — which no rule's frontmatter lists — so it writes zero files at exit 0. The generate step therefore always passes --targets (from the input) and --features rules,ignore. CLI options take precedence over rulesync.jsonc and rulesync.local.jsonc, so a consuming repo's own config does not change the targets. Options the command line does not set, such as delete, are still read from a config if one exists. With the default target, a sync of this repository's rules writes the fifteen .claude/rules/ files and .claude/settings.json, plus the fetched .rulesync/ copy. In an existing .claude/settings.json, generation replaces every Read(...) entry in permissions.deny with the fetched .aiignore set and keeps the other deny entries, allow and enabledPlugins, so a consuming repo's own Read(...) denies are dropped on each sync.
The same two steps the workflow runs, from the root of the consuming repo:
npx rulesync@40.2.0 fetch ForumViriumHelsinki/.github --features rules,ignore --conflict overwrite --path .rulesync
npx rulesync@40.2.0 generate --targets claudecode,copilot,antigravity-ide,cursor --features rules,ignore
A consuming repo that commits a copy of this repository's rulesync.jsonc can drop both flags. --path .rulesync is required: rulesync 9 and later resolve features against the source repo's root, so without it the fetch looks for rules/ at the top level, 404s, and returns zero files at exit 0.
Each line of .rulesync/.aiignore becomes a Read(...) entry in the permissions.deny block of the generated .claude/settings.json, and a line in .cursorignore. The file lists rulesync.local.jsonc, rulesync.lock and node_modules/: untracked, machine-local paths. It does not list .rulesync/ or rulesync.jsonc, because a Claude Code Read deny also blocks Edit, Write and Bash commands that name the path (permissions docs), which would make the rule sources and the config uneditable in a Claude Code session. Claude Code does not auto-load .rulesync/, so the sources are not read twice; Cursor and Antigravity CLI index them as ordinary files.
Generation replaces every Read(...) deny in an existing .claude/settings.json with the .aiignore set and leaves the rest of the file — non-Read denies, allow, enabledPlugins, everything else — untouched. A Read(...) deny added to settings.json by hand is dropped on the next generate; add the pattern to .aiignore instead. A consuming repo that fetches --features rules,ignore receives this .aiignore too, so the same replacement applies to its settings.json.
Release images are signed with Sigstore cosign using keyless mode (OIDC identity from GitHub Actions). Signatures are recorded in the Rekor public transparency log.
cosign verify \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity-regexp='https://github.com/ForumViriumHelsinki/' \
ghcr.io/forumviriumhelsinki/my-app:1.2.3Pass enable-signing: false to the release workflow:
uses: ForumViriumHelsinki/.github/.github/workflows/reusable-container-release.yml@main
with:
image-name: forumviriumhelsinki/my-app
tag-prefix: my-app-v
enable-signing: falseFiles at the root of this repo serve as org-wide defaults. Individual repos can override any of them by providing their own copy.
Files that cannot be org defaults (must be per-repo): LICENSE