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
41 changes: 41 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# actionlint configuration — see `just lint-actions`.
#
# Why this exists: a malformed `${{ }}` expression makes a workflow file invalid,
# and GitHub does not tell you until the workflow is triggered. For a reusable
# workflow like service-refresh.yml, which prod.yml and staging.yml both call,
# that means the first thing it breaks is a deploy. Two such errors shipped in
# #1139 and were only noticed from the Actions tab. actionlint catches them at
# commit time; this file is what keeps its output at zero so a new finding is
# visible instead of buried.
#
# The ignores below are a documented baseline of findings that predate the linter
# being introduced. Each is accepted for a stated reason, not silenced because it
# was inconvenient. Do not add to this list to make a new finding go away —
# adding an entry here is a decision that needs the same justification as these.

self-hosted-runner:
labels:
- self-hosted-arm64

paths:
.github/workflows/**:
ignore:
# service-refresh.yml types its workflow_call toggles as `string` and
# accepts either form at the point of use (`== true || == 'true'`), which is
# deliberate: the same inputs are passed from workflow_dispatch (boolean)
# and from three calling workflows. actionlint is stricter than the runtime
# here. Retyping them is a behavior change and belongs in its own PR.
- 'bool value cannot be assigned'
# ACTIONS_TOKEN is an organization-level secret, so it is not declared in
# any workflow_call `secrets:` block and actionlint cannot see it.
- 'property "actions_token" is not defined'
# Composite action inputs do not support `type:`; GitHub ignores the key
# rather than failing. Harmless, and cleaning it touches three actions
# unrelated to whatever change is in flight.
- 'unexpected key "type" for definition of input'
# graph-asg-refresh.yml offers an empty `tier` choice on purpose — it means
# "all tiers".
- 'string should not be empty'
# A required workflow_call input with a default: the default is unreachable,
# which is untidy but not wrong.
- 'but it is also required'
18 changes: 13 additions & 5 deletions .github/workflows/service-refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,15 @@ jobs:
# shorter than the 30-minute default wait, so a genuinely busy instance
# could never be waited out.
#
# Computed in shell, NOT in a `${{ }}` expression: GitHub Actions
# Computed in shell, NOT in a workflow expression: GitHub Actions
# expressions have no arithmetic operators (the operator set is
# grouping, index, dereference, !, comparisons, && and ||), so a `+`
# there is a workflow parse error, not a value.
# in one is a workflow parse error, not a value.
#
# Do not write the literal dollar-brace-brace sequence in this block,
# not even in a comment: the expression parser scans the whole `run:`
# body and does not know what a shell comment is, so an empty or
# malformed one here invalidates the entire workflow file.
MAX_WAIT="${{ inputs.max_wait_minutes || '30' }}"
if ! [[ "$MAX_WAIT" =~ ^[0-9]+$ ]]; then
echo "::error::max_wait_minutes must be a positive integer, got '$MAX_WAIT'"
Expand Down Expand Up @@ -259,9 +264,12 @@ jobs:
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-latest' || fromJSON(inputs.runner_config) }}
# Derived from the busy-wait ceiling by the collect job (see its "Compute
# refresh job timeout" step) rather than hardcoded here, so the two cannot
# drift. The `|| 45` is a floor for the impossible case of an empty output,
# since an empty timeout-minutes is not a valid value.
timeout-minutes: ${{ needs.collect-graph-instances.outputs.job_timeout_minutes || 45 }}
# drift. fromJSON is required, not decorative: job outputs are always
# strings and timeout-minutes must be a number, so passing the output
# through raw is a type error that invalidates the workflow. A `|| 45`
# fallback has the same problem — it yields a string — and is unnecessary
# anyway, since this job only runs when the collect job set the output.
timeout-minutes: ${{ fromJSON(needs.collect-graph-instances.outputs.job_timeout_minutes) }}
permissions:
id-token: write
contents: read
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,17 @@ jobs:
echo "🔍 Linting CloudFormation templates..."
uv run cfn-lint

- name: Lint GitHub Actions workflows
timeout-minutes: 1
run: |
echo "🔍 Linting GitHub Actions workflows..."
# GitHub does not validate a workflow file until it is triggered, so a
# malformed expression is invisible until it breaks a run — and for a
# reusable workflow like service-refresh.yml the run it breaks is a
# deploy. Accepted pre-existing findings are baselined in
# .github/actionlint.yaml; anything reported here is new.
uv run actionlint -shellcheck=

- name: Scan dependencies for vulnerabilities
uses: aquasecurity/trivy-action@v0.36.0
with:
Expand Down
19 changes: 19 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ test-all:
@just format
@just typecheck
@just cf-lint-all
@just lint-actions

# Run tests (exclude slow tests)
test module="":
Expand Down Expand Up @@ -156,6 +157,7 @@ test-code:
@just format
@just typecheck
@just cf-lint-all
@just lint-actions

# Run linting
lint fix="":
Expand Down Expand Up @@ -187,6 +189,23 @@ cf-lint template:
cf-lint-all:
@uv run cfn-lint -t cloudformation/*.yaml

# Lint GitHub Actions workflows and composite actions.
#
# GitHub does not validate a workflow file until it is triggered, so a malformed
# expression is invisible until it breaks a run — and for a reusable workflow like
# service-refresh.yml, the run it breaks is a deploy. Accepted pre-existing
# findings are baselined in .github/actionlint.yaml, so any output here is new.
#
# shellcheck integration is off: it reports dozens of info-level SC2086 quoting
# notes across workflows that predate this recipe, which would drown the errors
# that actually invalidate a file. Run `just lint-actions-shell` to see them.
lint-actions:
@uv run actionlint -shellcheck=

# Same, with shellcheck integration on (noisy; not part of the gate)
lint-actions-shell:
@uv run actionlint

# Validate the rs-gaap framework: structure + package integrity + CoA coverage (--summary terse, --coverage-only for just coverage)
framework-validate *args="":
UV_ENV_FILE={{_local_env}} uv run python -m robosystems.scripts.framework_validate {{args}}
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ build-backend = "setuptools.build_meta"
# PEP 735 dependency groups - isolated from main project dependencies
# Use: uv export --only-group lambda --no-hashes --frozen
[dependency-groups]
dev = [
"actionlint-py>=1.7,<2",
]
lambda = [
"boto3>=1.39.0,<2.0", # AWS SDK (all Lambdas)
"psycopg2-binary>=2.9.0,<3.0", # PostgreSQL (postgres-init, postgres-rotation)
Expand Down
81 changes: 0 additions & 81 deletions tests/infrastructure/test_workflow_expressions.py

This file was deleted.

10 changes: 10 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.