Skip to content
CobenianPublic

About

Shared CI/CD: reusable Elixir workflow, per-repo templates, and default community-health files for Cobenian repos.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Cobenian/.github — shared CI/CD & community health

This is the org-special Cobenian/.github repo — the single source of truth that all Cobenian repos inherit from, implementing STANDARDS.md §9.

Standards

The canonical, org-wide normative docs (RFC 2119). Every product cites these.

Doc Governs
STANDARDS.md How repos are built and shipped (GitHub, CI, CD)
PLATFORM-ADOPTION.md How a product consumes the platform (inbound: product → Accounts/Foundry/Data)
PRODUCT-SURFACE.md How a product exposes itself (outbound: REST, MCP, OAuth2 — caller → product)
IN-APP-ADMIN.md The in-app admin surface each product owns (tier 3 of 3)
DELIVERY-CONTRACT.md How a product gets a message to somebody (Accounts Notify + Foundry Comms conform to one shape)

Layout

.github/
  workflows/
    elixir-ci.yml          # REUSABLE CI (workflow_call) — the heavy lifting
    fly-deploy.yml         # REUSABLE deploy (workflow_call) — retry + --wait-timeout + smoke
    aws-ecs-deploy.yml     # REUSABLE AWS ECS deploy: build, scan gate, roll, verify + watch (@v2)
    aws-guard.yml          # REUSABLE guard check: runs the caller's main bin/guard on a PR (@v2)
    aws-automerge.yml      # REUSABLE automerge of bot PRs once CI + guard pass (@v2)
    aws-toolchain-check.yml # REUSABLE weekly Elixir/OTP/Debian image check (@v2)
    mutation.yml           # REUSABLE weekly mutation sample (workflow_call)
    test-improvement.yml   # REUSABLE weekly unit test improvement (workflow_call)
tools/                     # mutation-sample, test-improvement-verify — checked out by those two
tests/                     # self-test.yml runs these
  PULL_REQUEST_TEMPLATE.md # org-default PR template (inherited)
  ISSUE_TEMPLATE/
    bug_report.md          # org-default (inherited)
    feature_request.md     # org-default (inherited)
templates/                 # COPY these into each consumer repo (not inherited)
  ci.yml                   # thin caller of the reusable CI
  fly-deploy.yml           # thin caller of the reusable deploy (workflow_run trigger + app inputs)
  mutation.yml             # thin caller of the weekly mutation sample
  test-improvement.yml     # thin caller of the weekly unit test improvement
  aws-deploy.yml           # AWS apps: → .github/workflows/deploy.yml
  aws-guard.yml            # AWS apps: → .github/workflows/guard.yml
  aws-automerge.yml        # AWS apps: → .github/workflows/automerge.yml
  aws-refresh.yml          # AWS apps: → .github/workflows/refresh.yml
  dependabot-app.yml       # → .github/dependabot.yml  (apps)
  dependabot-library.yml   # → .github/dependabot.yml  (libraries)
  CODEOWNERS               # → .github/CODEOWNERS       (not inheritable)
  credo.exs                # → .credo.exs              (apps; ships the fleet log-attribution guard)

Inherited automatically (org defaults)

