Repository navigation
sec(ci): unprotect only the RDS instance each state owns before destroy #2875
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: pre-commit | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| jobs: | |
| pre-commit: | |
| name: Run pre-commit hooks | |
| runs-on: ubuntu-latest | |
| # 35 minutes accommodates the 3-attempt retry wrapper on the | |
| # `Run pre-commit` step below (3 attempts * 10 min per-attempt | |
| # timeout + 2 * 90 s retry waits = 33 min worst case) plus a | |
| # small margin for setup/install steps. Without the bump, the | |
| # job-level cap killed any retry attempt before it could start, | |
| # making the retry policy ineffective (CR finding on #697). | |
| timeout-minutes: 35 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| # Read from go.mod rather than a hardcoded string, matching | |
| # aws_sanity / azure_sanity / database-migration. One fewer place the | |
| # Go version has to be bumped by hand (issue #1833). | |
| go-version-file: go.mod | |
| - name: Set up Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "24" | |
| # Cache the npm download cache keyed on the frontend lockfile. | |
| # Saves ~30-40s per run vs an uncached `npm ci`. Same pattern | |
| # already used in frontend-build-sentinel.yml. | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Set up Terraform | |
| # Required by the terraform_fmt + terraform_validate pre-commit hooks. | |
| # terraform_validate calls `terraform init` per module, which the | |
| # action wraps with HTTP-cached provider downloads. | |
| # | |
| # Pin must satisfy `required_version = ">= 1.10.0"` declared by every | |
| # `terraform/environments/*/main.tf` — pinning to a sub-1.10 version | |
| # makes init abort before validate even runs. Action major matches | |
| # `.github/workflows/ci.yml` so both workflows resolve to the same | |
| # Terraform binary; otherwise a behavioural drift between the two | |
| # could pass one and fail the other. | |
| uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 | |
| with: | |
| terraform_version: "1.10.5" | |
| terraform_wrapper: false | |
| - name: Install tflint | |
| # Pinned to a release tag (not master) so a malicious or accidental | |
| # change to install_linux.sh on master can't silently land on this | |
| # CI runner. `curl -fsSL` makes transport errors fail loudly | |
| # instead of writing an HTML error page to stdin and feeding it | |
| # to bash. | |
| env: | |
| TFLINT_VERSION: v0.55.0 | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL -o /tmp/tflint-install.sh \ | |
| "https://raw.githubusercontent.com/terraform-linters/tflint/${TFLINT_VERSION}/install_linux.sh" | |
| bash /tmp/tflint-install.sh | |
| # Cache the tflint ruleset plugins (aws/azurerm/google) that | |
| # `tflint --init` downloads from the GitHub Releases API. Without | |
| # this cache EVERY run re-downloads all three plugins and is exposed | |
| # to transient GitHub release-API 503s — a sustained 503 outrode the | |
| # GITHUB_TOKEN auth + 3-attempt pre-commit retry below and reddened | |
| # this job repo-wide (blocking every PR). Keyed on .tflint.hcl so a | |
| # plugin-version bump re-downloads; restore-keys seeds a warm start. | |
| - name: Cache tflint plugins | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.tflint.d/plugins | |
| key: tflint-plugins-${{ runner.os }}-${{ hashFiles('.tflint.hcl') }} | |
| restore-keys: | | |
| tflint-plugins-${{ runner.os }}- | |
| # Pre-populate the plugin cache with a dedicated, authenticated, | |
| # retried `tflint --init` BEFORE pre-commit runs. On a cache hit this | |
| # is a fast no-op (tflint skips download when the pinned plugin | |
| # versions are already present — no API call, so immune to the 503). | |
| # On a cache miss (version bump / cold cache) the retry loop rides | |
| # out transient release-API 503s at the init level instead of | |
| # re-running every hook via the coarse outer retry. GITHUB_TOKEN | |
| # raises the release-API limit above the 60/hr anonymous ceiling. | |
| - name: Initialize tflint plugins | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -uo pipefail | |
| for attempt in 1 2 3 4 5 6; do | |
| if tflint --init --config="${GITHUB_WORKSPACE}/.tflint.hcl"; then | |
| echo "tflint --init succeeded (attempt ${attempt})" | |
| exit 0 | |
| fi | |
| wait=$((attempt * 20)) | |
| echo "tflint --init failed (attempt ${attempt}/6); retrying in ${wait}s..." >&2 | |
| sleep "${wait}" | |
| done | |
| echo "tflint --init failed after 6 attempts — GitHub release API likely unavailable" >&2 | |
| exit 1 | |
| # Cache the installed tool binaries (gosec, gocyclo). Keyed on the | |
| # pinned version strings so a tool-version bump still triggers a | |
| # fresh install. Both binaries land in ~/go/bin which setup-go@v6 | |
| # already adds to PATH. Restored BEFORE the install steps so the | |
| # `if: cache-hit != 'true'` guards below can short-circuit them on | |
| # cache-hit runs (the `go install` invocations cost ~3-5s each | |
| # even when the module cache is warm; skipping them on cache-hit | |
| # is worth the extra `if`). | |
| - name: Cache Go-installed tools (gosec, gocyclo) | |
| id: cache-go-tools | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/go/bin | |
| key: go-tools-${{ runner.os }}-gosec-v2.28.0-gocyclo-v0.6.0 | |
| - name: Install gosec | |
| if: steps.cache-go-tools.outputs.cache-hit != 'true' | |
| # Pinned to the same version ci.yml's `securego/gosec` Action uses, | |
| # so an upstream gosec release with rule changes can't silently | |
| # downgrade the gate between the two workflows. | |
| run: go install github.com/securego/gosec/v2/cmd/gosec@v2.28.0 | |
| - name: Install gocyclo | |
| if: steps.cache-go-tools.outputs.cache-hit != 'true' | |
| # Pinned to match ci.yml — security tool installs must not use | |
| # @latest; that's exactly the supply-chain weakness this PR is | |
| # closing for Dockerfile FROMs. | |
| run: go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 | |
| - name: Install Trivy | |
| # Pinned to v0.69.3 (the latest release with published GitHub-release | |
| # tarballs as of writing). Tags exist for v0.58 onwards but several | |
| # mid-range releases skipped publishing assets to the Releases page; | |
| # the install.sh script fetches via GitHub Releases, so picking one | |
| # of those tags makes install bail silently after detecting the | |
| # version. v0.69.3 ships the standard `trivy_<ver>_Linux-64bit.tar.gz` | |
| # asset. | |
| # | |
| # The installer itself is fetched from the same pinned release tag | |
| # (not the mutable `main` branch) and downloaded to a file before | |
| # execution, matching the tflint step above: a malicious or | |
| # accidental change to install.sh on main can't silently execute | |
| # on this CI runner, and `curl -fsSL` makes transport errors fail | |
| # loudly instead of piping an HTML error page into sh. | |
| env: | |
| TRIVY_VERSION: v0.69.3 | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL -o /tmp/trivy-install.sh \ | |
| "https://raw.githubusercontent.com/aquasecurity/trivy/${TRIVY_VERSION}/contrib/install.sh" | |
| sh /tmp/trivy-install.sh -b /usr/local/bin "${TRIVY_VERSION}" | |
| - name: Install git-secrets | |
| # Pinned to a release tag rather than master HEAD. After install | |
| # we register the AWS pattern set and ASSERT at least one pattern | |
| # was registered — without the assert, a registration failure | |
| # produces a patternless scanner that exits 0 unconditionally, | |
| # leaving the gate silently downgraded. | |
| run: | | |
| set -euo pipefail | |
| git clone --depth 1 --branch 1.3.0 https://github.com/awslabs/git-secrets.git /tmp/git-secrets | |
| sudo make -C /tmp/git-secrets install | |
| git secrets --register-aws --global | |
| git secrets --list --global | grep -q '.' || { | |
| echo "git-secrets registration produced no patterns — gate would be silently disabled" | |
| exit 1 | |
| } | |
| # Note: the local `hadolint` hook in .pre-commit-config.yaml (search for | |
| # `id: hadolint`; no line number, because this comment has already gone | |
| # stale twice as that file shifted) runs ghcr.io/hadolint/hadolint pinned | |
| # by digest. The version lives in that entry and is the single source of | |
| # truth; do not restate it here, or this comment rots again. We do NOT | |
| # install a host binary here — it would be dead code (never invoked by | |
| # the hook) AND a supply-chain hole (latest tag, no checksum). Note: | |
| # an earlier version of this comment claimed the hook already pinned | |
| # the image to v2.14.0 via the hook repo's `rev:` -- it did not; that | |
| # `rev:` only pins hadolint's *hook definition*, whose upstream entry | |
| # (`ghcr.io/hadolint/hadolint hadolint`) has no image tag and floats to | |
| # `:latest`. See the digest pin in .pre-commit-config.yaml for the fix. | |
| # If a future change switches the hook to a host binary, install a | |
| # pinned + sha256-verified binary here. | |
| - name: Install pre-commit | |
| run: pip install 'pre-commit==4.0.1' | |
| # Cache pre-commit's per-hook environments (Go, Python, Node, etc. | |
| # virtualenvs it builds on first run). Keyed on the hook config | |
| # because pre-commit will rebuild any env whose pinned rev changes. | |
| # Saves ~30-60s per cache-hit run; safe because pre-commit verifies | |
| # env integrity on use. | |
| - name: Cache pre-commit environments | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.cache/pre-commit | |
| key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} | |
| restore-keys: | | |
| pre-commit-${{ runner.os }}- | |
| # Cache the Go build cache so `go vet`, `gosec`, and any other | |
| # Go-compiling hooks reuse compiled object files instead of | |
| # rebuilding from source. setup-go@v6 caches ~/go/pkg/mod | |
| # (modules) but NOT ~/.cache/go-build (compiled output) — this | |
| # step covers the latter. Keyed on go.sum so a dep upgrade still | |
| # invalidates the cache and gets clean builds. | |
| - name: Cache Go build cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.cache/go-build | |
| key: go-build-${{ runner.os }}-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| go-build-${{ runner.os }}- | |
| - name: Install frontend deps | |
| run: | | |
| if [ -f frontend/package-lock.json ]; then | |
| cd frontend && npm ci | |
| fi | |
| - name: Run pre-commit | |
| # SKIP=terraform_validate: that hook calls `terraform init` per | |
| # module, which creates `.terraform.lock.hcl` files. Those are | |
| # gitignored, so on a fresh CI checkout they don't exist and the | |
| # init step "modifies files", which pre-commit reports as a | |
| # failure. Local pre-commit runs work because lock files persist | |
| # between invocations. terraform_fmt and terraform_tflint still | |
| # run and catch the syntax/style issues that terraform_validate | |
| # would catch; the deeper schema validation runs in | |
| # `terraform plan` during deploy workflows. | |
| # | |
| # GITHUB_TOKEN is passed so terraform_tflint's `tflint --init` | |
| # step authenticates against the GitHub API (5000/hr per-token) | |
| # when it downloads ruleset plugin releases. Without the token, | |
| # tflint goes anonymous and hits the 60/hr per-IP limit shared | |
| # across every workflow on the runner's NAT IP, which trips | |
| # intermittently when PRs land in the same hour (issue #564). | |
| # | |
| # nick-fields/retry wraps the run with up to 3 attempts and a | |
| # 90-second wait so transient flakes (GitHub Releases blips, | |
| # tflint plugin download timeouts, etc.) do not require a | |
| # manual rerun. The GITHUB_TOKEN fix above is the primary fix; | |
| # the retry wrapper is the cheap defense-in-depth for the | |
| # residual flakes that token alone cannot eliminate. | |
| uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 | |
| env: | |
| # SKIP the `gosec` pre-commit hook in CI: it is designed to scan | |
| # only the changed packages of a local commit, but `pre-commit run | |
| # --all-files` (this CI job) feeds it EVERY .go file across all six | |
| # modules at once, and gosec's whole-repo analysis exhausts the | |
| # runner's memory — the job dies with "The runner has received a | |
| # shutdown signal" at this step on every run. gosec is NOT dropped: | |
| # the dedicated `Security Scanning` job in ci.yml runs gosec v2.28.0 | |
| # per-module (SARIF) as the authoritative gate, so this only removes | |
| # the duplicate that OOMs CI — the same dedup rationale as the | |
| # already-skipped `terraform_validate` (covered by Validate Terraform). | |
| # Local developers still get the fast per-changed-package gosec hook. | |
| SKIP: terraform_validate,gosec | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| timeout_minutes: 10 | |
| max_attempts: 3 | |
| retry_wait_seconds: 90 | |
| command: pre-commit run --all-files |