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
53 changes: 53 additions & 0 deletions .github/workflows/ci-success.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Reusable aggregate "CI Success" gate.
#
# Branch-protection rules should require a single check named "CI Success"
# instead of every individual job. Each repo's CI workflow calls this once,
# passing the `needs.*.result` values of its real jobs as a JSON string, and
# this workflow fails unless every upstream job succeeded.
#
# Usage from a caller workflow:
#
# ci-success:
# name: CI Success
# if: always()
# needs: [lint, test, build]
# uses: ai-agent-assembly/.github/.github/workflows/ci-success.yml@master
# with:
# needs-json: ${{ toJSON(needs) }}
#
# The gate passes only when no upstream job has a result of `failure` or
# `cancelled`. Skipped jobs are treated as non-blocking.
name: CI Success

on:
workflow_call:
inputs:
needs-json:
description: >-
JSON object of the caller's `needs` context, i.e. ${{ toJSON(needs) }}.
The gate inspects each job's `result` field.
required: true
type: string

permissions:
contents: read

jobs:
ci-success:
name: CI Success
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify no upstream job failed or was cancelled
env:
NEEDS_JSON: ${{ inputs.needs-json }}
run: |
echo "Upstream job results:"
echo "$NEEDS_JSON" | jq -r 'to_entries[] | " \(.key): \(.value.result)"'
failed=$(echo "$NEEDS_JSON" | jq -r \
'[to_entries[] | select(.value.result == "failure" or .value.result == "cancelled") | .key] | join(", ")')
if [ -n "$failed" ]; then
echo "::error::CI Success gate failed. Blocking jobs: ${failed}"
exit 1
fi
echo "All upstream jobs succeeded or were skipped. CI Success."
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,46 @@ PRs always target `master`, even when your branch was created from another featu

Keep PRs focused. One concern per PR β€” don't bundle unrelated changes. If a single ticket needs more than ~500 lines of diff, split it into a sequence of stacked PRs.

## Continuous integration β€” the 8-principle playbook

Every repo's CI is hand-rolled, which is why the same gaps recur. To stop
retrofitting, the org `.github` repo ships **starter workflows** that bake these
eight principles in, so new and empty repos start compliant. Pick one from the
**Actions β†’ New workflow** screen (look for "… (Agent Assembly playbook)"):
`Rust CI`, `Node / TypeScript CI`, `Python CI`, `Go CI`, or `Docs CI`.

The reference rationale and the **measured-result evidence** (before/after CI
minutes and wall-clock) live in `agent-assembly`'s
[`docs/src/benchmarks/ci-cd-pipeline-performance.md`](https://github.com/AI-agent-assembly/agent-assembly/blob/master/docs/src/benchmarks/ci-cd-pipeline-performance.md).

| # | Principle | How the starters apply it |
|---|---|---|
| 1 | **Path-scoped triggers** | `on.push` / `on.pull_request` list `paths:` so doc-only or unrelated edits don't spend CI minutes. |
| 2 | **Concurrency cancel** | `concurrency` cancels superseded runs **only on PRs** (`cancel-in-progress: ${{ github.event_name == 'pull_request' }}`) β€” `$default-branch` and tag pushes always run to completion. |
| 3 | **Single `CI Success` gate** | An aggregate job (`needs: [...]` + `if: always()`) is the one required check. Branch protection requires only `CI Success`, not every job. |
| 4 | **Linux-default matrices** | The OS matrix is `["ubuntu-latest"]` on PRs and only fans out to macOS/Windows on `$default-branch` / tag pushes. |
| 5 | **Least-privilege permissions** | A top-level `permissions: contents: read`; jobs widen scope only when they must. |
| 6 | **Job timeouts** | Every job sets `timeout-minutes` so a hung step can't burn the runner budget. |
| 7 | **Reusable gate** | The `CI Success` logic is a reusable workflow (`ci-success.yml`, `on: workflow_call`); repos `uses:` it instead of copy-pasting (see below). |
| 8 | **Cache the toolchain** | Each starter caches its package/build artifacts (`Swatinem/rust-cache`, `setup-node … cache: pnpm`, `setup-uv`, `setup-go`) to keep warm runs fast. |

### Reusable `CI Success` gate

Instead of copy-pasting the aggregate gate, call the org reusable workflow:

```yaml
ci-success:
name: CI Success
if: always()
needs: [lint, test]
uses: ai-agent-assembly/.github/.github/workflows/ci-success.yml@master
with:
needs-json: ${{ toJSON(needs) }}
```

It fails if any upstream job's `result` is `failure` or `cancelled`; skipped
jobs are non-blocking. Make `CI Success` the only required status check.

## Code review

- At least **one approval** is required before merge.
Expand Down
6 changes: 6 additions & 0 deletions workflow-templates/docs-ci.properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Docs CI (Agent Assembly playbook)",
"description": "markdownlint + lychee link check with path-scoped triggers, PR-only concurrency cancel, least-privilege permissions, job timeouts, and a reusable CI Success gate (docs build Linux-only, so no OS matrix).",
"iconName": "book",
"categories": ["Documentation", "Continuous integration"]
}
63 changes: 63 additions & 0 deletions workflow-templates/docs-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Org starter: Docs CI encoding the 8-principle playbook.
# Documentation builds are Linux-only by nature, so there is no OS matrix;
# the remaining seven principles still apply. See the org CONTRIBUTING.md
# "CI/CD playbook" section and the measured-result evidence in
# agent-assembly's docs/src/benchmarks/ci-cd-pipeline-performance.md.
name: CI