PULL_REQUEST_TEMPLATE.md and ISSUE_TEMPLATE/* apply to any repo that doesn't define its own — no per-repo copy needed.

Reusable, referenced by tag

elixir-ci.yml (CI) and fly-deploy.yml (deploy) are consumed via uses: Cobenian/.github/.github/workflows/<name>@v1. Tag releases (v1, v1.1, …) so consumers pin and bump deliberately; v1 is the moving major pointer. Deploy inputs are documented in STANDARDS.md §4.

Two lines: v1 for Fly apps, v2 for AWS apps (2026-11-03)

AWS apps (ECS on Fargate) call the aws-*.yml workflows at @v2; Fly apps stay on @v1. Each tag is moved separately, so an AWS-only change ships by moving v2 alone and never reaches a Fly app. A change both need (a new Elixir in elixir-ci.yml, say) moves both tags; the release notes say which moved. v1 freezes when the Fly apps are retired. The aws-* workflows were extracted from cobenian-company's own loop so every AWS app shares one copy.

What the @v2 workflows check, and their inputs (each input is also described in the workflow):

  • aws-ecs-deploy.yml — app, url, deploy-role, stack-name (all required), aws-region (us-east-1), alerts-topic, ci-check, max-severity (HIGH), head-sha, smoke-path (/sign-in), watch-minutes (10), rebuild (false).
    • Deploys only when the ci-check check run passed and was reported by GitHub Actions (app.slug == "github-actions"), so a same-named check posted by anything else doesn't count.
    • Blocks the image when ECR's scan finds anything at or above max-severity: CRITICAL, HIGH (high or critical, the default) or MEDIUM. ECR basic scanning sees OS packages only; Hex packages are covered by CI's hex.audit / deps.audit.
    • Builds the new revision from the task definition CloudFormation manages in stack-name (exactly one AWS::ECS::TaskDefinition), never from the service's live one, changing only the image; registers it with the original's tags plus app=<app>; and refuses to roll if the service runs a different task-definition family. The deploy role needs cloudformation:DescribeStackResources on the stack and ecs:TagResource for tag-on-register; it can require aws:RequestTag/app. The SNS alert is best-effort, so a role without sns:Publish still gets the incident issue.
  • aws-guard.yml — guard-command (bin/guard), approver (brweber2), approval-label (bryan-approved). Every PR is judged by the guard script, whoever opened it: there is no pass for a person's own PR (agents open theirs as brweber2). A held change needs the approval below.
  • aws-automerge.yml — bots (app/dependabot), environment (engineer), app-id-secret, app-key-secret, guard-command, approver, approval-label. It finds the PR by the run's head sha (never the branch name, which a pull_request_target run may report as the base).

Approval (guard and automerge, identical code): a held change is approved only when the label is on the PR, the head is still the judged sha on a branch of this repo, and the issue events show a labeled event for the label by the approver, created after the branch's latest push. The latest push comes from GitHub's repository activity API (GET /repos/{repo}/activity?ref=refs/heads/<branch>), which records each push and force-push with a server timestamp and the sha it moved to; commit author/committer dates, which the commit's author chooses, are never used. All pages of events are read. Anything unreadable fails closed.

Must be copied per repo (NOT inheritable)

ci.yml, fly-deploy.yml (both thin callers now), dependabot.yml, CODEOWNERS.

Consuming the CI in a repo

.github/workflows/ci.yml (from templates/ci.yml):

name: CI
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }
permissions: { contents: read }
jobs:
  build-and-test:
    uses: Cobenian/.github/.github/workflows/elixir-ci.yml@v1
    with:
      profile: app          # app | library
      umbrella: false       # true for Companion
      postgres: true        # false for DB-less libraries
      nonblocking: ""        # e.g. "credo,dialyzer" while ratcheting a live repo
    secrets: inherit

Reusable CI inputs

Input Default Notes
profile — (required) app | library; library skips Sobelow
umbrella false set sobelow_root / migrations_path to the child app paths
postgres true starts Postgres + runs ecto.create/migrate; false for DB-less libraries
nonblocking "" comma list → those checks run report-only. Recognized: credo, sobelow, hex_audit, deps_audit
dialyzer_blocking false Dialyzer is report-only by default; flip to true once PLTs are stable
sobelow_root . apps/<web_app> for umbrellas
migrations_path priv/repo/migrations duplicate-timestamp check target
elixir-version 1.20.4
otp-version 29.1.2
postgres_image postgres:17
gitleaks_version 8.21.2 CLI binary (the action needs a paid org license)
audit_ignore_max_days 90 furthest an .audit-ignore.yml entry may expire (1–365). See STANDARDS.md §5.1

Weekly mutation sample and unit test improvement

Two reusable workflows, one pair of tools, every app (Elixir or Python):

Workflow When (caller's cron) Does
mutation.yml Monday 06:17 UTC Breaks one operator at a time in the files listed in .github/mutation/paths.txt, runs the tests that name each file, posts which breaks no test noticed to the mutation-sample issue. Report-only.
test-improvement.yml Tuesday 07:17 UTC Same ISO-week seed. A model drafts tests for the missed breaks; tools/test-improvement-verify decides from the record (only tests changed, nothing removed, suite green, every earlier catch kept, at least one new catch) whether to open ONE pull request for a person.
  • Tools live here only (tools/mutation-sample, tools/test-improvement-verify). The workflows check them out at job.workflow_sha, so moving v1 moves workflow and tools together. Tested by self-test.yml.
  • Per repo: copy templates/mutation.yml and templates/test-improvement.yml, set language (and postgres, umbrella), and write .github/mutation/paths.txt: 3-10 files where a wrong operator would cost money, leak access or corrupt data, each with a # comment saying why.
  • Not configured is a failure. No paths file, an empty one, or (for test improvement) no CLAUDE_CODE_OAUTH_TOKEN secret fails the run with "not configured" in the job summary. A green run always did something.
  • Test improvement runs on the Claude subscription, never the API. Claude Code authenticates with CLAUDE_CODE_OAUTH_TOKEN (made with claude setup-token, an org secret shared with each repository). ANTHROPIC_API_KEY is deliberately not a fallback and is removed from the drafting step's environment: a fallback would resume billing the API, unseen, the day the token lapsed.
  • Model: TEST_IMPROVEMENT_MODEL variable, default claude-sonnet-5.
  • A pull request opened by the job does not trigger CI (GitHub will not start workflows for a pull request GITHUB_TOKEN opened); close and reopen it to run CI.

⚠️ Branch-protection check name

Because CI is a reusable workflow, the status-check context is nested: build-and-test / Build and test (not bare Build and test). Set branch protection's required context to that nested string. Verify with:

gh api repos/Cobenian/<repo>/commits/<sha>/check-runs --jq '.check_runs[].name'

The deploy trigger keys off the workflow name (CI), which is unaffected.

CI security-posture artifact → Foundry / Panel

After the build, the reusable CI re-runs the Elixir scanners in machine-readable mode, folds their results into one JSON file, and uploads it as the workflow-run artifact cobenian-scan-posture. Foundry's GitHub connector reads that artifact off the completed run and emits connectors.scan_result.received so Cobenian Panel's app & security monitor can fold CI-computed posture per repo + commit (issue #106 Option A; cobenian-foundry SPEC-CON-003 §14).

  • No configuration, no secret, no endpoint. The connector pulls the artifact via the GitHub App install it already has — CI needs no FOUNDRY_INGEST_TOKEN and makes no network call to Foundry. (This replaces the reverted POST-with-bearer emitter, PR #11 → reverted by #12; only the transport changed.)

  • Always-on & never a gate. The build/upload steps are continue-on-error: true + if: always() and internally guarded, so they can never fail a caller's build and still run when an earlier blocking scanner failed. The existing blocking scanner steps are unchanged and still own pass/fail.

  • Artifact contract — the single JSON file's body:

    { "source": "github",                        // links to the repo/workflow_run events
      "repo": "<owner>/<repo>", "head_sha": "…", "run_url": "…",
      "workflow_run_id": "…", "scanned_at": "<ISO8601>",
      "sobelow":      { "findings": N, "by_confidence": { "high": N, "medium": N, "low": N } },
      "mix_audit":    [ { "package": "…", "id": "CVE/GHSA", "severity": "…", "title": "…" } ],
      "hex_audit":    [ { "package": "…", "version": "…", "reason": "…" } ],
      "gitleaks":     { "secrets_found": N },     // COUNT ONLY — never a value or location
      "hex_outdated": [ { "package": "…", "current": "…", "latest": "…" } ] }

    A scanner that did not run or failed is omitted — never fabricated as clean. Per cobenian-foundry CON-GH-015, no secret values, secret locations, or source snippets are ever emitted (gitleaks/Sobelow contribute counts only); the raw scanner reports stay on the runner and are never uploaded.

Per-repo profile cheat-sheet

Repo profile umbrella postgres deploy
cobenian-accounts app false true yes
cobenian-cadence app false true yes
cobenian-agents (Oversight) app false true yes
cobenian_companion_umbrella app true true yes
cobenian-core-accounts library false false no

About

Shared CI/CD: reusable Elixir workflow, per-repo templates, and default community-health files for Cobenian repos.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages