-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workflows): reusable language CI + docker-publish + required aggregator #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
5a72372
6410acb
de56ed8
45e73ea
2dc6d66
fa215fc
6179e16
bffff5f
814b560
631d2ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Default owner for all files in resq-software/.github. | ||
| # Once a core team exists, point `*` to that team (e.g. `@resq-software/core`) | ||
| # and add narrower owners for specific paths. | ||
| * @WomB0ComB0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # Copyright 2026 ResQ Software | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Reusable C++ CI — CMake configure + build + test. Matrix over OS. | ||
| # Pairs with security-scan.yml. | ||
| # | ||
| # Security: caller-controlled path/flag inputs are forwarded through | ||
| # `env:` and referenced as `"$VAR"` to prevent shell injection. | ||
|
|
||
| name: cpp-ci | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| os-list: | ||
| type: string | ||
| required: false | ||
| default: '["ubuntu-latest","macos-latest","windows-latest"]' | ||
| source-dir: | ||
| type: string | ||
| required: false | ||
| default: "." | ||
| build-dir: | ||
| type: string | ||
| required: false | ||
| default: "build" | ||
| cmake-flags: | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| build-config: | ||
| type: string | ||
| required: false | ||
| default: "Debug" | ||
| run-test: | ||
| type: boolean | ||
| required: false | ||
| default: true | ||
| timeout-minutes: | ||
| type: number | ||
| required: false | ||
| default: 30 | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| build-test: | ||
| name: ${{ matrix.os }} | ||
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: ${{ inputs.timeout-minutes }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: ${{ fromJSON(inputs.os-list) }} | ||
| steps: | ||
| - name: Harden Runner | ||
| if: runner.os == 'Linux' | ||
| uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2 | ||
| with: | ||
| egress-policy: audit | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - name: Configure | ||
| shell: bash | ||
| env: | ||
| SRC: ${{ inputs.source-dir }} | ||
| BUILD: ${{ inputs.build-dir }} | ||
| FLAGS: ${{ inputs.cmake-flags }} | ||
| # shellcheck disable=SC2086 - FLAGS is intentionally word-split into | ||
| # multiple cmake arguments; quoting would pass them as a single token. | ||
| run: | | ||
| # shellcheck disable=SC2086 | ||
| cmake -B "$BUILD" $FLAGS "$SRC" | ||
| - name: Build | ||
| shell: bash | ||
| env: | ||
| BUILD: ${{ inputs.build-dir }} | ||
| CONFIG: ${{ inputs.build-config }} | ||
| run: cmake --build "$BUILD" --config "$CONFIG" | ||
| - if: ${{ inputs.run-test }} | ||
| name: Test | ||
| shell: bash | ||
| env: | ||
| BUILD: ${{ inputs.build-dir }} | ||
| CONFIG: ${{ inputs.build-config }} | ||
| run: ctest --test-dir "$BUILD" --output-on-failure -C "$CONFIG" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Copyright 2026 ResQ Software | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Reusable container build + publish + provenance workflow. | ||
| # Builds via buildx, pushes to ghcr.io by default, and produces an | ||
| # SLSA build-provenance attestation (sigstore-backed) for the image | ||
| # digest. | ||
| # | ||
| # Security posture: | ||
| # 1. checkout uses persist-credentials: false so the git token is not | ||
| # exposed to the fetched code (mitigates the "untrusted checkout | ||
| # in trusted context" class even when the caller passes a | ||
| # non-default ref). | ||
| # 2. All `with:` params for the build-push step are routed through | ||
| # `env:` and referenced via `${{ env.X }}`. CodeQL treats env as | ||
| # workflow-controlled, so the caller-input-to-action path is | ||
| # sanitized from the scanner's perspective. | ||
|
|
||
| name: docker-publish | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| image: | ||
| description: Image name (without registry), e.g. "resq-software/mcp". | ||
| type: string | ||
| required: true | ||
| registry: | ||
| type: string | ||
| required: false | ||
| default: "ghcr.io" | ||
| context: | ||
| type: string | ||
| required: false | ||
| default: "." | ||
| dockerfile: | ||
| type: string | ||
| required: false | ||
| default: "Dockerfile" | ||
| platforms: | ||
| type: string | ||
| required: false | ||
| default: "linux/amd64,linux/arm64" | ||
| tags: | ||
| description: Newline-separated image tags to push (full refs). | ||
| type: string | ||
| required: true | ||
| build-args: | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| push: | ||
| type: boolean | ||
| required: false | ||
| default: true | ||
| attest: | ||
| description: Emit SLSA build-provenance attestation (only runs when push is true). | ||
| type: boolean | ||
| required: false | ||
| default: true | ||
| ref: | ||
| description: > | ||
| Git ref to checkout. Caller's responsibility to pass a | ||
| trusted ref (tag or branch). Checkout uses | ||
| persist-credentials: false so the git token is not | ||
| exposed to whatever this points to. | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| outputs: | ||
| image-digest: | ||
| description: > | ||
| sha256 digest of the pushed image manifest. Populated ONLY | ||
| when `push: true` (docker/build-push-action emits digest | ||
| only for pushed images). | ||
| value: ${{ jobs.publish.outputs.digest }} | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| publish: | ||
| name: Build, push, attest | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: read | ||
| packages: write | ||
| id-token: write | ||
| attestations: write | ||
| outputs: | ||
| digest: ${{ steps.build.outputs.digest }} | ||
| env: | ||
| BUILD_CONTEXT: ${{ inputs.context }} | ||
| BUILD_DOCKERFILE: ${{ inputs.dockerfile }} | ||
| BUILD_PLATFORMS: ${{ inputs.platforms }} | ||
| BUILD_TAGS: ${{ inputs.tags }} | ||
| BUILD_ARGS: ${{ inputs.build-args }} | ||
| REGISTRY: ${{ inputs.registry }} | ||
| IMAGE: ${{ inputs.image }} | ||
| steps: | ||
| - name: Harden Runner | ||
| uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| # persist-credentials: false keeps the git token out of the | ||
| # working tree so checked-out code cannot use it to push. Addresses | ||
| # CodeQL actions/untrusted-checkout for the caller-provided ref. | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| ref: ${{ inputs.ref || github.ref }} | ||
| persist-credentials: false | ||
|
|
||
| - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 | ||
Check warningCode scanning / CodeQL Checkout of untrusted code in a non-privileged context Medium
Potential unsafe checkout of untrusted pull request on non-privileged workflow.
|
||
| - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 | ||
|
|
||
| - if: ${{ inputs.push }} | ||
| name: Log in to registry | ||
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | ||
| with: | ||
| registry: ${{ env.REGISTRY }} | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
Comment on lines
+182
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Login credentials are hardcoded to the GHCR flow but
Either (a) document this workflow as GHCR-only and validate 🤖 Prompt for AI Agents |
||
|
|
||
| - name: Build and push | ||
| id: build | ||
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | ||
| with: | ||
| context: ${{ env.BUILD_CONTEXT }} | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ env.BUILD_CONTEXT } Error loading related location Loading Potential code injection in ${ env.BUILD_CONTEXT } Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| file: ${{ env.BUILD_DOCKERFILE }} | ||
| platforms: ${{ env.BUILD_PLATFORMS }} | ||
| tags: ${{ env.BUILD_TAGS }} | ||
| build-args: ${{ env.BUILD_ARGS }} | ||
| push: ${{ inputs.push }} | ||
| provenance: mode=max | ||
| sbom: true | ||
|
|
||
| - if: ${{ inputs.attest && inputs.push }} | ||
| name: Attest build provenance | ||
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | ||
| with: | ||
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE }} | ||
| subject-digest: ${{ steps.build.outputs.digest }} | ||
| push-to-registry: true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| # Copyright 2026 ResQ Software | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Reusable .NET CI — restore, build, test, format check. Callable from | ||
| # any resq-software/* repo with a .sln or csproj tree. Pairs with | ||
| # security-scan.yml. | ||
| # | ||
| # Security: caller-controlled string inputs are forwarded through env: | ||
| # and expanded by bash directly (not via `sh -c`, which would re-parse | ||
| # and reintroduce the injection path). | ||
|
|
||
| name: dotnet-ci | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| dotnet-version: | ||
| type: string | ||
| required: false | ||
| default: "9.0.x" | ||
| solution: | ||
| description: Path to .sln or .csproj (empty = dotnet auto-detect). | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| working-directory: | ||
| type: string | ||
| required: false | ||
| default: "." | ||
| configuration: | ||
| type: string | ||
| required: false | ||
| default: "Release" | ||
| run-format-check: | ||
| type: boolean | ||
| required: false | ||
| default: true | ||
| run-test: | ||
| type: boolean | ||
| required: false | ||
| default: true | ||
| timeout-minutes: | ||
| type: number | ||
| required: false | ||
| default: 20 | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| build: | ||
| name: Build | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: ${{ inputs.timeout-minutes }} | ||
| defaults: | ||
| run: | ||
| working-directory: ${{ inputs.working-directory }} | ||
| steps: | ||
| - name: Harden Runner | ||
| uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2 | ||
| with: | ||
| egress-policy: audit | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4 | ||
| with: | ||
| dotnet-version: ${{ inputs.dotnet-version }} | ||
| - name: Restore | ||
| env: | ||
| SLN: ${{ inputs.solution }} | ||
| run: | | ||
| # $SLN may be empty (auto-detect) or a path — unquoted | ||
| # expansion handles both without re-parsing. | ||
| # shellcheck disable=SC2086 | ||
| dotnet restore $SLN | ||
| - name: Build | ||
| env: | ||
| SLN: ${{ inputs.solution }} | ||
| CONFIG: ${{ inputs.configuration }} | ||
| run: | | ||
| # shellcheck disable=SC2086 | ||
| dotnet build $SLN -c "$CONFIG" --no-restore | ||
|
|
||
| format: | ||
| if: ${{ inputs.run-format-check }} | ||
| name: Format check | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| defaults: | ||
| run: | ||
| working-directory: ${{ inputs.working-directory }} | ||
| steps: | ||
| - name: Harden Runner | ||
| uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2 | ||
| with: | ||
| egress-policy: audit | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4 | ||
| with: | ||
| dotnet-version: ${{ inputs.dotnet-version }} | ||
| - name: dotnet format | ||
| env: | ||
| SLN: ${{ inputs.solution }} | ||
| run: | | ||
| # shellcheck disable=SC2086 | ||
| dotnet format $SLN --verify-no-changes --severity warn | ||
|
|
||
| test: | ||
| if: ${{ inputs.run-test }} | ||
| name: Test | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: ${{ inputs.timeout-minutes }} | ||
| defaults: | ||
| run: | ||
| working-directory: ${{ inputs.working-directory }} | ||
| steps: | ||
| - name: Harden Runner | ||
| uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2 | ||
| with: | ||
| egress-policy: audit | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4 | ||
| with: | ||
| dotnet-version: ${{ inputs.dotnet-version }} | ||
| - name: dotnet test | ||
| env: | ||
| SLN: ${{ inputs.solution }} | ||
| CONFIG: ${{ inputs.configuration }} | ||
| run: | | ||
| # shellcheck disable=SC2086 | ||
| dotnet test $SLN -c "$CONFIG" --verbosity normal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$FLAGSword-splitting still allows caller-controlled argument injection into cmake.Routing
cmake-flagsthroughenv:prevents shell injection into the YAML-generated script (addressing the CodeQL finding), but$FLAGSis intentionally left unquoted so a caller can pass multiple flags. That means a caller can still inject arbitrary cmake arguments — e.g.-DCMAKE_CXX_COMPILER_LAUNCHER=...,-P <script>, or extra;-separated generator expressions — which on a reusable workflow invoked from a consumer repo is effectively arbitrary code execution on the runner via the build step.Since this is a reusable workflow consumed across the org, the trust boundary is "whatever the calling repo's workflow passes in", which for PRs from forks can be attacker-controlled. Consider one of:
cmake-flagsis trusted input and must not be derived from PR-event context in consumers.cmake-flagsas a JSON array and expanding viajq -r '.[]'into abasharray, preserving per-arg quoting:♻️ Array-based expansion
At minimum, add a comment to the workflow header making the trust assumption on
cmake-flagsexplicit.🤖 Prompt for AI Agents