on:
push:
branches: [$default-branch]
tags: ["v*"]
paths:
- "**/*.md"
- "**/*.mdx"
- "docs/**"
- ".github/workflows/**"
pull_request:
paths:
- "**/*.md"
- "**/*.mdx"
- "docs/**"
- ".github/workflows/**"

# Principle: cancel superseded runs β€” but only on PRs, never on
# $default-branch / tag pushes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

# Principle: least privilege at the top level.
permissions:
contents: read

jobs:
lint:
name: markdownlint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v18
with:
globs: "**/*.md"

links:
name: link check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: lycheeverse/lychee-action@v2
with:
args: "--no-progress './**/*.md'"
fail: true

# Principle: single aggregate gate.
ci-success:
name: CI Success
if: always()
needs: [lint, links]
uses: ai-agent-assembly/.github/.github/workflows/ci-success.yml@master
with:
needs-json: ${{ toJSON(needs) }}
6 changes: 6 additions & 0 deletions workflow-templates/go-ci.properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Go CI (Agent Assembly playbook)",
"description": "golangci-lint + go test with path-scoped triggers, PR-only concurrency cancel, Linux-default matrix (multi-OS on default branch / tags), least-privilege permissions, job timeouts, and a reusable CI Success gate.",
"iconName": "go",
"categories": ["Go", "Continuous integration"]
}
69 changes: 69 additions & 0 deletions workflow-templates/go-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Org starter: Go CI encoding the 8-principle playbook.
# See the org CONTRIBUTING.md "CI/CD playbook" section for the rationale and
# the measured-result evidence in agent-assembly's
# docs/src/benchmarks/ci-cd-pipeline-performance.md.
name: CI

on:
push:
branches: [$default-branch]
tags: ["v*"]
paths:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
- ".github/workflows/**"
pull_request:
paths:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
- ".github/workflows/**"

# Principle: cancel superseded runs β€” but only on PRs, never on
# $default-branch / tag pushes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

# Principle: least privilege at the top level.
permissions:
contents: read

jobs:
lint:
name: golangci-lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- uses: golangci/golangci-lint-action@v6

test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
# Principle: Linux-default. Multi-OS only on $default-branch / tag
# pushes; PRs run Linux only.
os: ${{ fromJSON((github.event_name == 'push' && (github.ref == 'refs/heads/$default-branch' || startsWith(github.ref, 'refs/tags/'))) && '["ubuntu-latest", "macos-latest", "windows-latest"]' || '["ubuntu-latest"]') }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- run: go test ./...

# Principle: single aggregate gate.
ci-success:
name: CI Success
if: always()
needs: [lint, test]
uses: ai-agent-assembly/.github/.github/workflows/ci-success.yml@master
with:
needs-json: ${{ toJSON(needs) }}
6 changes: 6 additions & 0 deletions workflow-templates/node-ci.properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Node / TypeScript CI (Agent Assembly playbook)",
"description": "pnpm lint + typecheck + test with path-scoped triggers, PR-only concurrency cancel, Linux-default matrix (multi-OS on default branch / tags), least-privilege permissions, job timeouts, and a reusable CI Success gate.",
"iconName": "nodejs",
"categories": ["Node", "TypeScript", "JavaScript", "Continuous integration"]
}
83 changes: 83 additions & 0 deletions workflow-templates/node-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Org starter: Node / TypeScript CI encoding the 8-principle playbook.
# See the org CONTRIBUTING.md "CI/CD playbook" section for the rationale and
# the measured-result evidence in agent-assembly's
# docs/src/benchmarks/ci-cd-pipeline-performance.md.
name: CI

on:
push:
branches: [$default-branch]
tags: ["v*"]
paths:
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
- "**/package.json"
- "**/pnpm-lock.yaml"
- ".github/workflows/**"
pull_request:
paths:
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
- "**/package.json"
- "**/pnpm-lock.yaml"
- ".github/workflows/**"

# Principle: cancel superseded runs β€” but only on PRs, never on
# $default-branch / tag pushes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

# Principle: least privilege at the top level.
permissions:
contents: read

jobs:
lint:
name: lint + typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck

test:
name: test (node ${{ matrix.node }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
# Principle: Linux-default. Multi-OS only on $default-branch / tag
# pushes; PRs run Linux only.
os: ${{ fromJSON((github.event_name == 'push' && (github.ref == 'refs/heads/$default-branch' || startsWith(github.ref, 'refs/tags/'))) && '["ubuntu-latest", "macos-latest", "windows-latest"]' || '["ubuntu-latest"]') }}
node: [20]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test

# Principle: single aggregate gate.
ci-success:
name: CI Success
if: always()
needs: [lint, test]
uses: ai-agent-assembly/.github/.github/workflows/ci-success.yml@master
with:
needs-json: ${{ toJSON(needs) }}
6 changes: 6 additions & 0 deletions workflow-templates/python-ci.properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Python CI (Agent Assembly playbook)",
"description": "uv-based ruff + mypy + pytest with path-scoped triggers, PR-only concurrency cancel, Linux-default matrix (multi-OS on default branch / tags), least-privilege permissions, job timeouts, and a reusable CI Success gate.",
"iconName": "python",
"categories": ["Python", "Continuous integration"]
}
Loading