Skip to content

Commit f4b51a6

Browse files
authored
feat(workflows): org-wide reusable CI + governance payloads
Introduces the governance layer for resq-software: - 6 reusable workflows in .github/workflows/ — rust-ci, python-ci, node-ci, dotnet-ci, cpp-ci, docker-publish, plus a required.yml aggregator that composes language CI + security-scan into a single `required` status-check context consumed by org Ruleset A. - All caller-controlled string inputs forwarded through env with allowlist-regex validation where inputs reach shell or action params. CodeQL scanner alerts on the remaining input-surface dismissed with rationale (reusable-workflow trust model: caller is same-org, not external). - Shipped standalone JSON payloads under ops/ for the custom-property schema, per-repo value assignments, and both org rulesets (default-branch-baseline + critical-tier-extras). Companion docs/README in ops/governance-payloads.md. - CODEOWNERS added for the resq-software/.github repo itself. Rulesets are already applied in evaluate mode (ids 15191038, 15191046) with property schema + values live on the org. Flip to active after each consumer repo emits the `required` context and CODEOWNERS coverage is confirmed for all participating repos.
1 parent 9069479 commit f4b51a6

13 files changed

Lines changed: 1423 additions & 0 deletions

.github/CODEOWNERS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Default owner for all files in resq-software/.github.
2+
# Once a core team exists, point `*` to that team (e.g. `@resq-software/core`)
3+
# and add narrower owners for specific paths.
4+
* @WomB0ComB0

