.github/.github/workflows/ci-python-sam.yaml
Adam Moussa 1562cbda8e
ci: scope the three reusable CI workflows to contents:read
ci-python-sam, ci-typescript-cdk and ci-dotnet declared no permissions
at any level, unlike every other workflow here. A reusable workflow that
declares nothing inherits the CALLER's token scopes, and these are
called from deploy repos, so a lint/test/synth job could run holding an
OIDC-mintable token it has no use for. None of the three references
GITHUB_TOKEN, github.token, gh, or any secret, so contents:read is all
they need to check out and build.

Also pass node-version explicitly in the cdk-deploy and ci-node
templates. cicd.md requires callers to pin it so lockfileVersion 3 from
local Node 24 / npm 11 cannot drift from the runner, but no template
did. Only these two targets accept the input; sam-deploy, dotnet-eb,
dependency-review and labeler do not, so they are left alone.

Verified against all 22 callers across the org that none grants
permissions omitting contents:read, so no repo's CI breaks on the
caller-cannot-be-exceeded rule.
2026-07-28 12:13:07 -04:00

157 lines
5.2 KiB
YAML

name: CI — Python / SAM
on:
workflow_call:
inputs:
python-version:
description: "Python version to use"
type: string
default: "3.12"
source-dirs:
description: "Space-separated directories for ruff (default: repo root)"
type: string
default: "."
run-tests:
description: "Run pytest"
type: boolean
default: false
run-sam-validate:
description: "Run sam validate --lint"
type: boolean
default: true
sam-template:
description: "Path to SAM template file"
type: string
default: "template.yaml"
run-cdk-synth:
description: "Run cdk synth (for Python CDK repos)"
type: boolean
default: false
cdk-dir:
description: "Directory containing cdk.json"
type: string
default: "cdk"
node-version:
description: "Node.js version for CDK CLI"
type: string
default: "24"
enable-qemu:
description: "Enable QEMU so cdk synth can bundle arm64 Lambda assets on x86 runners"
type: boolean
default: false
run-conventions-check:
description: "Run lightweight conventions audit"
type: boolean
default: true
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: ci-python-sam-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ inputs.python-version }}
- name: Install ruff
run: pip install 'ruff==0.15.22'
- name: Ruff check
run: ruff check ${{ inputs.source-dirs }}
- name: Ruff format check
run: ruff format --check ${{ inputs.source-dirs }}
- name: Install Python dependencies
if: ${{ inputs.run-tests || inputs.run-cdk-synth }}
run: |
if [ "${{ inputs.run-tests }}" = "true" ]; then
pip install pytest
fi
for req in $(find . -name requirements.txt -not -path './.aws-sam/*'); do
pip install -r "$req"
done
- name: Run tests
if: ${{ inputs.run-tests }}
run: pytest
- name: Setup Node.js
if: ${{ inputs.run-cdk-synth }}
uses: actions/setup-node@v7
with:
node-version: ${{ inputs.node-version }}
- name: Set up QEMU
if: ${{ inputs.run-cdk-synth && inputs.enable-qemu }}
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: CDK synth
if: ${{ inputs.run-cdk-synth }}
working-directory: ${{ inputs.cdk-dir }}
run: npx -y cdk synth --quiet
- name: Conventions check
if: ${{ inputs.run-conventions-check }}
run: |
errors=0
warn() { echo "::warning::$1"; }
fail() { echo "::error::$1"; errors=$((errors + 1)); }
# README must exist
if [[ ! -f README.md ]]; then
fail "Missing README.md"
fi
# .gitignore must cover .env
if [[ -f .gitignore ]]; then
if ! grep -qE '^\.env$|^\.env\b' .gitignore; then
fail ".gitignore does not include .env"
fi
else
fail "Missing .gitignore"
fi
# SAM: check template for non-arm64 and missing log retention
TEMPLATE="${{ inputs.sam-template }}"
if [[ -f "$TEMPLATE" ]]; then
if grep -qi 'x86_64' "$TEMPLATE" 2>/dev/null; then
fail "SAM template: Lambda using x86_64 instead of arm64"
fi
if grep -qi 'AWS::Serverless::Function' "$TEMPLATE" 2>/dev/null; then
if ! grep -qi 'RetentionInDays\|AWS::Logs::LogGroup' "$TEMPLATE" 2>/dev/null; then
warn "SAM template: Lambda found but no explicit log retention"
fi
fi
# Flag HARDCODED secret values in the template. Secrets Manager
# references (e.g. SLACK_BOT_TOKEN_SECRET: my-app/slack-token) and
# intrinsic functions (!Ref/!Sub/{{resolve:...}}) are the correct
# pattern, so match on the value's shape — not the key name, which
# legitimately contains words like TOKEN/SECRET when pointing at a
# Secrets Manager id.
if grep -qiE '(xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|gh[posu]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9]{20,}|-----BEGIN[[:space:]][A-Z ]*PRIVATE KEY-----)' "$TEMPLATE" 2>/dev/null; then
fail "SAM template: hardcoded secret in template — use Secrets Manager"
fi
fi
if [[ $errors -gt 0 ]]; then
echo "Conventions check failed with $errors error(s)."
exit 1
fi
echo "Conventions check passed."
- name: Setup SAM CLI
if: ${{ inputs.run-sam-validate }}
uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3
- name: SAM validate
if: ${{ inputs.run-sam-validate }}
run: sam validate --lint --template ${{ inputs.sam-template }}