.github/workflows/cpp-ci.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright 2026 ResQ Software
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Reusable C++ CI — CMake configure + build + test. Matrix over OS.
5+
# Pairs with security-scan.yml.
6+
#
7+
# Security: caller-controlled path/flag inputs are forwarded through
8+
# `env:` and referenced as `"$VAR"` to prevent shell injection.
9+
10+
name: cpp-ci
11+
12+
on:
13+
workflow_call:
14+
inputs:
15+
os-list:
16+
type: string
17+
required: false
18+
default: '["ubuntu-latest","macos-latest","windows-latest"]'
19+
source-dir:
20+
type: string
21+
required: false
22+
default: "."
23+
build-dir:
24+
type: string
25+
required: false
26+
default: "build"
27+
cmake-flags:
28+
type: string
29+
required: false
30+
default: ""
31+
build-config:
32+
type: string
33+
required: false
34+
default: "Debug"
35+
run-test:
36+
type: boolean
37+
required: false
38+
default: true
39+
timeout-minutes:
40+
type: number
41+
required: false
42+
default: 30
43+
44+
permissions:
45+
contents: read
46+
47+
jobs:
48+
build-test:
49+
name: ${{ matrix.os }}
50+
runs-on: ${{ matrix.os }}
51+
timeout-minutes: ${{ inputs.timeout-minutes }}
52+
strategy:
53+
fail-fast: false
54+
matrix:
55+
os: ${{ fromJSON(inputs.os-list) }}
56+
steps:
57+
- name: Harden Runner
58+
if: runner.os == 'Linux'
59+
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
60+
with:
61+
egress-policy: audit
62+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
63+
- name: Configure
64+
shell: bash
65+
env:
66+
SRC: ${{ inputs.source-dir }}
67+
BUILD: ${{ inputs.build-dir }}
68+
FLAGS: ${{ inputs.cmake-flags }}
69+
# shellcheck disable=SC2086 - FLAGS is intentionally word-split into
70+
# multiple cmake arguments; quoting would pass them as a single token.
71+
run: |
72+
# shellcheck disable=SC2086
73+
cmake -B "$BUILD" $FLAGS "$SRC"
74+
- name: Build
75+
shell: bash
76+
env:
77+
BUILD: ${{ inputs.build-dir }}
78+
CONFIG: ${{ inputs.build-config }}
79+
run: cmake --build "$BUILD" --config "$CONFIG"
80+
- if: ${{ inputs.run-test }}
81+
name: Test
82+
shell: bash
83+
env:
84+
BUILD: ${{ inputs.build-dir }}
85+
CONFIG: ${{ inputs.build-config }}
86+
run: ctest --test-dir "$BUILD" --output-on-failure -C "$CONFIG"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Copyright 2026 ResQ Software
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Reusable container build + publish + provenance workflow.
5+
# Builds via buildx, pushes to ghcr.io by default, and produces an
6+
# SLSA build-provenance attestation (sigstore-backed) for the image
7+
# digest.
8+
#
9+
# Security posture:
10+
# 1. The caller-supplied ref is checked out with
11+
# `persist-credentials: false` so the git token is isolated from
12+
# the fetched code.
13+
# 2. All caller-controlled string inputs (image, registry, context,
14+
# dockerfile, platforms, tags, build-args) are validated against
15+
# allowlist regexes in a dedicated `Validate caller inputs` step,
16+
# then re-exported to `$GITHUB_ENV` with a `SAFE_` prefix.
17+
# Downstream steps reference `${{ env.SAFE_X }}` only — CodeQL
18+
# treats SAFE_* as sanitized after explicit validation.
19+
# 3. The caller-controlled `ref` input is the documented trust
20+
# boundary; the actions/untrusted-checkout/medium alert is
21+
# dismissed with rationale because this is a reusable-workflow
22+
# model where the caller is a sibling resq-software/* repo.
23+
24+
name: docker-publish
25+
26+
on:
27+
workflow_call:
28+
inputs:
29+
image:
30+
description: Image name (without registry), e.g. "resq-software/mcp".
31+
type: string
32+
required: true
33+
registry:
34+
type: string
35+
required: false
36+
default: "ghcr.io"
37+
context:
38+
type: string
39+
required: false
40+
default: "."
41+
dockerfile:
42+
type: string
43+
required: false
44+
default: "Dockerfile"
45+
platforms:
46+
type: string
47+
required: false
48+
default: "linux/amd64,linux/arm64"
49+
tags:
50+
description: Newline-separated image tags to push (full refs).
51+
type: string
52+
required: true
53+
build-args:
54+
type: string
55+
required: false
56+
default: ""
57+
push:
58+
type: boolean
59+
required: false
60+
default: true
61+
attest:
62+
description: Emit SLSA build-provenance attestation (only runs when push is true).
63+
type: boolean
64+
required: false
65+
default: true
66+
ref:
67+
description: >
68+
Git ref to checkout. Caller's responsibility to pass a
69+
trusted ref; persist-credentials:false is set on checkout
70+
so the git token is not exposed.
71+
type: string
72+
required: false
73+
default: ""
74+
outputs:
75+
image-digest:
76+
description: >
77+
sha256 digest of the pushed image manifest. Populated ONLY
78+
when `push: true`.
79+
value: ${{ jobs.publish.outputs.digest }}
80+
81+
permissions:
82+
contents: read
83+
84+
jobs:
85+
publish:
86+
name: Build, push, attest
87+
runs-on: ubuntu-latest
88+
timeout-minutes: 30
89+
permissions:
90+
contents: read
91+
packages: write
92+
id-token: write
93+
attestations: write
94+
outputs:
95+
digest: ${{ steps.build.outputs.digest }}
96+
steps:
97+
- name: Harden Runner
98+
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2
99+
with:
100+
egress-policy: audit
101+
102+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
103+
with:
104+
ref: ${{ inputs.ref || github.ref }}
105+
persist-credentials: false
106+
107+
- name: Validate caller inputs
108+
env:
109+
IN_IMAGE: ${{ inputs.image }}
110+
IN_REGISTRY: ${{ inputs.registry }}
111+
IN_CONTEXT: ${{ inputs.context }}
112+
IN_DOCKERFILE: ${{ inputs.dockerfile }}
113+
IN_PLATFORMS: ${{ inputs.platforms }}
114+
IN_TAGS: ${{ inputs.tags }}
115+
IN_BUILD_ARGS: ${{ inputs.build-args }}
116+
shell: bash
117+
run: |
118+
# shellcheck disable=SC2016,SC2129
119+
set -euo pipefail
120+
121+
fail() { echo "::error::$*" >&2; exit 1; }
122+
match() { [[ "$2" =~ $3 ]] || fail "Invalid $1: '$2' does not match $3"; }
123+
124+
# image: namespace/name
125+
match image "$IN_IMAGE" '^[a-zA-Z0-9][a-zA-Z0-9._/-]*$'
126+
# registry: hostname with optional :port
127+
match registry "$IN_REGISTRY" '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$'
128+
129+
# context: local path or known remote URL form; no ..
130+
if ! [[ "$IN_CONTEXT" =~ ^(\.|[a-zA-Z0-9._][a-zA-Z0-9._/-]*|https?://[a-zA-Z0-9._:/?#=\&%-]+|git://[a-zA-Z0-9._:/?#=\&%-]+|github\.com/[a-zA-Z0-9._/-]+)$ ]]; then
131+
fail "Invalid context: '$IN_CONTEXT'"
132+
fi
133+
case "$IN_CONTEXT" in *..*) fail "context may not contain '..'" ;; esac
134+
135+
# dockerfile: relative path, no traversal
136+
match dockerfile "$IN_DOCKERFILE" '^[a-zA-Z0-9._][a-zA-Z0-9._/-]*$'
137+
case "$IN_DOCKERFILE" in *..*) fail "dockerfile may not contain '..'" ;; esac
138+
139+
# platforms: comma-separated lowercase os/arch tokens
140+
match platforms "$IN_PLATFORMS" '^[a-z0-9]+/[a-z0-9_-]+(,[a-z0-9]+/[a-z0-9_-]+)*$'
141+
142+
# tags: newline-separated image refs
143+
while IFS= read -r tag; do
144+
[ -z "$tag" ] && continue
145+
match tag "$tag" '^[a-zA-Z0-9][a-zA-Z0-9._:/@-]*$'
146+
done <<< "$IN_TAGS"
147+
148+
# build-args: KEY=VALUE per line; no backticks / $(; reject smuggling
149+
while IFS= read -r arg; do
150+
[ -z "$arg" ] && continue
151+
[[ "$arg" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || fail "build-arg must be KEY=VALUE: '$arg'"
152+
case "$arg" in
153+
*'`'*) fail "build-arg may not contain backtick: '$arg'" ;;
154+
*'$('*) fail "build-arg may not contain command substitution: '$arg'" ;;
155+
esac
156+
done <<< "$IN_BUILD_ARGS"
157+
158+
# Re-export sanitized values — CodeQL treats SAFE_* as workflow-controlled.
159+
{
160+
echo "SAFE_IMAGE=$IN_IMAGE"
161+
echo "SAFE_REGISTRY=$IN_REGISTRY"
162+
echo "SAFE_CONTEXT=$IN_CONTEXT"
163+
echo "SAFE_DOCKERFILE=$IN_DOCKERFILE"
164+
echo "SAFE_PLATFORMS=$IN_PLATFORMS"
165+
} >> "$GITHUB_ENV"
166+
167+
{
168+
echo "SAFE_TAGS<<__EOF__"
169+
printf '%s\n' "$IN_TAGS"
170+
echo "__EOF__"
171+
} >> "$GITHUB_ENV"
172+
173+
{
174+
echo "SAFE_BUILD_ARGS<<__EOF__"
175+
printf '%s\n' "$IN_BUILD_ARGS"
176+
echo "__EOF__"
177+
} >> "$GITHUB_ENV"
178+
179+
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
180+
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
181+
182+
- if: ${{ inputs.push }}
183+
name: Log in to registry
184+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
185+
with:
186+
registry: ${{ env.SAFE_REGISTRY }}
187+
username: ${{ github.actor }}
188+
password: ${{ secrets.GITHUB_TOKEN }}
189+
190+
- name: Build and push
191+
id: build
192+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
193+
with:
194+
context: ${{ env.SAFE_CONTEXT }}
195+
file: ${{ env.SAFE_DOCKERFILE }}
196+
platforms: ${{ env.SAFE_PLATFORMS }}
197+
tags: ${{ env.SAFE_TAGS }}
198+
build-args: ${{ env.SAFE_BUILD_ARGS }}
199+
push: ${{ inputs.push }}
200+
provenance: mode=max
201+
sbom: true
202+
203+
- if: ${{ inputs.attest && inputs.push }}
204+
name: Attest build provenance
205+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
206+
with:
207+
subject-name: ${{ env.SAFE_REGISTRY }}/${{ env.SAFE_IMAGE }}
208+
subject-digest: ${{ steps.build.outputs.digest }}
209+
push-to-registry: true

0 commit comments

Comments
 (0)